TCP重传机制
TCP重传机制
TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的ACK
只会确认最后一个连续的包。比如,发送端发了#1,#2,#3,#4,#5一共五份数据,接收端收到了#1,#2,于是返回ACK
=3,然后收到了#4(#3未收到),此时的TCP会怎么办?我们要知道,seq
和ACK
是以字节数为单位,所以返回ack
的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
超时重传机制
一种是不返回ack
,死等#3,当发送方发现收不到#3的ack
超时后,会重传#3。一旦接收方收到#3后,会ack
返回4——意味着#3和#4都收到了。
但是,这种方式会有比较严重的问题,那就是因为要死等#3,所以会导致#4和#5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到ACK
,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4#和#5的重传。
对此有两种选择:
- 仅重传 timeout 的包。也就是第3份数据。
- 重传 timeout 后所有的数据,也就是#3,#4,#5这三份数据。
第一种会节省带宽,但是慢;第二种会快一点,但是会浪费带宽,也可能会做无用功。但总体来说都不好。因为都在等 timeout,timeout 可能会很长。
快速重传机制
于是,TCP引入了一种叫 Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果包没有连续到达,就ack
最后那个可能被丢了的包,如果发送方连续收到3次相同的ack
,就重传。Fast Retransmit 的好处是不用等 timeout 了再重传。
比如:如果发送方发出了#1,#2,#3,#4,#5份数据,第一份先到送了,于是就ack
回2,结果#2因为某些原因没收到,3到达了,于是还是ack
回2,后面的#4和#5都到了,但是还是ack
回#2,因为2还是没有收到,于是发送端收到了三个ack
=2的确认,知道了#2还没有到,于是就马上重传#2。然后,接收端收到了#2,此时因为#3,#4,#5都收到了,于是ack
回6。示意图如下:
Fast Retransmit 只解决了一个问题,就是timeout 的问题,它依然面临一个艰难的选择,就是重传之前的一个,还是重传所有的问题。对于上面的示例来说,是重传#2呢,还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack
(#2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从#2到#20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。
SACK 方法
另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK
的东西,ACK
还是Fast Retransmit的 ACK
,SACK则是汇报收到的数据碎版。参看下图:
这样,在发送端就可以根据回传的SACK
来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。
这里还需要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK
里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK
,还是要依赖ACK
,并维护Timeout,如果后续的ack
没有增长,那么还是要把SACK
的东西重传,另外,接收端这边永远不能把SACK
的包标记为ACK
。
注意:SACK
会消费发送方的资源,,试想,如果一个攻击者给数据发送方发一堆SACK
的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡》
Duplicate SACK – 重复收到数据的问题
Duplicate SACK又称D-SACK,其主要使用了 SACK
来告诉发送方有哪些数据被重复接收了。RFC-2833 里有详细描述和示例。下面举几个例子(来源于RFC-2833)
D-SACK
使用了SACK
的第一个段来做标志,如果SACK
的第一个段的范围被ACK
所覆盖,那么就是D-SACK
;如果SACK
的第一个段的范围被SACK
的第二个段覆盖,那么就是D-SACK
。
示例一:ACK丢包
下面的示例中,丢了两个ACK
,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK
=3000-3500,因为ACK
都到了#4000意味着收到了#4000之前的所有数据,所以这个SACK
就是D-SACK
——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK
包。
Transmitted Received ACK Sent Segment Segment (Including SACK Blocks) 3000-3499 3000-3499 3500 (ACK dropped) 3500-3999 3500-3999 4000 (ACK dropped) 3000-3499 3000-3499 4000, SACK=3000-3500
示例二:网络延误
下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK
,而后面到达的三个包触发了Fast Retransmit 算法,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK
=1000-1500,因为ACK
已到了#3000,所以,这个SACK
是D-SACK
——标识收到了重复的包。
这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延时了。
Transmitted | Received | ACK Sent |
---|---|---|
Segment | Segment | (Including SACK Blocks) |
500-999 | 500-999 | 1000 |
1000-1499 | (delayed) | |
1500-1999 | 1500-1999 | 1000, SACK=1500-2000 |
2000-2499 | 2000-2499 | 1000, SACK=1500-2500 |
2500-2999 | 2500-2999 | 1000, SACK=1500-3000 |
1000-1499 | 1000-1499 | 3000 |
1000-1499 | 3000, SACK=1000-1500 |
可见,引入了D-SACK
,有这么几个好处:
- 可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
- 是不是自己的 timeout太小了,导致重传。
- 网络上出现了先发的包后到的情况(又称reordering)
- 网络上是不是把我的数据包给复制了。
知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。
Linux下的tcp_dsack
参数用于开启这个功能(Linux 2.4后默认打开)
转载自:http://www.uml.org.cn/safe/201407041.asp?artid=2511 作者:火龙果软件