TCP 粘包
TCP 粘包
一、什么是 TCP?
TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是 IP(互联网协议)的补充,通常与 IP 一起被称为 TCP/IP 协议族。
TCP 的主要特点包括:
- 面向连接:在数据传输之前,TCP 需要在发送端和接收端之间建立一个连接。这个连接是通过三次握手(three-way handshake)过程来实现的。
- 可靠性:TCP 提供数据包的确认、重传和排序机制,确保数据在网络中的可靠传输。如果一个数据包在传输过程中丢失或损坏,TCP 会自动重传该数据包。
- 流量控制:TCP 使用滑动窗口机制来控制数据的发送速率,以防止发送端发送数据过快而导致接收端无法处理。
- 拥塞控制:TCP 能够感知网络的拥塞情况,并通过调整发送速率来避免网络拥塞。
- 全双工通信:TCP 连接允许数据在两个方向上同时传输,即发送端和接收端可以同时发送和接收数据。
TCP 广泛应用于需要可靠数据传输的应用层协议,如 HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)等。通过这些协议,TCP 确保了互联网上各种应用服务的稳定和可靠运行。
二、TCP的传输机制
数据切片
我们将今天是
,个好日子
这两条消息使用传输层上的 TCP 协议发送到传输层,消息在进入到传输层的时候会被切片成一个个的数据包,这个数据包的长度是 MSS
.
可以把网络比喻为一个水管,是有一定的粗细的,这个粗细由**网络接口层(数据链路层)**提供给**网络层**,一般认为是的MTU
(1500),直接传入整个消息,会超过水管的最大承受范围,那么,就需要进行切片,成为一个个数据包,这样消息才能正常通过“水管”。
MTU 和 MSS 有什么区别
- MTU:最大传输单元。由网络接口层(数据链路层)提供给网络层最大一次传输数据的大小,一般为 1500Byte。假设 IP 层有 <=1500Byte 的数据需要发送,只需要一个 IP 包就可以,如果 IP 层有>=1500Byte 的数据需要发送,那就需要分片才能完成发送,分片后的 IP Header ID 相同
- MSS:最大发送字节数。TCP 提交给 IP 层最大分段大小,不包含 TCP Header 和 TCP Option,只包含 TCP Payload,MSS 是 TCP 用来限制应用层最大的发送字节数。假设 MTU=1500Byte。那么 MSS=1500-20(IP Header) =- 20(TCP Header)=1460Byte,如果应用层有 2000Byte 发送,那么需要两个切片才能完成发送,第一个 TCP 切片大小为 1460,第二个 TCP 切片大小为 540
三、什么是粘包?
简单来说,就是获取到了不属于当前消息的数据。
比如我们发送的两个消息为 今天是
,个好日子
,但是真正获取到的其实是今天是个
,好日子
这两条消息。这就造成了粘包的情况。
跟粘包关系比较大的是 TCP协议中基于字节流的这个特点。字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一堆的 0101,他们直接是没有任务边界的。应用程序传到 TCP 协议的数据,不是以消息报为单位向目的地发送,而是以字节流的方式发送出去,这些数据就可能被切割和组装为各种数据包,接收端收到这些数据包以后并没有正确还原为原来的消息,就会出现粘包的情况。
四、造成原因
Nagle 算法
Nagle 算法原来是为了解决发送单个小包会造成网络拥堵的情况,所以就设定了两种情况下才会发送数据:
- 如果包长度达到
MSS
(或含有Fin
包),立刻发送,否则等待下一个包到来;如果下一包到来后两个包的总长度超过MSS
的话,就会进行拆分发送; - 等待超时(一般为
200ms
),第一个包没到MSS
长度,但是又迟迟等不到第二个包的到来,则立即发送。
因为会等待下一个包的到来,所以就会造成多个包被分片然后发送的情况。举例说明
现在有三个消息,abcdefg
,1234567
,今天是个好日子
,如果是开启了 Nagle 算法,可能出现的发送情况为:
abcdefg123
,4567
,今天是个好日子
abcde
,fg1234567
,今天是个好日子
abcdefg
,1234567今天
,是个好日子
这样就造成了粘包,读取消息的时候就无法正确的还原。
接收端未及时处理数据包
即使关闭了 Nagle 算法,也并不能完全解决粘包问题,因为 Nagle 算法只会控制发送端,但是接收端也可能出现粘包问题。
还是同样的三个消息,abcdefg
,1234567
,今天是个好日子
,接收端也是按照一定的字节数来取消息的,那么可能出现的读取情况为:
abcdefg123
,4567
,今天是个好日子
abcde
,fg1234567
,今天是个好日子
abcdefg
,1234567今天
,是个好日子
这样就造成了粘包,读取消息的时候就无法正确的还原
五、解决办法
添加特殊标识
在消息的头尾都添加标志位,比如当收到了0xfffffe
或者回车符,则认为收到了新消息的头,此时继续取数据,直到收到下一个头标志0xfffffe
或者尾部标记,才认为是一个完整消息。类似的像 HTTP 协议里当使用 chunked 编码 传输时,使用若干个 chunk 组成消息,最后由一个标明长度为 0 的 chunk 结束。
加入消息长度信息
这个一般配合上面的特殊标志一起使用,在收到头标志时,里面还可以带上消息长度,以此表明在这之后多少 byte 都是属于这个消息的。如果在这之后正好有符合长度的 byte,则取走,作为一个完整消息给应用层使用。在实际场景中,HTTP 中的Content-Length
就起了类似的作用,当接收端收到的消息长度小于 Content-Length 时,说明还有些消息没收到。那接收端会一直等,直到拿够了消息或超时。
添加校验字段
一般除了这个标志位,发送端在发送时还会加入各种校验字段(校验和
或者对整段完整数据进行 CRC
之后获得的数据)放在标志位后面,在接收端拿到整段数据后校验下确保它就是发送端发来的完整数据。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!