前言
写RPC用了Netty。涉及到粘包拆包问题。想复习一下。发现网上博客多是概念模糊不清。没有触及本质或者没有讲清楚。
遂决定自己写一篇
"TCP粘包"是谁的问题?
首先我们要明确TCP是面向字节流的协议。也就是说我们在应用层想使用TCP来传输数据时,它是无法区分消息的。具体举个例子。
我们想发两条消息。一个100字节,一个1000字节。
我们调用两次TCP的send()。send函数意味着把数据拷贝到发送缓冲区,若缓冲区不够全部写入则会分次写入。
这里我们假设发送缓冲区大小是不变的,仅是TCP的滑动窗口在变。
设我们的缓冲区有1400的大小,此时发送窗口为1100字节。
则写入缓冲区是这个状态,前1100是发送窗口,存放我们要发的两条消息。
但是实际上在缓冲区中都是字节数据,TCP是不会区分消息的,只会把这1100字节视为字节流来进行传输,包装为一个TCP报文来发送。也就是说TCP眼中看这1100个字节就是单纯的字节流,没有我们眼中的消息1,消息2之分。
然后由于Nagle算法,在一段时间后没有等到包,即使没到MSS也会发出。这样这两个消息作为一个TCP包中的数据被发了数据
这就发生了粘包。TCP并不区分应用层的消息边界,只会按发送窗口来发送字节数据。这导致在应用层中本来是两条的消息封装到一个TCP报文,服务端接收会该报文会读出两个粘连的消息。所以需要我们进行协议设计来解决这个粘包的问题。
注意:发送缓冲区等待MSS大小才发送是Nagle协议做的事。下文有介绍。
那么为什么说粘包不是TCP的问题?
因为TCP本身就是针对字节流传输的数据。按消息分割是我们使用者的需求,自然应当有我们自己去解决。
所以准确来说TCP粘包其实是使用TCP传输有边界的消息导致的消息粘连问题。
粘包或半包的原因
滑动窗口让我们可以发送多个数据包而无需等待确认。也即累计确认。
在思考过程中我开始纠结,连续发送多个包的大小是多少,是否都会是标准的MSS。但是实际上这个问题是与粘包半包无关的。TCP把多少字节数据封装为一个TCP报文并没有关系。
问题本质在于TCP字节流传输的性质。
只要基于字节流,那TCP必然无法区分我们应用层划分的多个数据包。无论TCP把数据怎么划分为TCP报文,都有出现粘包半包的可能性。
Nagle算法要MSS再发送是导致粘包半包发生的可能原因,那么我们关掉它,是否可以让缓冲区一有数据就发送呢?是否就能分消息发送了?
Nagle算法
Nagle算法是为了保证TCP报文尽量达到MSS大小。反正基于字节流不必按消息封装报文。只需要按字节流顺序封装即可。为什么要尽量达到MSS呢?
比如我们想发送1000条2字节的消息。若是每次立即发出就是1000个包,而TCP首部有40字节,body却只有2字节。这严重浪费了网络带宽。我们完全可以等待直到可以封装出一个MSS大小的包。
这样一次性发送了MSS长度的数据,只用了一个首部。大大提高了效率。
当然若迟迟等不到MSS大小的数据,它也会直接发送当前大小的数据包。
目前由于Nagle会提高延迟已经很少使用。
那么回到上段末尾的问题,关了Nagle算法是否就没了粘包半包?
当然不是。关了它不代表,操作系统会立即发送缓存区的数据。
假设这一种情况,我调用一次send,数据拷贝到内核缓冲区。由于没有Nagle算法,TCP直接发送。
此时我有send两条消息a和b,那么此时TCP正在发送上一个TCP报文,消息a到达缓冲区时TCP无法封装并发送,紧接着消息b也到达缓冲区。此时两条消息都在缓冲区。TCP再发送还是可能发送粘包半包问题(两条小于MSS则是粘包,大于MSS,则分两条会导致半包)。
同理在接受方的缓冲区仍然可能发生类似问题。因为接受缓冲区中存的是从TCP报文中提出来的字节数据。在我们应用层没有read时,它可能累计多条消息的字节数据。仍然可能发生粘包
结语
所以导致粘包半包的原因其实最底层还是TCP的字节流传输性质。
无论是Nagle算法还是不使用Nagle算法亦或者说MSS的限制,究其本质都是因为字节流协议,本身不区分消息边界,视角是字节。
因此,我们在写传输消息格式的需求,若使用TCP协议,一定要考虑这个问题,制定协议来解决。
回到标题,使用TCP协议发送有消息边界的数据,一定要自己解决。因为TCP很明确就是一个传输字节流的协议,不能按照消息来发送数据。
参考文章: