当前位置: 首页 > news >正文

Netty——TCP 粘包/拆包问题

文章目录

  • 1. 什么是 粘包/拆包 问题?
  • 2. 原因
    • 2.1 Nagle 算法
    • 2.2 滑动窗口
    • 2.3 MSS 限制
    • 2.4 粘包的原因
    • 2.5 拆包的原因
  • 3. 解决方案
    • 3.1 固定长度消息
    • 3.2 分隔符标识
    • 3.3 长度前缀协议
      • 3.3.1 案例一
      • 3.3.2 案例二
      • 3.3.3 案例三
  • 4. 总结


1. 什么是 粘包/拆包 问题?

  • 粘包 (Sticky Packet):发送方连续发送的 多个独立数据包,在接收方被合并成 一个数据包 接收,导致应用层无法区分原始消息的边界。例如,发送方依次发送 A 和 B,接收方可能收到 AB。
  • 拆包 (Packet Splitting):发送方发送的 一个完整数据包,在传输过程中 被分割成多个小包,接收方需要 重新组装 才能还原完整消息。例如,发送方发送 ABCD,接收方可能收到 AB 和 CD。

2. 原因

TCP 协议的设计目标是 高效传输字节流而非保证消息边界。以下机制是导致问题的核心原因:

2.1 Nagle 算法

每个数据包都必须加上 TCP 头 和 IP 头,如果要传递的数据很少,那么这个数据包中大部分都是头信息。如果将多个微小数据包合并成一个大数据包,那么网络利用率就会提高。于是,为了减少网络中 微小数据包 的数量,TCP 会将多个小数据包合并成一个大包发送,这就是 Nagle 算法。

2.2 滑动窗口

接收方为提高吞吐量,会采取以下两个措施:

  • 延迟发送 ACK 以合并多个数据包的确认
  • 将收到的数据暂存到缓冲区,积累到一定量后再通知应用层读取。从而导致应用层一次读取多个数据包。

2.3 MSS 限制

链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU (Maximum Transmission Unit),不同的链路设备的 MTU 值也有所不同,例如:

  • 以太网的 MTU 是 1500 字节。
  • 本地回环地址的 MTU 是 65535 字节 (本地测试不走网卡)。

MSS 是最大段长度 (Maximum Segment Size),它是 MTU 去除 TCP 头和 IP 头后剩余能够作为数据传输的字节数。IPv4 TCP 头占用 20 字节,IP 头占用 20 字节,因此以太网 MSS 的值为 1500 - 40 = 1460 字节。TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送。

2.4 粘包的原因

  • Nagle 算法:小数据包会被合并成大数据包,从而导致粘包。
  • 滑动窗口:假设 发送方 256 字节表示一个完整报文,但由于 接收方 处理不及时窗口大小足够大,这 256 字节就会缓冲在 接收方 的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包。

2.5 拆包的原因

  • MSS 限制:当 发送的数据量超过 MSS 限制 后,会将数据切分发送,从而导致拆包。
  • 滑动窗口:假设 接收方 的窗口只剩 128 字节,发送方 的报文大小是 256 字节,这时窗口放不下这个报文,只能先发送前 128 字节,等待 ACK 后才能发送剩余部分,这就造成了拆包。

3. 解决方案

TCP 层无法感知消息边界,因此需要应用层通过来解决,解决方案如下:

3.1 固定长度消息

思想:每条消息的长度固定,接收方按固定长度读取

在 Netty 中的实现:将 FixedLengthFrameDecoder 作为 ChannelPipeline 的第一个处理器,如下所示:

// 添加一个 消息长度固定为 512 字节的解码器
ch.pipeline().addLast(new FixedLengthFrameDecoder(512));

缺点消息长度不好把握,太短可能无法容纳比较长的消息,太长可能会导致浪费。

3.2 分隔符标识

思想:在消息末尾添加特殊分隔符(如 \n),接收方通过解析分隔符分割消息

在 Netty 中的实现:将 LineBasedFrameDecoderDelimiterBasedFrameDecoder 作为 ChannelPipeline 的第一个处理器,如下所示:

  • 添加一个以换行符为特殊分隔符的解码器:
    // 添加一个解码器,它以 \n 或 \r\n 为分隔符分割消息
    // 但消息长度不能超过 1024 字节,如果超过,会抛出异常
    ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
    
  • 添加一个以指定字符串为特殊分隔符的解码器:
    // 指定分隔符为 "EOM"
    ByteBuf delimiter = Unpooled.copiedBuffer("EOM".getBytes());
    // 添加一个解码器,它以 "EOM" 为分隔符分割消息
    // 但消息长度不能超过 1024 字节,如果超过,会抛出异常
    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
    

缺点分隔符不好确定,如果内容本身包含了分隔符,那么就会解析错误。

3.3 长度前缀协议

思想:在消息前添加固定长度的字段,表示消息总长度

