0%

tcp粘包问题和Nagle算法

1. TCP 粘包

粘包并不是 TCP 协议造成的,它的出现是因为应用层协议设计者对 TCP 协议的错误理解,忽略了 TCP 协议的定义并且缺乏设计应用层协议的经验。我们经常提到的 TCP 协议中的粘包是如何发生的:

  • TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;
  • 应用层协议的没有定义消息的边界导致数据的接收方无法拼接数据;

TCP本来就是基于字节流而不是消息包的协议,会把你的数据变成字节流发到对面去,而且保证顺序不会乱,但是你要自己搞定字节流解析。

2. 粘包问题如何处理?

粘包问题的本质就是无法区分数据包的边界,只要解决了这个问题,也就解决了粘包问题。

2.1 关闭Nagle算法

Nagle 算法确实能够在数据包较小时提高网络带宽的利用率并减少 TCP 和 IP 协议头带来的额外开销,但是使用该算法也可能会导致应用层协议多次写入的数据被合并或者拆分发送,当接收方从 TCP 协议栈中读取数据时会发现不相关的数据出现在了同一个数据段中,应用层协议可能没有办法对它们进行拆分和重组。

Nagle算法问题导致的,需要结合应用场景适当关闭该算法。

2.2 消息边界

如果我们能在应用层协议中定义消息的边界,那么无论 TCP 协议如何对应用层协议的数据包进程拆分和重组,接收方都能根据协议的规则恢复对应的消息。在应用层协议中,最常见的两种解决方案就是

  • 基于长度

    基于长度的实现有两种方式,一种是使用固定长度,所有的应用层消息都使用统一的大小,另一种方式是使用不固定长度,但是需要在应用层协议的协议头中增加表示负载长度的字段,这样接收方才可以从字节流中分离出不同的消息,HTTP 协议的消息边界就是基于长度实现的。

    在上述 HTTP 消息中,我们使用 Content-Length 头表示 HTTP 消息的负载大小,当应用层协议解析到足够的字节数后,就能从中分离出完整的 HTTP 消息,无论发送方如何处理对应的数据包,我们都可以遵循这一规则完成 HTTP 消息的重组。

  • 基于终结符(Delimiter)

    不过 HTTP 协议除了使用基于长度的方式实现边界,也会使用基于终结符的策略,当 HTTP 使用块传输(Chunked Transfer)机制时,HTTP 头中就不再包含 Content-Length 了,它会使用负载大小为 0 的 HTTP 消息作为终结符表示消息的边界。

    还有例如\n,\r,\t,或者一些隐藏字符。

当然除了这两种方式之外,我们可以基于特定的规则实现消息的边界,例如:使用 TCP 协议发送 JSON 数据,接收方可以根据接收到的数据是否能够被解析成合法的 JSON 判断消息是否终结。

3. Nagle算法和延迟确认

3.1 Nagle算法

小数据块存在一起,一起发送。

(1)如果包长度达到MSS,则允许发送;

(2)如果该包含有FIN,则允许发送;

(3)设置了TCP_NODELAY选项,则允许发送;

(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

  • 优点:避免网络中充斥着许多小数据块,降低网络负载,减少网络拥塞,提高网络吞吐。

  • 缺点:客户端的延迟会增加,实时性降低,不适合延时要求尽量小的场景;且对于大文件传输这种场景,会降低传输速度。

用TCP_NODELAY选项可以禁止Negale 算法。此时,应用程序向内核递交的每个数据包都会立即发送出去。需要注意的是,虽然禁止了Negale 算法,但网络的传输仍然受到TCP确认延迟机制的影响。

3.2 延迟确认

接收方在收到数据后,并不会立即回复ACK, 而是延迟一定时间 或者 达到2x最大段数据长度为止 (不同操作系统实现并不一样)

  1. 这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
  2. 如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。

3.3 Nagle和延迟确认一起使用产生的问题

两种算法优点都有减小网络数据包的优点,但是都增加了网络通信的延时. 结合在一起就会有问题

例如客户端发送一个数据报, 由于延时确认,服务端不是马上确认;

若客户端启用nagle算法,要等到收到上一个数据报的确认在发送下一个数据报;

如此以来,客户端和服务端相互等待,直到超时或者其他情况.

4. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!