如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。
如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!
按照TCP协议,确认机制是累积的。也就是确认号X的确认指示的是所有X之前但不包括X的数据已经收到了。确认号(ACK)本身就是不含数据的分段,因此大量的确认号消耗了大量的带宽,虽然大多数情况下,ACK还是可以和数据一起捎带传输的,但是如果没有捎带传输,那么就只能单独回来一个ACK,如果这样的分段太多,网络的利用率就会下降。为缓解这个问题,RFC建议了一种延迟的ACK,也就是说,ACK在收到数据后并不马上回复,而是延迟一段可以接受的时间。延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为TCP协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。延迟ACK就算没有数据捎带,那么如果收到了按序的两个包,那么只要对第二包做确认即可,这样也能省去一个ACK消耗。由于TCP协议不对ACK进行ACK的,RFC建议最多等待2个包的积累确认,这样能够及时通知对端Peer,我这边的接收情况。Linux实现中,有延迟ACK(Delay Ack)和快速ACK,并根据当前的包的收发情况来在这两种ACK中切换:在收到数据包的时候需要发送ACK,进行快速ACK;否则进行延迟ACK(在无法使用快速确认的条件下也是)。
一般情况下,ACK并不会对网络性能有太大的影响,延迟ACK能减少发送的分段从而节省了带宽,而快速ACK能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。
关于ACK分段,有个细节需要说明一下:
ACK的确认号,是确认按序收到的最后一个字节序,对于乱序到来的TCP分段,接收端会回复相同的ACK分段,只确认按序到达的最后一个TCP分段。TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。
推荐查看《TCP-IP详解:Delay ACK》
TCP的重传机制以及重传的超时计算前面说过,每一个数据包都带有下一个数据包的编号。如果下一个数据包没有收到,那么 ACK 的编号就不会发生变化
如果发送方发现收到三个连续的重复 ACK,或者超时了还没有收到任何 ACK,就会确认丢包,从而再次发送这个包。
TCP的重传超时计算TCP交互过程中,如果发送的包一直没收到ACK确认,是要一直等下去吗?
显然不能一直等(如果发送的包在路由过程中丢失了,对端都没收到又如何给你发送确认呢?),这样协议将不可用,既然不能一直等下去,那么该等多久呢?等太长时间的话,数据包都丢了很久了才重发,没有效率,性能差;等太短时间的话,可能ACK还在路上快到了,这时候却重传了,造成浪费,同时过多的重传会造成网络拥塞,进一步加剧数据的丢失。也是,我们不能去猜测一个重传超时时间,应该是通过一个算法去计算,并且这个超时时间应该是随着网络的状况在变化的。为了使我们的重传机制更高效,如果我们能够比较准确知道在当前网络状况下,一个数据包从发出去到回来的时间RTT(Round Trip Time),那么根据这个RTT(我们就可以方便设置RTO(Retransmission TimeOut)了。
如何计算设置这个RTO?
设长了,重发就慢,丢了老半天才重发,没有效率,性能差;
设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
RFC793中定义了一个经典算法——加权移动平均(Exponential weighted moving average),算法如下:
首先采样计算RTT(Round Trip Time)值——也就是一个数据包从发出去到回来的时间
然后计算平滑的RTT,称为SRTT(Smoothed Round Trip Time),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)——其中的 α 取值在0.8 到 0.9之间
RTO = min[UpBOUND,max[LowBOUND,(BETA*SRTT)]]——BETA(延迟方差因子(BETA is a delay variance factor (e.g., 1.3 to 2.0))
针对上面算法问题,有众多大神改进,难以长篇累牍,推荐阅读《TCP 的那些事儿》、《TCP中RTT的测量和RTO的计算》
TCP的重传机制