在 Netty 中的实现:将 LengthFieldBasedFrameDecoder 作为 ChannelPipeline 的第一个处理器,如下所示:

ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
        1024,  // 最大帧(消息)长度
        0,     // 长度字段偏移量
        4,     // 长度字段长度
        0,     // 长度调整值
        4      // 初始跳过字节数
));

LengthFieldBasedFrameDecoder 的重要参数:

  • maxFrameLength允许的最大帧长度。若接收到的消息长度超出这个值,解码器会抛出 TooLongFrameException 异常,避免内存溢出。
  • lengthFieldOffset长度字段在消息中的偏移量,即从消息的哪个位置开始是长度字段。
  • lengthFieldLength长度字段本身的字节数
  • lengthAdjustment长度字段的值与实际消息长度之间的调整值。比较复杂,一般不使用。
  • initialBytesToStrip解码后需要跳过的初始字节数

以下举出几个例子帮助理解这几个参数(参考了 LengthFieldBasedFrameDecoder 的 JavaDoc,Magic 表示校验消息的魔数,Length 代表消息长度,Actual Content 代表消息内容):

3.3.1 案例一

参数配置:

// 长度字段的长度为 2,长度字段代表消息内容的长度
lengthFieldOffset = 0;
lengthFieldLength = 2;
initialBytesToStrip = 0;

解码过程:

解码前 (14 字节)					解码后 (14 字节)
+--------+----------------+		+--------+----------------+
| Length | Actual Content |---->| Length | Actual Content |
| 0x000C | "Hello, Netty" |		| 0x000C | "Hello, Netty" |
+--------+----------------+		+--------+----------------+

3.3.2 案例二

参数配置:

lengthFieldOffset = 0;
lengthFieldLength = 2;		// 长度字段的长度为 2
initialBytesToStrip = 2;	// 解码后跳过长度字段

解码过程:

解码前 (14 字节)					解码后 (12 字节)
+--------+----------------+		+----------------+
| Length | Actual Content |---->| Actual Content |
| 0x000C | "Hello, Netty" |		| "Hello, Netty" |
+--------+----------------+		+----------------+

3.3.3 案例三

参数配置:

// 魔数字段的长度为 2
lengthFieldOffset = 2;		// 长度字段位于魔数字段的右边,需要偏移 2 字节
lengthFieldLength = 2;		// 长度字段的长度为 2
initialBytesToStrip = 4;	// 解码后跳过长度和魔数字段

解码过程:

解码前 (16 字节)								解码后 (12 字节)
+--------+--------+----------------+		+----------------+
| Magic  | Length | Actual Content |------->| Actual Content |
| 0x0013 | 0x0010 | "Hello, Netty" |		| "Hello, Netty" |
+--------+--------+----------------+		+----------------+

4. 总结

TCP 协议的设计目标是 高效传递字节流,所以没有考虑到消息的边界。由于 Nagle 算法、滑动窗口、MSS 限制 的因素,可能会导致 TCP 传输出现 粘包/拆包 的问题,这时就需要通过应用层来解决了。

应用层一般有三种解决方案:根据固定的消息长度分割消息根据固定的分隔符分割消息通过传输的消息长度分割消息。最常用的第三种方案,前两种方案有一定的缺陷。

相关文章:

  • 信息系统运行管理员教程1--信息系统运维概述
  • 人员进出新视界:视觉分析算法的力量
  • MySQL入门级操作
  • Ubuntu服务器中Swapper如何与虚拟内存配合
  • 【八股】未知宽高元素水平垂直居中的三种方法
  • CNN基础考点
  • 【C++ Linux编程进阶 从0实现muduo库系列】第五讲:实现C++日志库
  • system V 消息队列信息量(了解)
  • 基于MoE架构的AIGC引擎:海螺AI重新定义人机协同新范式
  • 青柠视频云支持808协议和1078协议,支持SIP信令日志追踪
  • C++ queue容器总结
  • Android系统的安全问题 - Linux的能力模型(Capability)和 SELinux 的区别
  • MarkLogic索引详解
  • C++20 中的std::c8rtomb和 std::mbrtoc8
  • LangChain开发(六)多模态输入与自定义输出
  • 国外计算机证书推荐(考证)(6 Sigma、AWS、APICS、IIA、Microsoft、Oracle、PMI、Red Hat)
  • Vue 把 Echarts 图传给后端:文件流信息方式传递
  • BFS专项练习 —— 蓝桥杯刷题
  • Java基础 3.26
  • uniapp主题切换功能,适配H5、小程序
  • 中国驻美大使:远离故土的子弹库帛书正随民族复兴踏上归途
  • 上海国际珠宝时尚功能区未来三年如何建设?六大行动将开展
  • 科普|男性这个器官晚到岗,可能影响生育能力
  • 【社论】公平有序竞争,外卖行业才能多赢
  • 金正恩观摩朝鲜人民军各兵种战术综合训练
  • 佩斯科夫:若普京认为必要,将公布土耳其谈判俄方代表人选