参考文献

粘包现象

  • 粘包现象指的是发送方在一次发送中,将多个数据包粘在一起发送,接收方在接收数据时可能会将多个数据包看作一个数据包进行处理,导致数据解析错误的情况.这种现象通常出现在TCP协议中,由于TCP是面向流的协议,它会将应用层传递的数据流拆分成多个数据包进行传输,而接收方并不知道这些数据包的边界,因此可能会将多个数据包看作一个数据包进行处理.
  • 发送 abc def,接收 abcdef

粘包产生的原因

  • 应用层缓冲区:接收方 ByteBuf 设置太大(Netty 默认 1024),即数据包大小小于应用层缓冲区,多个数据包黏在一起.
  • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
  • Nagle 算法:会造成粘包
  • 发送方发送速率过快:如果发送方发送数据的速率过快,接收方可能无法及时处理所有的数据包,就会导致多个数据包被合并成一个数据包发送,从而产生粘包现象.
  • 网络拥塞:如果网络拥塞,数据包的传输可能会受到阻碍,TCP协议为了减少网络传输的开销,可能会将多个数据包合并成一个数据包发送,导致粘包现象的发生.
  • 数据包大小不一致:如果发送方发送的多个数据包大小不一致,接收方可能会无法正确拆分和组装数据包,从而产生粘包现象.

半包现象

  • 半包现象指的是发送方在一次发送中,将一个数据包拆分成多个数据包进行发送,接收方在接收数据时可能只收到了部分数据包,无法解析完整的数据包导致数据解析错误的情况.这种现象通常出现在UDP协议中,由于UDP是面向数据报的协议,它会将应用层传递的数据报拆分成多个数据包进行传输,而接收方无法保证所有数据包都能够被收到,因此可能会出现半包现象.
  • 发送 abcdef,接收 abc def

半包产生的原因

  • 应用层缓冲区:接收方 ByteBuf 小于实际发送数据量,即数据包的大小大于应用层缓冲区,数据包被截断.

  • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包

  • MSS(Maximum Segment Size,意为最大分段大小) 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

Netty解码器解决粘包半包问题

  • TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送.

  • TCP粘包/分包的原因:

    • 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;
    • 进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
    • 以太网帧的payload(净荷)大于MTU(1500字节)进行IP分片.

消息定长:FixedLengthFrameDecoder

1
2
3
4
// 指定单个消息长度
private static final int FRAME_LENGTH = 10;
// 添加FixedLengthFrameDecoder解码器
pipeline.addLast(new FixedLengthFrameDecoder(FRAME_LENGTH));

包尾增加特殊字符分割

  • 行分隔符类:LineBasedFrameDecoder或自定义分隔符类:DelimiterBasedFrameDecoder

    1
    2
    // LineBasedFrameDecoder默认使用"\r\n"作为行分隔符,并限定最大长度设置为1024个字节,以避免处理过长的行分隔符
    pipeline.addLast(new LineBasedFrameDecoder(1024));
    1
    2
    3
    4
    // 自定义分隔符
    private static final ByteBuf DELIMITER = Unpooled.copiedBuffer("$_".getBytes());
    // 添加DelimiterBasedFrameDecoder解码器
    pipeline.addLast(new DelimiterBasedFrameDecoder(1024, DELIMITER));
1
2
3
4
5
6
7
8
9
10
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {

private final ByteBuf[] delimiters;
private final int maxFrameLength;
private final boolean stripDelimiter;
private final boolean failFast;
private boolean discardingTooLongFrame;
private int tooLongFrameLength;
// ...略
}

长度域解码器LengthFieldBasedFrameDecoder

  • 将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类.分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包.

    1
    2
    // 添加LengthFieldBasedFrameDecoder解码器
    pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
    • maxFrameLength:指定解码器支持的最大帧长度,超出该长度的帧将被丢弃;
    • lengthFieldOffset:指定长度字段的偏移量,即长度字段的起始位置;
    • lengthFieldLength:指定长度字段的长度,可以是1、2、3或4个字节;
    • lengthAdjustment:指定长度字段的值需要调整的偏移量,才到实际内容部分,一般为0;
    • initialBytesToStrip:指定从解码帧中跳过的字节数,一般为4,即跳过长度字段本身.