TCP網絡通訊時候會發生粘包/拆包的問題,接下來探討其解決之道。java
什麼是粘包/拆包編程
通常所謂的TCP粘包是在一次接收數據不能徹底地體現一個完整的消息數據。TCP通信爲什麼存在粘包呢?主要緣由是TCP是以流的方式來處理數據,再加上網絡上MTU的每每小於在應用處理的消息數據,因此就會引起一次接收的數據沒法知足消息的須要,致使粘包的存在。處理粘包的惟一方法就是制定應用層的數據通信協議,經過協議來規範現有接收的數據是否知足消息數據的須要。bootstrap
狀況分析服務器
TCP粘包一般在流傳輸中出現,UDP則不會出現粘包,由於UDP有消息邊界,發送數據段須要等待緩衝區滿了纔將數據發送出去,當滿的時候有可能不是一條消息而是幾條消息合併在換中去內,在成粘包;另外接收數據端沒能及時接收緩衝區的包,形成了緩衝區多包合併接收,也是粘包。網絡
解決辦法app
一、消息定長,報文大小固定長度,不夠空格補全,發送和接收方遵循相同的約定,這樣即便粘包了經過接收方編程實現獲取定長報文也能區分。
socket
二、包尾添加特殊分隔符,例如每條報文結束都添加回車換行符(例如FTP協議)或者指定特殊字符做爲報文分隔符,接收方經過特殊分隔符切分報文區分。ide
三、將消息分爲消息頭和消息體,消息頭中包含表示信息的總長度(或者消息體長度)的字段oop
四、更復雜的自定義應用層協議大數據
代碼例子
一、Netty中提供了FixedLengthFrameDecoder定長解碼器能夠幫助咱們輕鬆實現第一種解決方案,定長解碼報文。
服務器端:
package im; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.FixedLengthFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** * 定長解碼 服務器端 * @author xwalker */ public class Server { public void bind(int port) throws Exception{ //接收客戶端鏈接用 EventLoopGroup bossGroup=new NioEventLoopGroup(); //處理網絡讀寫事件 EventLoopGroup workerGroup=new NioEventLoopGroup(); try{ //配置服務器啓動類 ServerBootstrap b=new ServerBootstrap(); b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO))//配置日誌輸出 .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new FixedLengthFrameDecoder(30));//設置定長解碼器 長度設置爲30 ch.pipeline().addLast(new StringDecoder());//設置字符串解碼器 自動將報文轉爲字符串 ch.pipeline().addLast(new Serverhandler());//處理網絡IO 處理器 } }); //綁定端口 等待綁定成功 ChannelFuture f=b.bind(port).sync(); //等待服務器退出 f.channel().closeFuture().sync(); }finally{ //釋放線程資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port=8000; new Server().bind(port); } }
package im; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; /** * 服務器handler * @author xwalker */ public class Serverhandler extends ChannelHandlerAdapter { int counter=0; private static final String MESSAGE="It greatly simplifies and streamlines network programming such as TCP and UDP socket server."; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("接收客戶端msg:["+msg+"]"); ByteBuf echo=Unpooled.copiedBuffer(MESSAGE.getBytes()); ctx.writeAndFlush(echo); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
客戶端:
package im; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.FixedLengthFrameDecoder; import io.netty.handler.codec.string.StringDecoder; /** * 客戶端 * @author xwalker * */ public class Client { /** * 連接服務器 * @param port * @param host * @throws Exception */ public void connect(int port,String host)throws Exception{ //網絡事件處理線程組 EventLoopGroup group=new NioEventLoopGroup(); try{ //配置客戶端啓動類 Bootstrap b=new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true)//設置封包 使用一次大數據的寫操做,而不是屢次小數據的寫操做 .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new FixedLengthFrameDecoder(30));//設置定長解碼器 ch.pipeline().addLast(new StringDecoder());//設置字符串解碼器 ch.pipeline().addLast(new ClientHandler());//設置客戶端網絡IO處理器 } }); //鏈接服務器 同步等待成功 ChannelFuture f=b.connect(host,port).sync(); //同步等待客戶端通道關閉 f.channel().closeFuture().sync(); }finally{ //釋放線程組資源 group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port=8000; new Client().connect(port, "127.0.0.1"); } }
package im; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; /** * 客戶端處理器 * @author xwalker * */ public class ClientHandler extends ChannelHandlerAdapter { private static final String MESSAGE="Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients."; public ClientHandler(){} @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(Unpooled.copiedBuffer(MESSAGE.getBytes())); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("接收服務器響應msg:["+msg+"]"); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
服務器和客戶端分別設置了定長解碼器 長度爲30字節,也就是規定發送和接收一次報文定長爲30字節。
運行結果:
客戶端接收到服務器的響應報文 一段文字被定長分紅若干段接收。
服務器端接收客戶端發送的報文 一段話也是分紅了等長的若干段。
上述是一個簡單長字符串傳輸例子,將一個長字符串分割成若干段。咱們也能夠自定義一系列定長的指令發送出去
例如指令長度都是30個字節,批量發出N條指令,這樣客戶端粘包後發出一個比較大的數據指令集,服務器接收到的數據在緩衝區內,只須要按照定長一個個指令取出來執行便可。