文章目錄
- 1. 什么是 粘包/拆包 問題?
- 2. 原因
- 2.1 Nagle 算法
- 2.2 滑動窗口
- 2.3 MSS 限制
- 2.4 粘包的原因
- 2.5 拆包的原因
- 3. 解決方案
- 3.1 固定長度消息
- 3.2 分隔符標識
- 3.3 長度前綴協議
- 3.3.1 案例一
- 3.3.2 案例二
- 3.3.3 案例三
- 4. 總結
1. 什么是 粘包/拆包 問題?
- 粘包 (Sticky Packet):發送方連續發送的 多個獨立數據包,在接收方被合并成 一個數據包 接收,導致應用層無法區分原始消息的邊界。例如,發送方依次發送 A 和 B,接收方可能收到 AB。
- 拆包 (Packet Splitting):發送方發送的 一個完整數據包,在傳輸過程中 被分割成多個小包,接收方需要 重新組裝 才能還原完整消息。例如,發送方發送 ABCD,接收方可能收到 AB 和 CD。
2. 原因
TCP 協議的設計目標是 高效傳輸字節流,而非保證消息邊界。以下機制是導致問題的核心原因:
2.1 Nagle 算法
每個數據包都必須加上 TCP 頭 和 IP 頭,如果要傳遞的數據很少,那么這個數據包中大部分都是頭信息。如果將多個微小數據包合并成一個大數據包,那么網絡利用率就會提高。于是,為了減少網絡中 微小數據包 的數量,TCP 會將多個小數據包合并成一個大包發送,這就是 Nagle 算法。
2.2 滑動窗口
接收方為提高吞吐量,會采取以下兩個措施:
- 延遲發送 ACK 以合并多個數據包的確認。
- 將收到的數據暫存到緩沖區,積累到一定量后再通知應用層讀取。從而導致應用層一次讀取多個數據包。
2.3 MSS 限制
鏈路層對一次能夠發送的最大數據有限制,這個限制稱之為 MTU (Maximum Transmission Unit),不同的鏈路設備的 MTU 值也有所不同,例如:
- 以太網的 MTU 是 1500 字節。
- 本地回環地址的 MTU 是 65535 字節 (本地測試不走網卡)。
MSS 是最大段長度 (Maximum Segment Size),它是 MTU 去除 TCP 頭和 IP 頭后剩余能夠作為數據傳輸的字節數。IPv4 TCP 頭占用 20 字節,IP 頭占用 20 字節,因此以太網 MSS 的值為 1500 - 40 = 1460
字節。TCP 在傳遞大量數據時,會按照 MSS 大小將數據進行分割發送。
2.4 粘包的原因
- Nagle 算法:小數據包會被合并成大數據包,從而導致粘包。
- 滑動窗口:假設 發送方 256 字節表示一個完整報文,但由于 接收方 處理不及時 且 窗口大小足夠大,這 256 字節就會緩沖在 接收方 的滑動窗口中,當滑動窗口中緩沖了多個報文就會粘包。
2.5 拆包的原因
- MSS 限制:當 發送的數據量超過 MSS 限制 后,會將數據切分發送,從而導致拆包。
- 滑動窗口:假設 接收方 的窗口只剩 128 字節,發送方 的報文大小是 256 字節,這時窗口放不下這個報文,只能先發送前 128 字節,等待 ACK 后才能發送剩余部分,這就造成了拆包。
3. 解決方案
TCP 層無法感知消息邊界,因此需要應用層通過來解決,解決方案如下:
3.1 固定長度消息
思想:每條消息的長度固定,接收方按固定長度讀取。
在 Netty 中的實現:將 FixedLengthFrameDecoder
作為 ChannelPipeline
的第一個處理器,如下所示:
// 添加一個 消息長度固定為 512 字節的解碼器
ch.pipeline().addLast(new FixedLengthFrameDecoder(512));
缺點:消息長度不好把握,太短可能無法容納比較長的消息,太長可能會導致浪費。
3.2 分隔符標識
思想:在消息末尾添加特殊分隔符(如 \n
),接收方通過解析分隔符分割消息。
在 Netty 中的實現:將 LineBasedFrameDecoder
或 DelimiterBasedFrameDecoder
作為 ChannelPipeline
的第一個處理器,如下所示:
- 添加一個以換行符為特殊分隔符的解碼器:
// 添加一個解碼器,它以 \n 或 \r\n 為分隔符分割消息 // 但消息長度不能超過 1024 字節,如果超過,會拋出異常 ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
- 添加一個以指定字符串為特殊分隔符的解碼器:
// 指定分隔符為 "EOM" ByteBuf delimiter = Unpooled.copiedBuffer("EOM".getBytes()); // 添加一個解碼器,它以 "EOM" 為分隔符分割消息 // 但消息長度不能超過 1024 字節,如果超過,會拋出異常 ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
缺點:分隔符不好確定,如果內容本身包含了分隔符,那么就會解析錯誤。
3.3 長度前綴協議
思想:在消息前添加固定長度的字段,表示消息總長度。
在 Netty 中的實現:將 LengthFieldBasedFrameDecoder
作為 ChannelPipeline
的第一個處理器,如下所示:
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, // 最大幀(消息)長度0, // 長度字段偏移量4, // 長度字段長度0, // 長度調整值4 // 初始跳過字節數
));
LengthFieldBasedFrameDecoder
的重要參數:
maxFrameLength
:允許的最大幀長度。若接收到的消息長度超出這個值,解碼器會拋出TooLongFrameException
異常,避免內存溢出。lengthFieldOffset
:長度字段在消息中的偏移量,即從消息的哪個位置開始是長度字段。lengthFieldLength
:長度字段本身的字節數。lengthAdjustment
:長度字段的值與實際消息長度之間的調整值。比較復雜,一般不使用。initialBytesToStrip
:解碼后需要跳過的初始字節數。
以下舉出幾個例子幫助理解這幾個參數(參考了 LengthFieldBasedFrameDecoder
的 JavaDoc,Magic
表示校驗消息的魔數,Length
代表消息長度,Actual Content
代表消息內容):
3.3.1 案例一
參數配置:
// 長度字段的長度為 2,長度字段代表消息內容的長度
lengthFieldOffset = 0;
lengthFieldLength = 2;
initialBytesToStrip = 0;
解碼過程:
解碼前 (14 字節) 解碼后 (14 字節)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |---->| Length | Actual Content |
| 0x000C | "Hello, Netty" | | 0x000C | "Hello, Netty" |
+--------+----------------+ +--------+----------------+
3.3.2 案例二
參數配置:
lengthFieldOffset = 0;
lengthFieldLength = 2; // 長度字段的長度為 2
initialBytesToStrip = 2; // 解碼后跳過長度字段
解碼過程:
解碼前 (14 字節) 解碼后 (12 字節)
+--------+----------------+ +----------------+
| Length | Actual Content |---->| Actual Content |
| 0x000C | "Hello, Netty" | | "Hello, Netty" |
+--------+----------------+ +----------------+
3.3.3 案例三
參數配置:
// 魔數字段的長度為 2
lengthFieldOffset = 2; // 長度字段位于魔數字段的右邊,需要偏移 2 字節
lengthFieldLength = 2; // 長度字段的長度為 2
initialBytesToStrip = 4; // 解碼后跳過長度和魔數字段
解碼過程:
解碼前 (16 字節) 解碼后 (12 字節)
+--------+--------+----------------+ +----------------+
| Magic | Length | Actual Content |------->| Actual Content |
| 0x0013 | 0x0010 | "Hello, Netty" | | "Hello, Netty" |
+--------+--------+----------------+ +----------------+
4. 總結
TCP 協議的設計目標是 高效傳遞字節流,所以沒有考慮到消息的邊界。由于 Nagle 算法、滑動窗口、MSS 限制 的因素,可能會導致 TCP 傳輸出現 粘包/拆包 的問題,這時就需要通過應用層來解決了。
應用層一般有三種解決方案:根據固定的消息長度分割消息、根據固定的分隔符分割消息 和 通過傳輸的消息長度分割消息。最常用的第三種方案,前兩種方案有一定的缺陷。