TCP 实现可靠传输的方式之一,是通过序列号与确认应答。在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。正常的传输如下所示:
但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?所以 TCP 针对数据包丢失的情况,会用重传机制解决。
接下来说说常见的重传机制:
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。TCP 会在以下两种情况发生超时重传:
超时重传的两种情况具体如下图所示:
超时时间应该设置为多少呢?我们先来了解一下什么是 RTT(Round-Trip Time 往返时延),从下图我们就可以知道:
RTT 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。
假设在重传的情况下,超时时间 RTO 「较长或较短」 时,会发生什么事情呢?
上图中有两种超时时间不同的情况:
精确的测量超时时间 RTO 的值是非常重要的,这可让我们的重传机制更高效。根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。
至此,可能大家觉得超时重传时间 RTO 的值计算,也不是很复杂嘛。好像就是在发送端发包时记下 t0 ,然后接收端再把这个 ack 回来时再记一个 t1,于是 RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。
实际上 「报文往返 RTT 的值」 是经常变化的,因为我们的网络也是时常变化的。也就因为 「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。我们来看看 Linux 是如何计算 RTO 的呢?
估计往返时间,通常需要采样以下两个:
RFC6289 建议使用以下的公式计算 RTO:
其中 SRTT 是计算平滑的 RTT ,DevRTR 是计算平滑的 RTT 与 最新 RTT 的差距。在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。别问怎么来的,问就是大量实验中调出来的。
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?于是就可以用「快速重传」机制来解决超时重发的时间等待。
TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。快速重传机制,是如何工作的呢?其实很简单,一图胜千言。
在上图,发送方发出了 1,2,3,4,5 份数据:
所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。
比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。
根据 TCP 不同的实现,以上两种情况都是有可能的。可见,这是一把双刃剑。为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。
还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认)。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。
Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。下面举例两个例子,来说明 D-SACK 的作用。
可见,D-SACK 有这么几个好处:
在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。