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 传输出现 粘包/拆包 的问题,这时就需要通过应用层来解决了。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/74626.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript Fetch API

简介 fetch() API 是用于发送 HTTP 请求的现代异步方法,它基于 Promise,比传统的 XMLHttpRequest 更加简洁、强大 示例 基本语法 fetch(url, options).then(response > response.json()).then(data > console.log(data)).catch(error > con…

UMI-OCR Docker 部署

额外补充 Docker 0.前置条件 部署前,请检查主机的CPU是否具有AVX指令集 lscpu | grep avx 输出如下即可继续部署 Flags: ... avx ... avx2 ... 1.下载dockerfile wget https://raw.githubusercontent.com/hiroi-sora/Umi-OCR_runtime_linux/main/Do…

C++ --- 二叉搜索树

1 二叉搜索树的概念 ⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树: 1 若它的左⼦树不为空,则左⼦树上所有结点的值都⼩于等于根结点的值 2 若它的右⼦树不为空,则右⼦树上所有结点的值都⼤于等于根结点…

跨语言语言模型预训练

摘要 最近的研究表明,生成式预训练在英语自然语言理解任务中表现出较高的效率。在本研究中,我们将这一方法扩展到多种语言,并展示跨语言预训练的有效性。我们提出了两种学习跨语言语言模型(XLM)的方法:一种…

文件描述符,它在哪里存的,exec()后还存在吗

学过计系肯定了解 寄存器、程序计数器、堆栈这些 程序运行需要的资源。 这些是进程地址空间。 而操作系统分配一个进程资源时,分配的是 PCB 进程控制块。 所以进程控制块还维护其他资源——程序与外部交互的资源——文件、管道、套接字。 文章目录 文件描述符进程管…

Slidev使用(一)安装

文章目录 1. **安装位置**2. **使用方式**3. **适用场景**4. **管理和维护** 全局安装1. **检查 Node.js 和 npm 是否已安装**2. **全局安装 Slidev CLI**3. **验证安装是否成功**4. **创建幻灯片文件**5. **启动 Slidev**6. **实时编辑和预览**7. **构建和导出(可选…

第二十一章:模板与继承_《C++ Templates》notes

模板与继承 重点和难点编译与测试说明第一部分:多选题 (10题)第二部分:设计题 (5题)答案与详解多选题答案:设计题参考答案 测试说明 重点和难点 21.1 空基类优化(EBCO) 知识点 空基类优化(Empty Base Cla…

AOA与TOA混合定位,MATLAB例程,自适应基站数量,三维空间下的运动轨迹,滤波使用EKF

本代码实现了一个基于 到达角(AOA) 和 到达时间(TOA) 的混合定位算法,结合 扩展卡尔曼滤波(EKF) 对三维运动目标的轨迹进行滤波优化。代码通过模拟动态目标与基站网络,展示了从信号测量、定位解算到轨迹滤波的全流程,适用于城市峡谷、室内等复杂环境下的定位研究。 文…

量子计算:开启未来计算的新纪元

一、引言 在当今数字化时代,计算技术的飞速发展深刻地改变了我们的生活和工作方式。从传统的电子计算机到如今的高性能超级计算机,人类在计算能力上取得了巨大的进步。然而,随着科技的不断推进,我们面临着越来越多的复杂问题&…

AMD机密计算虚拟机介绍

一、什么机密计算虚拟机 机密计算虚拟机 是一种基于硬件安全技术(如 AMD Secure Encrypted Virtualization, SEV)的虚拟化环境,旨在保护虚拟机(VM)的 ​运行中数据​(包括内存、CPU 寄存器等)免受外部攻击或未经授权的访问,即使云服务提供商或管理员也无法窥探。 AMD …

如何通过数据可视化提升管理效率

通过数据可视化提升管理效率的核心方法包括清晰展示关键指标、及时发现和解决问题、支持决策优化。其中,清晰展示关键指标尤为重要。通过数据可视化工具直观地呈现关键绩效指标(KPI),管理者能快速、准确地理解业务现状&#xff0c…

.git 文件夹

文件夹介绍 🍎 在 macOS 上如何查看 .git 文件夹? ✅ 方法一:终端查看(最推荐) cd /你的项目路径/ ls -a-a 参数表示“显示所有文件(包括隐藏的)”,你就能看到: .git…

MongoDB 与 Elasticsearch 使用场景区别及示例

一、核心定位差异 ‌MongoDB‌ ‌定位‌:通用型文档数据库,侧重数据的存储、事务管理及结构化查询,支持 ACID 事务‌。‌典型场景‌: 动态数据结构存储(如用户信息、商品详情)‌。需事务支持的场景&#xf…

【深度学习基础 2】 PyTorch 框架

目录 一、 PyTorch 简介 二、安装 PyTorch 三、PyTorch 常用函数和操作 3.1 创建张量(Tensor) 3.2 基本数学运算 3.3 自动求导(Autograd) 3.4 定义神经网络模型 3.5 训练与评估模型 3.6 使用模型进行预测 四、注意事项 …

uniapp中APP上传文件

uniapp提供了uni.chooseImage(选择图片), uni.chooseVideo(选择视频)这两个api,但是对于打包成APP的话就没有上传文件的api了。因此我采用了plus.android中的方式来打开手机的文件管理从而上传文件。 下面…

推陈换新系列————java8新特性(编程语言的文艺复兴)

文章目录 前言一、新特性秘籍二、Lambda表达式2.1 语法2.2 函数式接口2.3 内置函数式接口2.4 方法引用和构造器引用 三、Stream API3.1 基本概念3.2 实战3.3 优势 四、新的日期时间API4.1 核心概念与设计原则4.2 核心类详解4.2.1 LocalDate(本地日期)4.2…

树莓派5从零开发至脱机脚本运行教程——1.系统部署篇

树莓派5应用实例——工创视觉 前言 哈喽,各位小伙伴,大家好。最近接触了树莓派,然后简单的应用了一下,学习程度并不是很深,不过足够刚入手树莓派5的小伙伴们了解了解。后面的几篇更新的文章都是关于开发树莓派5的内容…

GPT Researcher 的win docker安装攻略

github网址是:https://github.com/assafelovic/gpt-researcher 因为docker安装方法不够清晰,因此写一个使用方法 以下是针对 Windows 系统 使用 Docker 运行 AI-Researcher 项目的 详细分步指南: 步骤 1:安装 Docker 下载 Docke…

【后端】【Django DRF】从零实现RBAC 权限管理系统

Django DRF 实现 RBAC 权限管理系统 在 Web 应用中,权限管理 是一个核心功能,尤其是在多用户系统中,需要精细化控制不同用户的访问权限。本文介绍如何使用 Django DRF 设计并实现 RBAC(基于角色的访问控制)系统&…

C#基础学习(五)函数中的ref和out

1. 引言:为什么需要ref和out? ​问题背景:函数参数默认按值传递,值类型在函数内修改不影响外部变量;引用类型重新赋值时外部对象不变。​核心作用:允许函数内部修改外部变量的值,实现“双向传参…