在开发naive-rpc过程中需要处理 TCP 粘包的问题,此处结合网上的一些资料进行了总结。
1. 为什么 TCP 协议有粘包问题?
当应用层协议使用 TCP 协议传输数据时,TCP 协议可能会将应用层发送的数据拆分成多个包依次发送,而数据的接收方收到的数据段可能由多个”应用层包“组成,因此当应用层从 TCP 缓冲区读取数据并发现粘包时,需要将收到的数据进行拆分。
粘包不是 TCP 协议造成的,而是应用层协议设计者对 TCP 协议的理解不够深入,他们忽略了 TCP 协议的定义,缺乏设计应用层协议的经验。在本文中,我们将从 TCP 协议和应用层协议两个方面来分析粘性数据包是如何在我们经常提到的 TCP 协议中出现的。
- TCP 协议是面向字节流的协议,它可以合并或拆分应用层协议的数据。
- 应用层协议没有定义消息的边界,导致数据的接收者不能缝合数据。
TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议。在某些情况下,应用层交给 TCP 协议的数据并不是作为一个消息整体传输到目的主机;这些数据被组合成一数据段并发送到目的主机。
1.1 面向字节流的
Nagle 算法是一种通过减少数据包数量来提高 TCP 传输性能的算法。由于网络带宽有限,它不会将小块数据直接发送到目的主机,而是在本地缓冲区中等待更多需要发送的数据,然后批量的发送出去。这种批量发送数据的策略降低了网络拥塞的可能性,并减少了额外的开销,尽管它会影响实时性能和网络延迟。
在互联网的早期,Telnet 是一个被广泛使用的应用,然而,使用 Telnet 产生大量的有效数据,只有 1 字节的负载,每个包有 40 字节的额外开销,带宽利用率只有~2.44%,Nagle 算法就是在这种场景下设计的。
当应用层协议通过 TCP 传输数据时,要发送的数据实际上是先写入 TCP 缓冲区的。如果用户开启 Nagle 算法,TCP 协议可能不会立即发送写入的数据,而是会等到缓冲区中的数据超过最大数据段(MSS)或者之前的数据段被确认后,再发送缓冲区中的数据。
网络拥塞是几十年前的问题,但今天的网络带宽资源不像过去那样紧张,并且默认情况下,Linux 内核使用以下代码默认禁用 Nagle 算法。
1 | TCP_NODELAY = 1 |
Nagle 算法确实提高了网络带宽利用率,减少了数据包较小时 TCP 和 IP 协议头的额外开销,但使用这种算法也可能导致应用层协议多次写入的数据被合并或拆分发送,当接收方从 TCP 栈中读取数据,发现同一段中有不相关的数据时,应用层协议可能没有办法拆分重组。
除了 Nagle 算法,TCP 栈中还有另一个延迟发送数据的选项,TCP_CORK。如果我们打开这个选项,那么当发送的数据小于 MSS 时,TCP 协议将延迟 200ms 发送数据,或者等待缓冲区中的数据超过 MSS。
TCP_NODELAY 和 TCP_CORK 都是通过延迟数据的发送来提高带宽利用率的,它们对应用层协议写的数据进行拆分和重组,而这些机制和配置之所以可能的最重要的原因是——TCP 协议是基于字节流的协议,它本身没有包的概念,不按包发送数据。
1.2 找出消息边界
如果我们已经系统的学习了 TCP 协议和基于 TCP 的应用层协议的设计,那么设计一个可以被 TCP 栈任意拆分组装成包的应用层协议就没有问题了。由于 TCP 协议是基于字节流的,这实际上意味着应用层协议必须绘制自己的消息边界。
如果能在应用层协议中定义消息的边界,那么无论 TCP 协议如何拆分重组应用层协议的包过程,接收端都能根据协议的规则恢复出相应的消息。
2. Netty 如何处理 TCP 粘包问题?
2.1 消息边界
方式\比较 | 寻找消息边界方式 | 优点 | 缺点 | 推荐度 | |
---|---|---|---|---|---|
TCP 连接改成短连接,一个请求一个短连接 | 建立连接到释放连接之间的信息即为传输信息 | 简单 | 效率低下 | 不推荐 | |
封装成帧(Framing) | 固定长度 | 满足固定长度即可 | 简单 | 空间浪费 | 不推荐 |
分割符 | 分隔符之间 | 空间不浪费,也比较简单 | 内容本身出现分隔符时需转义,所以需要扫描内容 | 推荐 | |
固定长度字段存个内容的长度信息 | 先解析固定长度的字段获取长度,然后读取后续内容 | 精确定位用户数据,内容也不用转义 | 长度理论上有限制,需提前预知可能的最大长度从而定义长度占用字节数 | 推荐+ | |
其他方式 | 每种都不同,例如JSON 可以看{}是否应已经成对 | 衡量实际场景,很多是对现有协议的支持 |
2.2 Netty 对三种常用封帧方式的支持
方式\比较 | 解码 | 编码 | |
---|---|---|---|
封装成帧(Framing) | 固定长度 | FixedLengthFrameDecoder | 简单 |
分割符 | DelimiterBasedFrameDecoder | 简单 | |
固定长度字段存个内容的长度信息 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
3. 关于 UDP
UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收,所以无粘包、半包问题。
4. 参考
https://www.sobyte.net/post/2021-12/whys-the-design-tcp-message-frame/