Fluge Site

原文
TCP的可靠性不止建立在建立一个稳固的链接上,还有就是数据包丢失的重传机制,和防止网络波动的拥塞处理机制,这些都是慢慢发展而来的。
要先了解TCP的重传和拥塞处理,需要先了解两个很常见的变量–RTT和RTO,这两个是对重传和拥塞很重要的概念。

RTT(Round Trip Time):就是发送一个数据包的往返时间的测量,由于路由器和网络流量均会变化,因此我们认为这个时间可能经常会发生变化,TCP应该跟踪这些变化并相应地改变其超时时间。
RTO(Retransmission TimeOut):重传超时时间,是根据RTT计算得到的。

重传

在重传机制中,首先在介绍重传的几个机制前,要注意。接收端给发送端的Ack确认只会确认最后一个连续的包。比如:发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到,3丢失)此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

超时重传

如果发送端决定死等3的ACK的话,等timeout后在重传3,然后接收端会回一个4(以为着3,4都收到了)。
但是有个问题很严重。在等3的超时的时候,由于4已经发送了,但是接收端不会发送4的ACK。说明在等3的时候,发送端会悲观的认为4也丢了。这个时候就会有两种选择:

  1. 一种是仅重传第一个timeout的包。也就是第3份数据
  2. 第二种就是重传timeout后面所有的包

这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等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的实际的实现)。可见,这是一把双刃剑。

Selective Acknowledgment(SACK) 方法

不管是超时重传还是快熟重传都会面临一个重传一个还是重传所有的问题,这个时候可以使用SACK的方法来告知真正被丢失的包。要使用SACK的方式需要TCP的两端同时支持才行,可以通过tcp_sack参数打开这个功能(2.4后默认打开)
SACK方法需要在TCP头里加一个SACK option的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的一系列的非连续的没有确认的seq range。参看下图:

这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到,对没有收到的数据进行重传。

在接受端向发送端发送ack 时会带上sack option

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TCP SACK Option
Kind: 5
Length: Variable
+-------------+-------------+
| Kind = 5 | Length |
+------------+------------+-------------+-------------+
| Left Edge of list Block |
+------------+------------+-------------+-------------+
| Right Edge of list Block |
+------------+------------+-------------+-------------+
| |
/ . . . /
| |
+------------+------------+-------------+-------------+
| Left Edge of list Block |
+------------+------------+-------------+-------------+
| Right Edge of list Block |
+------------+------------+-------------+-------------+

sack option 一般占40字节。其中kind 占4字节,length占4个字节,剩下的32字节,每8个字节就为一个sack段,来记录一个连续block的开始序号和结束序号,最多记录4组block。

注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。可以参考TCP 选择性应答的性能权衡

D-SACK 方法

Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了。可以用tcp_dsack 参数进行开启(Linux2.4下默认开启)
D-SACK 使用了 SACK 的第一个block来做标志:

  1. 如果SACK的第一个block的范围被ACK所覆盖,那么就是D-SACK
  2. 如果SACK的第一个block的范围被SACK的第二个段覆盖,那么就是D-SACK

引入了D-SACK,有这么几个好处:

  1. 可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
  2. 是不是自己的timeout太小了,导致重传。
  3. 网络上出现了先发的包后到的情况(又称reordering)。
  4. 网络上是不是把我的数据包给复制了。

知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。


参考:
TCP 的那些事儿(上)
TCP 的那些事儿(下)