高性能網絡框架Netty的TCP拆包、粘包解決方案

簡單地說,網絡通信時由於TCP會對傳輸的數據進行對用戶透明的拆分與重新組裝,然後將拆分後的分別發送,而我們接收時要獲取發送時的數據報,如何再對其拆分與組裝,以便於我們能知道報文的意思,這個提取報文的過程就是TCP的拆包與粘包,在我們自己做底層的通信設計時,這是必須要考慮的。結合最近在做一個和通信相關的項目,本文講幾個經典且常用的幾種粘包與拆包方法及其在Netty中的實現,Netty是高性能的通信框架,Netty和另一個通信框架Apache的MINA比較像,而且他們作者相同。關於Netty4與MINA2我做過一次比較總結,並將PPT上傳在了網上,地址:https://download.csdn.net/download/zhaowen25/9128699

進入主題,Netty提供的拆包與粘包工具類:

1、 基於長度字段
 io.netty.handler.codec.LengthFieldPrepender

類關係圖如下:

原理和下面的io.netty.handler.codec.LengthFieldBasedFrameDecoder原理類似,不同是這個在編碼的過程使用,

例如原報文數據如下:

 +----------------------------+
  | "HELLO, WORLD" |
 +----------------------------+

長度佔2個字節且不包含本身的拆包粘包結果如下:

 +-----------+--------------------------+
  | 0x000C | "HELLO, WORLD" |
 +-----------+--------------------------+

長度佔2個字節且包含本身的拆包粘包結果如下:

 +------------+----------------------------+
  | 0x000E | "HELLO, WORLD" |
 +------------+----------------------------+

 2、基於界定符解碼器
 io.netty.handler.codec.DelimiterBasedFrameDecoder

類關係圖如下:

原理如下:

假設收到的報文如下:

 +--------------------+
  | ABC\nDEF\r\n |
 +--------------------+

如果以‘\n’爲界定符,則拆包粘包後的報文就是:

 +--------+-------+
  | ABC | DEF |
 +--------+-------+

如果以‘\r\n’爲界定符,則拆包粘包後的報文就是:

 +-----------------+
  | ABC\nDEF |
 +-----------------+


 3、基於定長解碼器
 io.netty.handler.codec.FixedLengthFrameDecoder

類關係圖如下:


 定長就是指定了報文的長度,解析時就是按長度組合截取,原理如下:

假設接收到的報文如下:

 +----+-----+---------+----+
  | A | BC | DEFG | HI |
 +----+-----+---------+----+

當定長參數爲3時,拆包與粘包的結果是:

 +--------+-------+------+
  | ABC | DEF | GHI |
 +--------+-------+------+


4、基於長度字段解碼器
 io.netty.handler.codec.LengthFieldBasedFrameDecoder

類關係圖如下:


 所謂長字段就是在報文裏有說明報文總長度的字段,其實在TCP的報文規則裏就用的這個方法,在頭部存放報文總長或除報頭的內容總長,具體如下:

長度包含長度字段本身且不排除本身的拆包與粘包:

 lengthFieldOffset   = 0     長度字段偏移量
 lengthFieldLength   = 2    長度字段所佔長度
 lengthAdjustment    = 0   
 initialBytesToStrip = 0      (要排除的用於初始化的偏移位置)

         解碼前 (14 bytes)                                    解碼後 (14 bytes)
 +------------+---------------------------+             +------------+---------------------------+
  | Length  |    Actual Content   |   ----->    | Length  |    Actual Content   |
  | 0x000C | "HELLO, WORLD" |             | 0x000C | "HELLO, WORLD" |
 +------------+----------------------------+            +-----------+----------------------------+

長度包含長度字段本身且排除本身的拆包與粘包:

 lengthFieldOffset   = 0     長度字段偏移量
 lengthFieldLength   = 2     長度字段所佔長度
 lengthAdjustment    = 0
 initialBytesToStrip = 2 (排除頭部)

            解碼前 (14 bytes)                        解碼後 (12 bytes)
 +------------+----------------------------+           +---------------------------+
  | Length  |    Actual Content    |     ----->|   Actual Content    |
  | 0x000C | "HELLO, WORLD" |            | "HELLO, WORLD" |
 +------------+----------------------------+          +----------------------------+
 

長度包含長度字段本身且不排除本身的拆包與粘包:

 lengthFieldOffset   =  0   長度字段偏移量
 lengthFieldLength   =  2   長度字段偏移量
 lengthAdjustment    = -2  調整長度 (長度字段所佔長度)
 initialBytesToStrip =  0

      解碼前 (14 bytes)                                   解碼後 (14 bytes)
 +------------+----------------------------+             +-----------+----------------------------+
  | Length  |    Actual Content    |   ----->    | Length  |    Actual Content   |
  | 0x000E | "HELLO, WORLD" |              | 0x000E | "HELLO, WORLD" |
 +------------+----------------------------+             +-----------+----------------------------+

 

