乐趣区

细说-Netty-中的粘包和拆包

TCP/IP 中的“粘包”与“拆包”

“粘包拆包”是个伪命题

确实,我也认为这是个伪命题,tcp 这种双工面向流的协议,本来就没有粘拆包的说法,包的界限问题应该需要由上层的应用处理。

但为什么会有粘拆包问题呢?

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
  3. 进行 MSS(最大报文长度)大小的 TCP 分段,当 TCP 报文长度 -TCP 头部长度 >MSS 的时候将发生拆包。
  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。(例如连接复用时,如不处理包界限问题一定会发生“粘包”,因为 tcp 并不知道接收的数据属于应用的第几次报文)

在应用层角度来观察粘拆包

写一个简易版 TCP Server

    // 接收 4k*1000 大小的数据
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.bind(new InetSocketAddress(9527));
    SocketChannel socketChannel = serverSocketChannel.accept();
    int size = 4096*1000;
    ByteBuffer byteBuffer = ByteBuffer.allocate(size);
    while (byteBuffer.hasRemaining()){int read = socketChannel.read(byteBuffer);
        System.out.println(read);
    }

简易版 TCP Client

    // 发送 4k*1000 大小的数据
    int size = 4096*1000;
    ByteBuffer byteBuffer = ByteBuffer.allocate(size);
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("127.0.0.1",9527));
    for (int i = 0; i < size; i++) {byteBuffer.put((byte) 1);
    }
    byteBuffer.flip();
    while (byteBuffer.hasRemaining()){int write = socketChannel.write(byteBuffer);
        System.out.println(write);
    }

当前环境下,MSS 是 1380,IP MTU=MSS+20bytes(IP 包头)+20bytes(TCP 包头),IP 层分片默认是禁用的(Don`t fragment)

看一下执行结果:

// 服务端打印结果
39672
2736
2736
2736
13680
2736
19152
2736
10944
2736
5472
2736
16416
2736
2736
12312
1368
5472
1368

从日志上看,每次读取的报文最小值是 1368,刚好比 mss 小一点点(mss 只是最大报文段长度,实际可读取的值需要减去各层协议的首部大小,所以最小值是 1368)。每次读取的长度值有波动,但都是 1368 的整数倍。

由此可见,每次可读取的报文大小,都是以 ip 数据报为单位的。接收端每次接收的报文大小也都是以 ip 数据报为单位。

所以在读取报文(tcp buffer)时,也是以 ip 数据报为单位,绝对不会出现读取到半个 ip 包的问题(忽略因 mtu 大小导致的 ip 层分片)。

那么粘包拆包里的这个“包”的最小单位也是一个 IP 数据报

Netty 中的粘拆包处理

Netty 中并没有直接说粘包拆包这个问题,但《Netty 权威指南》这本书上倒解释了粘包拆包,不用纠结这个名词,跟着大多数人叫也没错,错的人多了也就是对的。

Netty 的请求处理是一个 Pipeline 结构,通过 handler 接口,可以定义不同的 encoder/decoder,从而解决粘包拆包(处理包界限)问题,当然也可以自己处理,原理都是相同的。

Netty 中内置了几个编解码器,可以很简单的处理包界限问题。

LengthFieldBasedFrameDecoder

通过在包头增加消息体长度的解码器,解析数据时首先获取首部长度,然后定长读取 socket 中的数据。

LineBasedFrameDecoder

换行符解码器,报文尾部增加固定换行符 rn,解析数据时以换行符作为报文结尾。

DelimiterBasedFrameDecoder

分隔符解码器,使用特定分隔符作为报文的结尾,解析数据时以定义的分隔符作为报文结尾

FixedLengthFrameDecover

定长解码器,这个最简单,消息体固定长度,解析数据时按长度读取即可

退出移动版