defineYIDA/NoneIM

6-packet 粘包和半包的原因使用WireShark分析

Opened this issue · 5 comments

6-packet 粘包和半包的原因使用WireShark分析

粘包,半包原因分析 🐔

客户机的测试用例

我们的测试报文为字符串

@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个字节。

结果:

发生随机的
粘包:
image

半包:
image
当然也有完整包:
image

思考:

为什么会出现粘包,半包现象?
为什么现象会连续出现,当又能够恢复?

注:这里恢复针对的是我们传输的字符串,发生粘包,半包后不会一直持续这种状态,这只是针对于该只包含数据(字符串)的数据报文的情况,并不是指所有的粘包,半包后都会恢复。

可能原因

因为我们的连接建立在传输层TCP协议上,而我们发送的字符串,本质上是一种极简的应用层协议报文,区别于HTTP协议,我们发送的字符串没有报文头,只有数据(19字节)。既然建立在TCP上就得从TCP可靠传输的概念上去理解原因。
那么原因可能出现在发送方,接收方中的一方或者双方,如果不针对于我们当前这种数据量小,双方又是在同一主机的情况,应该会有如下几种原因:

  1. 写入的字节过大;

会受到两个的限制:

  • Socket在内核中的缓冲区(sk_buff)的大小;
  • TCP发送队列(tcp_write_queue)的大小

可见内核分配给1个TCP连接的缓存空间有限,当字节过大没有多余的内核缓存来复制用户待发送的字节时,就会等待TCP发送一部分字节后,释放得到空间再进行拷贝,也就发生了拆分现象。如过接收方接收到了先发出去的这部分数据,并且被接收进程读取,也就发生了半包 (接收到了残缺数据)。

  1. 进行了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)

  1. 进行了IP分片;

MSS,在三次握手时发送和接收方交换各自的MSS,但是不一定一直靠谱,若中间网络的MTU小于MSS,则代表选定的MSS还是太大了,也就出现了IP分片。

  1. 接收方从内核的接收缓存中读取了过多或者过少的数据;

前面的都是发送方产生的影响,导致发送数据被拆分,接受方进程读取到的数据不完整,进而出现为半包。
其实当发送方的数据完整的按序的到达,如果我们接收数据时未设置字节大小或者某种接收约束,就会导致多读(粘包)少读(半包)。

  1. 基于UDP协议包的丢失,乱序等。

如果想细致理解,还是得了解TCP的发送和接收消息的过程
TCP消息的发送
TCP消息的接收

Wireshark抓包分析

环境:发送方和接收方在一台主机(Win10)
Wireshark:3.00
传输层协议:TCP
编码方式:UTF-8

连接建立过程:
image
MSS = 65495 ,即不存在数据的拆分。

数据传输过程:
image

所有的数据包的长度都是19字节(包括出现粘包,半包时的分组),验证前面数据未拆分的推断。

分析:

写入socket缓冲区的数据没有大于MSS,所以应该未发生拆包,并且TCP发送数据的速度和写入速度是匹配,内核缓冲区空间充足,未出现将数据拆分或者合并现象,即数据的发送方不存在问题!
既然发送方不存在问题,可能的原因也就是4了。数据接收和读取的速度不匹配,发生了多读或者少读。

验证

通过设置定长字节的读取来验证我的推断,如果我限制每一次只能读取19个字节,相当于约定一个完整的数据报文为19个字节,即代表如果接收缓存里没有一个完整的报文到达,会发生等待,如果有多于19个字节的数据也只会读取一个报文的数据。
通过Netty的FixedLengthFrameDecoder拆包器 设置定长接收(19字节):
image
结果完全符合预料,不再出现粘包和半包。
也可以设置为定长38字节:
image
会每一次接收2个数据包。

结论

这种处于一台主机的数据量小的,产生粘包半包的原因是,接收方进程从接收缓存中读取数据的大小未被限制。

延伸

  • Socket从接收缓存读取数据的长度受什么影响?
  • Netty拆包器的原理?