文章目录
- 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 传输出现 粘包/拆包 的问题,这时就需要通过应用层来解决了。
应用层一般有三种解决方案:根据固定的消息长度分割消息、根据固定的分隔符分割消息 和 通过传输的消息长度分割消息。最常用的第三种方案,前两种方案有一定的缺陷。