簡單地說,網絡通信時由於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的源碼。