TCP 的粘包与拆包问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_39453325/article/details/84181805

之前在做项目时,使用 Java NIO 来搭建服务器端及客户端程序,发现待发送的数据大于发送缓冲区 ByteBuffer 大小时,将发生拆包情况,会把待发送的数据包分多次发送到客户端。当时是分配了更大的字节缓冲区来解决这个问题,后来了解到这是 TCP 协议中的粘包与拆包问题。首先我们了解一下 TCP 的特性。

TCP 特性

TCP (Transmission Control Protocol) 传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

面向连接:通信双方在应用 TCP 协议进行通信之前需要通过三次握手来建立 TCP 连接,连接建立后才能进行正常的数据传输。

可靠性:由于 TCP 处于 IP 层之上,而 IP 层提供不可靠的传输,因此在 TCP 层存在四种常见传输错误,分别是比特错误(packet bit errors)、包乱序(packet reordering)、包重复(packet duplication)、丢包(packet drops),因此 TCP 要提供可靠的传输,就需要具有超时与重传管理、窗口管理、流量控制、拥塞控制等功能。

可靠性体现在三方面,首先 TCP 通过超时重传和快速重传两个常见手段来保证数据包的正确传输,即接收端在没有收到数据包或者收到错误的数据包时会触发发送端的数据包重传(处理比特错误和丢包);其次 TCP 接收端会缓存接收到的乱序到达数据,重排序后再向应用层提供有序的数据(处理包乱序);最后 TCP 发送端会维持一个发送窗口动态的调整发送速率以适用接收端缓存限制和网络拥塞情况,避免了网络拥塞或者接收端缓存满而大量丢包的情况(降低丢包率)。

字节流式:应用层发送的数据会再 TCP 的发送端缓存起来,统一分片(如一个应用层的数据包分成两个 TCP 包,即拆包)或者打包(如两个或者多个应用层的数据包打包成一个 TCP 数据包,即粘包)发送,到接收端的时候接收端也是直接按照字节流将数据传递给应用层。UDP 并不会对应用层的数据包进行打包和分片操作,一般一个应用层的数据包就对应一个 UDP 包。

粘包与拆包

UDP 是基于报文发送的,从 UDP 的帧结构可以看出,在 UDP 报文的首部采用了 16 bit 来指示 UDP 数据报文的长度,不同的报文之间是可以区分隔离出来的,所以应用层在接收传输层的报文时,不会存在拆包与粘包的问题。

而 TCP 是基于字节流传输的,应用层和传输层之间数据交互是大小不等的数据块,但 TCP 把这些数据块仅仅看成一连串无结构的字节流,没有边界,所以它不知道哪些数据块跟哪些数据块应该一起发送,哪些数据块应该是单独发送的;从 TCP 的帧结构也可以看出,TCP 首部没有表示数据长度的字段,即 TCP 并没有像 UDP 那样首部有数据长度,所以 TCP 存在拆包和粘包的问题。

关于 UDP 帧结构与 TCP 帧结构的详情大家可以参考https://mp.csdn.net/postedit/83656881这篇博客。 

粘包与拆包表现形式

现在假设客户端向服务端连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为以下三种,如下所示:

第一种:接收端正常收到两个数据包,没有发生拆包和粘包情况,此种情况本处不讨论。

第二种:接收端只收到一个数据包,TCP 把两个数据包合并成一个发送给接收端了,这一个数据包中包含了发送端发送的两个数据包的信息,这种情况称为粘包,由于接收端不知道这两个数据包之间的分隔界限,所以对于接收端来说是很难处理的。 

第三种:这种情况有两种表现形式,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出一部分,这种情况发生了拆包与粘包。这种情况如果不加特殊处理,接收端同样是不好处理的。 

粘包与拆包发生的原因 

1. 要发送的数据包大于 TCP 发送缓存区的可用空间大小时,数据会发生拆包;

2. 要发送的数据大于 MSS(Maximum Segment Size)最大报文段时,TCP 在发送前会对数据进行拆包;

TCP 在三次握手建立连接过程中,会在 SYN(同步序号) 报文中使用 MSS 选项功能,协商建立连接双方能够接收的最大报文段 MSS 的值,MSS 是传输层 TCP 协议范畴内的概念,它是标识 TCP 能够承载的最大的应用数据段长度,有 MSS = MTU (最大传送单元,一般是 1500 bit ,超过这个量要分成多个报文段) - 20 字节 TCP 报头 - 20 字节 IP 报头,那么在以太网环境下,MSS 值一般就是 1500 - 20 - 20 = 1460 字节。

3. 发送的数据小于 TCP 缓存区大小时,TCP 会将几次写入缓冲区的数据一次性发送,会存在粘包;

4. 接收数据端的应用层没有及时读取接收缓存区中的数据时,会发生粘包。

粘包与拆包解决方法

由于传输层的 TCP 无法理解应用层的业务数据,所以在传输层是无法保证数据包不被拆分和重组的,那么该问题只能通过应用层协议栈设计解决,给数据包加分界标记,来处理最后接收到的数据,不管拆分还是粘包都可以很好处理。

1. 发送端给数据包增加首部,首部包含数据包中数据的长度,这样接收端的应用层在接收到数据后,根据首部中的长度就可以知道数据的实际长度了,可以很好处理数据。设计思路,可以在首部固定 10 个字节长度用来保存整个数据包长度,位数不够补0。

0000000042{"type":"message","content":"hello"}

2. 设置数据包的长度为固定长度,不够数据则以0填充,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3. 应用层在发送每个数据包时,给每个数据包加分界标记,如换行符 "\n",这样接收端通过这个分界标记就可以将不同的数据包拆分开来了。如下是一个符合这个规则的请求包(需注意请求数据内部本身不能包含换行符,数据格式为 Json)。

{"type":"message","content":"HelloWorld!"}\n

猜你喜欢

转载自blog.csdn.net/weixin_39453325/article/details/84181805