6-packet 粘包和半包的原因使用WireShark分析
Opened this issue · 5 comments
粘包,半包原因分析 🐔
客户机的测试用例
我们的测试报文为字符串
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 1000; i++) {
ByteBuf buffer = getByteBuf(ctx);
ctx.channel().writeAndFlush(buffer);
}
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
byte[] bytes = "你好,陌生人!".getBytes(Charset.forName("utf-8"));
System.out.println(bytes.length);
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(bytes);
return buffer;
}
向对等方(服务器)发送19字节的字符串"你好,陌生人!"
,1000次
使用的时UTF-8编码,
6(汉字+中文字符) * 3 + 1(英文符号) = 19字节
其中汉字在UTF-8中占3个字节,ASCII里的字符占1个字节。
结果:
思考:
为什么会出现粘包,半包现象?
为什么现象会连续出现,当又能够恢复?
注:这里恢复针对的是我们传输的字符串,发生粘包,半包后不会一直持续这种状态,这只是针对于该只包含数据(字符串)的数据报文的情况,并不是指所有的粘包,半包后都会恢复。
可能原因
因为我们的连接建立在传输层TCP协议上,而我们发送的字符串,本质上是一种极简的应用层协议报文,区别于HTTP协议,我们发送的字符串没有报文头,只有数据(19字节)。既然建立在TCP上就得从TCP可靠传输的概念上去理解原因。
那么原因可能出现在发送方,接收方中的一方或者双方,如果不针对于我们当前这种数据量小,双方又是在同一主机的情况,应该会有如下几种原因:
- 写入的字节过大;
会受到两个的限制:
- Socket在内核中的缓冲区(
sk_buff
)的大小;- TCP发送队列(
tcp_write_queue
)的大小
可见内核分配给1个TCP连接的缓存空间有限,当字节过大没有多余的内核缓存来复制用户待发送的字节时,就会等待TCP发送一部分字节后,释放得到空间再进行拷贝,也就发生了拆分现象。如过接收方接收到了先发出去的这部分数据,并且被接收进程读取,也就发生了半包 (接收到了残缺数据)。
- 进行了MSS的切分;
当发送数据大于MSS时,会将数据按照MSS进行分片,主要目的是防止IP分片
MSS的大小由MTU决定,例如以太网中的两台主机,MSS大小为 1460字节
MTU(1500) - 20(IPV4 数据报的头部) - 20(TCP的头部) = 1460
MTU由所在局域网决定,当发送和接收方处于同一个主机,那就不存在MTU的限制,MSS的大小为65495字节
65535(2^{16} - 1) - 20(IPV4 数据报的头部) - 20(TCP的头部) == 65495(MSS)
- 进行了IP分片;
MSS,在三次握手时发送和接收方交换各自的MSS,但是不一定一直靠谱,若中间网络的MTU小于MSS,则代表选定的MSS还是太大了,也就出现了IP分片。
- 接收方从内核的接收缓存中读取了过多或者过少的数据;
前面的都是发送方产生的影响,导致发送数据被拆分,接受方进程读取到的数据不完整,进而出现为半包。
其实当发送方的数据完整的按序的到达,如果我们接收数据时未设置字节大小或者某种接收约束,就会导致多读(粘包)少读(半包)。
- 基于UDP协议包的丢失,乱序等。
Wireshark抓包分析
环境:发送方和接收方在一台主机(Win10)
Wireshark:3.00
传输层协议:TCP
编码方式:UTF-8
连接建立过程:
MSS = 65495 ,即不存在数据的拆分。
所有的数据包的长度都是19字节(包括出现粘包,半包时的分组),验证前面数据未拆分的推断。
分析:
写入socket缓冲区的数据没有大于MSS,所以应该未发生拆包,并且TCP发送数据的速度和写入速度是匹配,内核缓冲区空间充足,未出现将数据拆分或者合并现象,即数据的发送方不存在问题!
既然发送方不存在问题,可能的原因也就是4了。数据接收和读取的速度不匹配,发生了多读或者少读。
验证
通过设置定长字节的读取来验证我的推断,如果我限制每一次只能读取19个字节,相当于约定一个完整的数据报文为19个字节,即代表如果接收缓存里没有一个完整的报文到达,会发生等待,如果有多于19个字节的数据也只会读取一个报文的数据。
通过Netty的FixedLengthFrameDecoder拆包器 设置定长接收(19字节):
结果完全符合预料,不再出现粘包和半包。
也可以设置为定长38字节:
会每一次接收2个数据包。
结论
这种处于一台主机的数据量小的,产生粘包半包的原因是,接收方进程从接收缓存中读取数据的大小未被限制。
延伸
- Socket从接收缓存读取数据的长度受什么影响?
- Netty拆包器的原理?