TCP 粘包

TCP 粘包

一、什么是 TCP?

TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是 IP(互联网协议)的补充,通常与 IP 一起被称为 TCP/IP 协议族。

TCP 的主要特点包括:

  1. 面向连接:在数据传输之前,TCP 需要在发送端和接收端之间建立一个连接。这个连接是通过三次握手(three-way handshake)过程来实现的。
  2. 可靠性:TCP 提供数据包的确认、重传和排序机制,确保数据在网络中的可靠传输。如果一个数据包在传输过程中丢失或损坏,TCP 会自动重传该数据包。
  3. 流量控制:TCP 使用滑动窗口机制来控制数据的发送速率,以防止发送端发送数据过快而导致接收端无法处理。
  4. 拥塞控制:TCP 能够感知网络的拥塞情况,并通过调整发送速率来避免网络拥塞。
  5. 全双工通信: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 协议 ,转载请注明出处!