- 粘包和拆包
- 发生原因
- 业界解决策略
- Netty提供的策略
- LineBasedFrameDecoder
- DelimiterBasedFrameDecoder
- FixLengthFrameDecoder
1. 粘包和拆包
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
- 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
- 特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。
2. 发生原因
(1)UDP协议是否会发生粘包或者拆包问题
- 答:不会。UDP是基于报文发送的,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开
(2)TCP发生粘包和拆包的原因
通俗的讲:有两点原因
- TCP是基于字节流的,TCP把报文数据块看成一连串无结构的字节流,没有边界
- 在TCP的首部没有表示数据长度的字段
具体的讲:有三个原因
- 3滑动窗口:为了防止客户端发送过快,TCP有滑动窗口机制,服务端和客户端都设立了缓存区
- 拆包情况1:如果发送方的窗口只剩128,而要发包的大小256,那么就会拆包
- 粘包情况1:如果发送方的窗口为500,TCP将多次写入缓冲区的数据一次发送出去(256 + 244),将会发生粘包
- 粘包情况2:接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
- MSS / MTU限制
- MSS:表示TCP报文中data部分的最大长度,根据MSS计算得出,MSS长度=MTU长度-IP Header-TCP Header
- MTU:链路层对一次可以发送的最大数据的限制,默认1500个字节
- 当需要传输的数据大于MSS或者MTU时,数据会被拆分成多个包进行传输
- Nagle算法
- 见Reference
3. 解决策略
解决策略
-
消息定长,例如每个报文固定200字节,如果不够,空位补空格
-
在包尾增加回车换行符进行分割,如FTP协议
-
将消息分为消息头和消息体,消息头中包含消息的长度,字段等信息 。通常消息头第一个字段用int32表示消息总长度
-
更复杂的应用层协议
4. Netty解决策略:
-
LineBasedFrameDecoder:将回车换行符作为消息结束符
-
DelimiterBasedFrameDecoder:将固定字符作为分割依据
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
//单条消息最大长度为1024,用"$_"分割
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
-
FixLengthFrameDecoder:固定长度
ch.pipeline().addLast(new FixLengthFrameDecoder(20)); //以20为长度
Reference