有外部頭部的拆包與粘包:

 lengthFieldOffset   = 2        長度字段偏移量 ( = 外部頭部Header 1的長度)
 lengthFieldLength   = 3      長度字段佔用字節數
 lengthAdjustment    = 0
 initialBytesToStrip = 0

                解碼前 (17 bytes)                                                                  解碼後 (17 bytes)
 +--------------+--------------+--------------------------+              +-------------+---------------+--------------------------+
  | Header 1 |     Length   |     Actual Content    |    ----->   | Header 1 |    Length    |     Actual Content    |
  |  0xCAFE   | 0x00000C | "HELLO, WORLD" |                |  0xCAFE   | 0x00000C | "HELLO, WORLD" |
 +--------------+--------------+--------------------------+              +--------------+--------------+--------------------------+

長度字段在前且有擴展頭部的拆包與粘包:

 lengthFieldOffset   = 0   長度字段偏移量
 lengthFieldLength   = 3   長度字段佔用字節數
 lengthAdjustment    = 2 ( Header 1 的長度)
 initialBytesToStrip = 0

                        解碼前 (17 bytes)                                                   解碼後 (17 bytes)
 +----------------+---------------+---------------------------+              +---------------+----------------+---------------------------+
  |    Length   |  Header 1 |    Actual Content   |    ----->    |    Length   |   Header 1 |   Actual Content    |
  | 0x00000C |  0xCAFE  | "HELLO, WORLD" |               | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
 +----------------+---------------+---------------------------+              +---------------+----------------+---------------------------+

多擴展頭部的拆包與粘包:

 lengthFieldOffset   = 1   長度字段偏移量(=頭HDR1的長度)
 lengthFieldLength   = 2   長度字段佔用字節數
 lengthAdjustment    = 1  調整長度(= HDR2的長度)
 initialBytesToStrip = 3     排除的偏移量(= the length of HDR1 + LEN)

                       解碼前 (16 bytes)                                           解碼後 (13 bytes)
 +----------+-----------+----------+----------------------------+              +----------+---------------------------+
  | HDR1 | Length  | HDR2 |   Actual Content     |     ----->    | HDR2 |    Actual Content   |
  | 0xCA | 0x000C |  0xFE | "HELLO, WORLD" |                |  0xFE | "HELLO, WORLD" |
 +---------+------------+----------+---------------------------+               +----------+---------------------------+

調整的多擴展頭部的拆包與粘包:

 lengthFieldOffset   =  1        長度字段偏移量(=頭HDR1的長度)
 lengthFieldLength   =  2      長度字段佔用字節數
 lengthAdjustment    = -3      (= the length of HDR1 + LEN, negative)
 initialBytesToStrip =  3        排除的偏移量(= the length of HDR1 + LEN)

                   解碼前 (16 bytes)                                                        解碼後 (13 bytes)
 +---------+-----------+---------+--------------------------+                 +---------+-------------------------+
  | HDR1 | Length  | HDR2 |    Actual Content     |     ----->     | HDR2 |     Actual Content   |
  |  0xCA  | 0x0010 |  0xFE   | "HELLO, WORLD" |                   |  0xFE  | "HELLO, WORLD" |
 +---------+-----------+---------+--------------------------+                 +---------+-------------------------+


5、基於換行符解碼器
 io.netty.handler.codec.LineBasedFrameDecoder

類關係圖如下:

英文的解釋是:A decoder that splits the received ByteBufs on line endings.

一行的結束標誌包括: "\n" 和 "\r\n",所以又屬於io.netty.handler.codec.FixedLengthFrameDecoder的範疇。


6、關於Netty中的ByteBuf

由於Netty底層是ByteBuf的結構特殊,具有雙指針(讀指針和寫指針)如下:

所以相比MINA的ChannelBuffer的性能要高很多,這也是拆包與粘包的應用之處,就是如何將byte數組轉換成我們想要的Messgae。

從類的關係圖中我們可以看到Netty裏兩種數據流向,其實這是ChannelPipeline(管道)中的兩種處理鏈,如圖所示:

所以處理連接是繼承類ChannelInboundHandlerAdapter,如下:


用Netty創建服務並且用能到這些拆包與粘的地方的代碼如下(第36行處):


總結一下:和拆包與粘包相關的還有就是大小端,也就是高位與低位的位置問題,這些都與編解碼相關,通信相關的問題也是這些問題,

今天基本上講清楚了TCP拆包與粘包,其中引例來自Netty中的註釋,我翻譯了一下,可能有些地方翻譯的不是很到位,感興趣的可以直接看Netty4的源碼。

發佈了56 篇原創文章 · 獲贊 401 · 訪問量 40萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章