【网络协议】TCP - 重传机制

前言

TCP 要保证所有的数据包都可以到达,所以,必需要有重传机制。

其中涉及 ACK 包比较关键,因为在窗口的这篇文章中,已经说过了,ACK 机制起始就是类似于一种反馈机制。

TCP 重传机制

注意,接收端给发送端的 Ack 确认只会确认最后一个连续的包,比如,发送端发了 1,2,3,4,5 一共五份数据,接收端收到了 1,2,于是回 ack 3,然后收到了 4(注意此时 3 没收到),此时的 TCP 会怎么办?我们要知道,因为正如前面所说的,SeqNum 和 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 是怎么动态地计算出 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

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

这样,在发送端就可以根据回传的 SACK 来知道哪些数据到了,哪些没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux 下,可以通过 tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。
s
这里还需要注意一个问题——接收方 Reneging,所谓 Reneging 的意思就是接收方有权把已经报给发送端 SACK 里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖 SACK,还是要依赖 ACK,并维护 Time-Out,如果后续的 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 包。

1
2
3
4
5
6
7
8
9
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 包丢了,而是因为网络延时了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,有这么几个好处:
s
1)可以让发送方知道,是发出去的包丢了,还是回来的 ACK 包丢了。

2)是不是自己的 timeout 太小了,导致重传。

3)网络上出现了先发的包后到的情况(又称 reordering)

4)网络上是不是把我的数据包给复制了。

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

Linux 下的 tcp_dsack 参数用于开启这个功能(Linux 2.4 后默认打开)