● TCP中的Nagle、DelayedAck、Cork算法
先确定一个概念,什么是小包?小包就是字节长度加上各类头后小于MSS长度的包,就是小包。
Nagle算法
基本概念
在使用一些协议通讯的时候,比如Telnet,会有一个字节一个字节的发送的情景,每次发送一个字节的有用数据,就会产生41个字节长的分组,20个字节的IP Header 和 20个字节的TCP Header,这就导致了1个字节的有用信息要浪费掉40个字节的头部信息,这是一笔巨大的字节开销,而且这种Small packet在广域网上会增加拥塞的出现。
当包的数量很多,而包的内容却很少的时候,网络中极容易产生拥塞情况。当然,实际上和内容多少没有直接关联,而是和数量相关,但就是因为数量多,所以才进一步加剧了拥塞情况。
那么,什么是Nagle算法呢?
Nagle算法只允许一个未被ACK的小包存在于网络,它并不管小包的大小,因此它事实上就是一个扩展的停等协议(停止等待ARQ协议),只不过它是基于包停等的,而不是基于字节停等的。Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低。
if there is new data to send
if the window size >= MSS and available data is >= MSS
send complete MSS segment now
else
if there is unconfirmed data still in the pipe
enqueue data in the buffer until an acknowledge is received
else
send data immediately
end if
end if
end if
Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):
- 如果包长度达到MSS,则允许发送
- 如果该包含有FIN,则允许发送
- 设置了TCP_NODELAY选项,则允许发送
- 未设置TCP_CORK选项时,若所有发出去的数据小包(包长度小于MSS)均被确认,则允许发送
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送
我们要注意的一点是Nagle算法只是减少小包发送,而不是禁止,因为如果网络中没有未被确认的报文(或者小包),Nagle还是允许发送小包的,Nagle算法只是想解决小包造成的网络拥塞问题,所以网络不拥塞的话,还是要发送小包。
做一个小实验 Client端每次发送1个字节,将hello发送到Server端,然后server再全部发送给Client,其实要点在于Client的发送,预期的结果是:
我们虽然一个字节一个字节的发,但是在协议中使用Nagle算法,可能会有延时等待的状况,即将几个字符合成一个片段进行发送;必须是收到对方的确认之后,才能再次发送。
看下实验的结果:
从图中的结果可以看出:
-
HELLO 被分成 2个包发送了,应用层调用send() 5次,每次发送一个字节,由于Nagle算法,将ELLO合成一个包发送,这样大可以减少Samll packet的数量,增加TCP传输的效率;
-
分成的2个数据包,并没有连续被发出,这也符合Nagle算法的原则,即TCP连接上最多只能有一个未被确认的小分组,等待收到ACK之后,才发第二个封包;
所以说,假设每个字节的包发出去都能立刻被返回,那么,HELLO必然会按5个包发送;但实际上,当第一个H出去后,肯定来不及等到其ACK返回时第二个E已经开始准备发送了,从上面示例中看出,当ACK返回时,后面的ELLO都早已被塞入了“组包队列”在默默地等着他;
但是?
如果禁用Nagle算法?
在默认的情况下,Nagle算法是默认开启的,Nagle算法比较适用于发送方发送大批量的小数据,并且接收方作出及时回应的场合,这样可以降低包的传输个数。
但是,当你的应用不是连续请求+应答的模型的时候,而是需要实时的单项的发送数据并及时获取响应,这种情况下就明显不太适合Nagle算法,明显有delay的,这在音视频开发中极为常见。
如何禁用它?
Linux提供了TCP_NODELAY
的套接字选项来禁用Nagle算法:
// TCP_NODELAY,关闭Nagle算法
setsockopt(client_fd, SOL_TCP, TCP_NODELAY, (int[]){1}, sizeof(int));
再做一次实验
很显然,在失去Nagle算法的时候,TCP包会按实际真实字节(<MSS)为一包直接发送出去,并且是连续发送,而不会因为等待ACK而暂停。
Delayed ACK
问题是什么?
在一个有数据传输的TCP连接中,如果只有数据发送方启用Nagle算法,在其连续发送多个小报文时,Nagle算法机制会减少网络中的小报文数量。这就意味着,同样传输相同大小的应用数据,在网络上的报文个数却不同。
还是上面那个例子,发送端未启用Nagle算法,但接收端延迟确认默认时间为200ms,来看看这时的情况:
RFC 1122规定,Delayed ACK 对单个的小报文可以延长确认的时间,但不允许有两个连续的小报文不被确认。所以,当发送端连续发送2个字节后,接收端必须给予确认,但这依然很有可能导致200ms的延时存在。
所以,当发送端启用Nagle而接收端由启用了Delayed Ack,那么延迟问题会变得严重。
如何理解这个算法?
简单的说,Delay Ack就是延时发送ACK,在收到数据包的时候,会检查是否需要发送ACK,如果需要的话,进行快速ACK还是延时ACK,在无法使用快速确认的条件下,就会使用Delay Ack。
TCP在何时发送ACK的时候有如下规定:
- 当有响应数据发送的时候,ACK会随着数据一块发送
- 如果没有响应数据,ACK就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右,如果在40ms内有数据发送,那么ACK会随着数据一块发送,对于这个延迟的需要注意一下,这个延迟并不是指的是收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms就会检查一次,比如定时器在0ms启动,200ms到期,180ms的时候data来到,那么200ms的时候没有响应数据,ACK仍然会被发送,这个时候延迟了20ms
- 如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!
优点:减少了数据段的个数,提高了发送效率 缺点:过多的delay会拉长RTT
做一个小实验
先通过一组实验来看下效果: 分别让服务器在0ms30m延时的情况下进行回显,看看数据的交互情况:
0ms的状况:
第一个字符没有Delay ACK的状况,后面的就一直出现data和ACK一起发送的状况,也就是说ACK不是立即回复,然后在定时器到达之前有数据发送和数据一块发送;
第一个字符没有Delay ACK的状况,后面的就一直出现data和ACK一起发送的状况,也就是说ACK不是立即回复,然后在定时器到达之前有数据发送和数据一块发送;
30ms的状况:
我们看到第一个字符和0ms的状况一样,也是快速回复ACK,之后的字符就是delay ACK,接收到数据不会立马回复ACK,如果此时有数据需要发送就一起带过去,否则就要等30ms!
如何禁用它?
Linux提供了 TCP_QUICKACK 的套接字选项来禁用Delayed ACK算法:
// TCP_QUICKACK,关闭DelayACK算法 setsockopt(client_fd, SOL_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
Cork算法
这是什么呢?
从下列代码中可以看出,注释掉下面逻辑后,改为:
无论什么情况下,任何小包都必须按一个定时器的节奏发出,而不是根据ACK来判断。这实际上就是明令禁止发送小包,如果就只有1个字节的小包要单独发,那么也只能等,等到计时器到点后才能发送。
Cork算法在Linux中是默认关闭的。
if there is new data to send
if the window size >= MSS and available data is >= MSS
send complete MSS segment now
else
enqueue data in the buffer,
start a timer and when the timer is expired, send the packet.
//if there is unconfirmed data still in the pipe
//enqueue data in the buffer until an acknowledge is received
//else
//send data immediately
//end if
end if
end if
如何启用它?
Linux提供了 TCP_CORK 的套接字选项来启用Cork算法:
// TCP_CORK,启用Cork算法
setsockopt(client_fd, SOL_TCP, TCP_CORK, (int[]){1}, sizeof(int));