TCP 是基於流傳輸的協議,請求數據在其傳輸的過程當中是沒有界限區分,因此咱們在讀取請求的時候,不必定能獲取到一個完整的數據包。若是一個包較大時,可能會切分紅多個包進行屢次傳輸。同時,若是存在多個小包時,可能會將其整合成一個大包進行傳輸。這就是 TCP 協議的粘包/拆包概念。
本文基於 Netty5 進行分析java
假設當前有123
和abc
兩個數據包,那麼他們傳輸狀況示意圖以下:bootstrap
123
和abc
封裝成了一個包。123
拆分紅了1
和23
,而且1
和abc
一塊兒傳輸。123
和abc
也多是abc
進行拆包。甚至123
和abc
進行屢次拆分也有可能。爲突出 Netty 的粘包/拆包問題,這裏經過例子進行重現問題,如下爲突出問題的主要代碼:segmentfault
服務端:緩存
/** * 服務端網絡事件的讀寫操做類 * * Created by YangTao. */ public class ServerHandler extends ChannelHandlerAdapter { // 接收消息計數器 private int i = 0; // client端消息 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { i++; System.out.print(msg); // 對每條讀取到的消息進行打數標記 System.out.println("================== ["+ i +"]"); // 發送應答消息給客戶端 ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i).getBytes()); ctx.write(rmsg); } // 其餘操做 ....... }
客戶端:網絡
/** * 客戶端發送數據 * * Created by YangTao. */ public class NettyClient { public void send() { Bootstrap bootstrap = new Bootstrap(); NioEventLoopGroup group = new NioEventLoopGroup(); try { bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new StringDecoder()); pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO)); pipeline.addLast(new ClientHandler()); } }); Channel channel = bootstrap.connect(HOST, PORT).channel(); int i = 1; while (i <= 300){ channel.writeAndFlush(String.format("【時間 %s: \t%s】", new Date(), i)); // 打印發送請求的次數 System.out.println(i); i++; } }catch (Exception e){ e.printStackTrace(); }finally { if (group != null) group.shutdownGracefully(); } } }
以上代碼中,咱們第一反應理解的是,若是非異常狀況下客戶端全部數據發送成功,而且服務端所有接收到。那麼從打印信息中能夠看到客戶端的發送次數i
和服務端的接收消息計數i
應該是相同的數。那麼下面經過運行程序,查看打印結果。app
如上圖所示,【】
中的最後一個數字與[]
中數字對上的是已獨立完整的包接收到(粘包/拆包示意圖中的狀況 I)。可是【】
中爲37
和38
的出現了粘包狀況(粘包/拆包示意圖中的狀況 II),兩條數據粘合在一塊兒。ide
上圖中能夠看到【】
中167
的數據被拆分爲了兩部分(圖中畫綠線數據),該狀況爲拆包(粘包/拆包示意圖中的狀況 III)。oop
上面程序沒有考慮到 TCP 的粘包/拆包問題,因此若是是咱們實際應用的程序的話,不能保證數據的正常狀況,就會致使程序異常。測試
Netty 的強大,方便,簡單使用的優點,在粘包/拆包問題上也提供了多種編解碼解決方案,而且很容易理解和掌握。
這裏使用 LineBasedFrameDecoder 和 StringDecoder(將接收到得對象轉換成字符串) 來解決粘包/拆包問題。
只需在服務端和客戶端分別添加 LineBasedFrameDecoder 和 StringDecoder解碼器,由於是雙向會話,因此兩端都要添加,因爲我一開始就添加 StringDecoder 編碼器,因此只需添加 LineBasedFrameDecoder 就夠了。
服務端:編碼
客戶端:
服務端網絡事件操做:
/** * 服務端網絡事件的讀寫操做類 * * Created by YangTao. */ public class ServerHandler extends ChannelHandlerAdapter { // 接收消息計數器 private int i = 0; // client端消息 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { i++; System.out.print(msg); // 對每條讀取到的消息進行打數標記 System.out.println("================== ["+ i +"]"); // 發送應答消息給客戶端 ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i + System.getProperty("line.separator")).getBytes()); ctx.write(rmsg); } // 其餘操做 ....... }
客戶端發送數據:
/** * 客戶端發送數據 * * Created by YangTao. */ public class NettyClient { public void send() { // 鏈接操做 ....... try { // 獲取 channel Channel channel = channel(); int i = 1; ByteBuf buf = null; while (i <= 300){ String str = String.format("【時間 %s: \t%s】", new Date(), i) + System.getProperty("line.separator"); byte[] bytes = str.getBytes(); // 寫入緩衝區 buf = Unpooled.buffer(bytes.length); buf.writeBytes(bytes); channel.writeAndFlush(buf); // 打印發送請求的次數 System.out.println(i); i++; } }catch (Exception e){ e.printStackTrace(); } // 退出操做 ....... } }
細心觀察代碼的變化,應該會發現如今的代碼每次在發送消息的時候,在消息末尾後加了換行分隔符。注意,使用 LineBasedFrameDecoder 時,換行分隔符必須加,不然接收消息端收不到消息,若是手寫換行分割,要記得區分不一樣系統得適配。
通過屢次測試 3W 條請求,沒有再出現過粘包/拆包狀況,看最後一條數據數字是否相同便知。
自定義分隔符和換行分隔符差很少,只需將發送的數據後換行符換成你本身設定的分割符便可。
服務端和客戶端均在 pipeline 添加 DelimiterBasedFrameDecoder:
// 指定的分隔符 public static final String DELIMITER = "$@$"; // 若是當前數據2048個字節中沒有分隔符,就會拋出異常,避免內存溢出。也能夠自定義預檢查當前讀取的數據,自定義這裏超過的規則 pipeline.addLast(new DelimiterBasedFrameDecoder( 2048, Unpooled.wrappedBuffer(DELIMITER.getBytes())) // 分割符緩衝對象 );
設定固定長度,進行數據傳輸,若是不達固定長度,使用空格補全。
服務端和客戶端均在 pipeline 添加 FixedLengthFrameDecoder:
// 100爲指定的固定長度 ch.pipeline().addLast(new FixedLengthFrameDecoder(100));
每次讀取數據時都會按照 FixedLengthFrameDecoder 中設置的固定長度進行解碼,若是出現粘包,那麼會進行屢次解碼,若是出現拆包的狀況,那麼 FixedLengthFrameDecoder 會先緩存當前部分包的信息,當接收下一個包時,會與緩存的部分包進行拼接,知道知足規定的長度。
動態指定長度就是說,每條消息的長度都是隨着消息頭進行指定,這裏使用的編碼器爲 LengthFieldBasedFrameDecoder。
pipeline().addLast( new LengthFieldBasedFrameDecoder( 2048, // 幀的最大長度,即每一個數據包最大限度 0, // 長度字段偏移量 4, // 長度字段所佔的字節數 0, // 消息頭的長度,能夠爲負數 4) // 須要忽略的字節數,從消息頭開始,這裏是指整個包 );
發送消息時,建立本身的消息對象編碼器
// 建立 byteBuf ByteBuf buf = getBuf(); // ..... // 設置該條消息內容長度 buf.writeInt(msg.length()); // 設置消息內容 buf.writeBytes(msg.getBytes("UTF-8"));
服務端讀取的時候就直接讀取便可,沒其餘特殊操做。
除了以上 Netty 提供的現成方案,還能夠經過重寫 MessageToByteEncoder 編碼實現自定義協議。
Netty 極大的爲使用者提供了多種解決粘包/拆包方案,而且能夠很愉快的對多種消息進行自動解碼,在使用過程當中也極容易掌握和理解,很大程度上提高開發效率和穩定性。
我的博客: https://ytao.top
個人公衆號 ytao