延时 ACK 与 Nagle 算法结合
八月 19, 2024 [网络与安全] #obsidian #网络延时 ACK 与 Nagle 算法结合
Nagle 算法#
我们以 SSH 协议举例,通常在 SSH 连接中,单次击键就会引发数据流的传输。如果使用 IPv4,一次按键会生成约 88 字节大小的 TCP/IPv4 包(使用安全加密和认证):20 字节的 IP 头部,20 字节的 TCP 头部(假设没有选项),数据部分为 48 字节。这些小包(称为微型报(tinygram))会造成相当高的网络传输代价。也就是说,与包的其他部分相比,有效的应用数据所占比例甚微。
上述问题不会对局域网产生很大影响,因为大部分局域网不存在拥塞,而且这些包无需传输很远。然而对于广域网来说则会加重拥塞,严重影响网络性能。John Nagle 提出了一种简单有效的解决方法,现在称其为 Nagle 算法。下面首先介绍该算法是怎样运行的:
Nagle 算法的基本定义是任一时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是长度小于 MSS 尺寸的数据块,而未被确认则是指没有收到对方的 ACK 数据包。Nagle 算法的规则(参考 tcp_output.c 文件里 tcp_nagle_check 函数注释):
- 如果包长度达到 MSS,则允许发送;
- 如果该数据包含有 FIN,则允许发送;
- 设置了 TCP_NODELAY 选项,则允许发送;
- 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送;
- 上述条件都未满足,但发送了超时(一般为 200 ms),则立即发送。
该算法的精妙之处在于它实现了自时钟(self-clocking)控制:ACK 返回得快,数据传输也越快。在相对高延迟的广域网中,更需要减少微型报的数目,该算法使得单位时间内发送的报文段数据更少。也就是说,RTT 控制着发包速率。
“粘包”问题的缘由可能发生在发送端也可能发生在接收端:
- 由 Nagle 算法造成的发送端的粘包:Nagle 算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给 TCP 发送时,TCP 并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这好几段数据发送出去。
- 接收端接受不及时造成的接收端粘包:TCP 会把接收到的数据存在自己的缓冲区中,然后通应用层取数据。当应用层由于某些原因不能及时地把 TCP 的数据取出来,就会造成 TCP 缓冲区中存放了几段数据。
粘包的本质#
上层通过 TCP 传递的数据好像被胶水黏在了一起,所以有了所谓的 TCP 粘包问题。但是在这里我们需要纠正的一个点是:TCP 是流协议,根本不存在所谓的粘包一说。
send(2) Upon successful completion, the number of types which were send is returned.
Otherwise, -1 is returned and the global variable errno is set to indicate the error.
recv(2) These calls return the number of bytes received, or -1 if an error occurred.
文档中已提及:send 和 recv 的返回值表示成功发送/接收端字节数。所以对于应用层来说,黏包确实是个伪命题,TCP 本来就是一个基于字节流的协议而不是消息包的协议,它只会将你的数据编程字节流发到对面去,而且保证顺序不会乱,而对于字节流的解析,就需要我们自己来搞定了。
解决粘包问题#
解决黏包问题的最关键一步就是确定消息边界。首先我们需要明白什么是消息,在我认为,消息就是一段有意义的信息报文,例如一次 HTTP 请求或者像我们上面代码中所要发送的 Hello World! This is a test demo.。
所以我们要找到消息边界,这并不难理解,确定消息边界就是确定消息的开始或者结束。简单地说,就三个办法:
- 定长消息:协议提前约定好包的长度为多少,每当接收端接收到固定长度的字节就确定一个包;
- 消息分隔符:利用特殊符号标志着消息的开始或者结束,例如 HTTP 协议中的换行符;
- 长度前缀:先发送N个字节代表包的大小(注意大端和小端问题),后续解析也按长度读取解析。
延时 ACK 与 Nagle 算法结合#
延时 ACK 是指接收端不会每个包都发送一次 ACK 确认,而是当接收到一个包后延迟一段时间,以期望这段时间内仍有包被接收到,这是就可以只发送一次 ACK 确认之前收到的数据包,以减少网络带宽压力。
但若将延时 ACK 与 Nagle 算法直接结合使用,得到的效果可能不尽如人意。考虑以下情景,客户端使用延时 ACK 方法发送一个对服务器的请求,而服务端的响应数据并不适合在同一个包中传输,如下图所示:

从图中可以看到,在接收到来自服务器端端两个包以后,客户端并不立即发送 ACK,而是处于等待状态,希望有数据一同捎带发送。通常情况下,TCP 在接收到两个全长的数据包后就应返回一个 ACK,但这里并非如此。在服务器端,由于使用了 Nagle 算法,直到收到 ACK 前都不能发送新数据,因为任一时刻只允许至多一个小数据包在传。因此延时 ACK 与 Nagle 算法的结合导致了某种程度的死锁(两端互相等待对方作出行动),当然这种死锁并不是永久的,在延时 ACK 计时器或者响应端超时之后,将会得到解除。