易栈 · 一盏

塞外秋来,衡阳雁去

运输层:TCP数据流与定时器

一、TCP的交互数据流

交互数据流的特点是数据量小,数据包都是小分组。TCP传输小分组的效率比较低,大部分的资源都耗费在传输IP首部和TCP首部上(至少40字节)。在广域网上,大量的小分组可能会导致网络拥塞。

TCP中用于减少小分组的算法主要有两个:

  • 经受时延的确认:TCP在收到数据后,不会马上返回ACK,而是延时一会看有没有数据需要发送的,有的话就捎带着把ACK一起发出去;如果超过时延还没有数据需要发送,才会单发ACK,这时候发的ACK称为经受时延的ACK。一般采用的时延为200ms。
  • Nagle算法:该算法要求一个TCP连接上最多只能有一个未被确认的小分组。也就是发送了一个小分组后,要等该分组的确认到达之后才能继续发新的小分组。这个算法的优点是它是自适应的:确认返回的越快,数据也就发送得越快。

这两种算法有助于减少小分组,减小丢失重传的概率,但是可能会引入额外的时延。

二、TCP的成块数据流

成块数据流的报文段基本上都是满长度的,所以要注意控制发送速度,防止超过网络负载或者超过对方的处理速度。前面讲过TFTP使用停止等待协议:每次发完一个数据块,需要得到对方的确认才能接着发下一个数据块。这个方式缺点是不够灵活、速度慢。TCP使用滑动窗口协议来做流量控制。

前面讲过TCP报文中有一个“窗口大小”字段。窗口大小表示接收方剩余的缓冲区大小,即发送方可以立即发送的数据量。进一步讲,窗口大小是发送方能够发送出去的但还没有收到ACK的最大数据报文段。这样就允许发送方在停止并等待确认前可以连续发送多个分组(而不用每发一个分组就停下来等待确认),直到未被确认的报文段字节数达到对方通告的窗口大小。接收方不必确认每一个收到的分组,而是累积收到多个分组后才返回一个确认报文段,并在确认报文段中更新自己的窗口大小。

三、TCP的超时与重传

TCP为每个发送的数据设置一个定时器,如果定时器溢出是还没收到确认,就重传该数据。那么问题来了,超时多久算超时呢?这就是重传超时时间RTO(Retransmission Timeout)。一个报文段发出去后,如果超过RTO时间还没得到确认,就会重传。重传会尝试多次,为了避免频繁的重传加剧拥塞情况,每次重传后RTO增加一倍(指数增长),这就是“指数退避”。

RTO一般基于往返时间RTT来计算。往返时间RTT(Round-trip Time)是从发送数据到接受到该数据的应答的时间(也就是包括了数据&数据的应答在链路上传输的时间和接收方生成应答的时间)。RTT的值随着网络状态变化,需要跟踪这些变化并相应改变RTO。

RTT的测量方法有两种:

  • 计算从发送一个报文段到接收到对它的确认之间的时间。注意数据报文段和ACK不是一一对应的,接收方生成ACK的时延(为了减少小分组产生的时延、滑动窗口协议导致的时延)也会被计算在内。这种方法对一个连接通常只启动一个定时器,所以RTT的计算只能串行地进行(不能同时对多个报文段进行计时)。这就导致采样频率低,被估计的RTT不准确,使用时间戳选项可以避免这个问题。
  • 使用时间戳选项。发送方在包里携带一个时间戳,接收方把这个时间戳放到应答数据中。发送方接收到应答数据后,取出这个时间戳,和自己本地时间做对比,就可以知道RTT。这种方式需要通信双方支持TCP协议的时间戳选项字段。

四、拥塞控制

为了不发生拥塞,最重要的是控制发送速度。

前面讲到接收方用窗口大小来控制发送方的发送速度,这个窗口称为通告窗口。但是通告窗口是接收方根据自己的缓冲区大小、处理速度等因素给出的建议,实际的发送速度还受到网络情况的限制。发送方会根据网络拥塞情况实时调整自己的发送窗口,这个窗口即拥塞窗口cwnd(Congestion Window)。所以就有:

发送窗口 = min(拥塞窗口, 通告窗口)

拥塞控制的过程简单来说是:在发送刚开始的时候,先将拥塞窗口快速地增加到某个阈值,然后缓慢增加直到到达上限。如果过程中出现拥塞,则减小拥塞窗口,然后重复上述过程。(加速-> 刹车-> 加速)

具体来说是:执行慢启动算法将cwnd快速增加到慢启动启动门限ssthresh(Slow Start Threshold),然后执行拥塞避免算法使cwnd缓慢增加,上限为对方通告窗口大小

下面来逐个介绍上述过程用到的几种算法(注意cwnd的增加/减少,都是以报文段大小为单位。假设每个报文都是满长度,则单位为最大报文段MSS):

  • 慢启动(Slow Start):初始化cwnd为1个报文段,每当有一个报文段被确认,cwnd就增加一个报文段。cwnd呈指数增长(假如当前可以发n=cwnd/segsize个报文段,那么在一个往返时间RTT内会收到对这n个报文段的确认,则下一次可以发2n个报文段)。注意慢启动时cwnd的增长速度是很快的,慢启动不是指增长速度慢,而是指起始值小
  • 拥塞避免(Congestion Avoidance)每次有一个报文段被确认,就将cwnd增加segsize/cwnd个报文段。cwnd呈线性增长(假如当前可以发n=cwnd/segsize个报文段,那么在一个往返时间内收到对n个报文段的确认,忽略这个过程中cwnd的增量,则cwnd总共增加n*segsize/cwnd=1个报文段,则下一次可以发n+1个报文段)。
  • 快速重传(Fast Retransmit):接收到3个重复ACK就马上重传丢失的数据报文段,而无需等待超时定时器溢出。收到1~2个重复ACK时有可能只是报文段失序,收到3个ACK基本能认为是报文段丢失了,但是能收到ACK表明网络还是畅通的,所以马上重传从而快速恢复正常状态。
  • 快速恢复(Fast Recovery):快速恢复一般紧接在快速重传之后。首先设置ssthresh = cwnd/2,然后设置cwnd = ssthresh + 3个报文段(之前收到的重复ACK数目),之后每收到一个重复的ACK,cwnd就增加一个报文段。直到收到一个确认新数据的ACK,设置cwnd为ssthresh,开始执行拥塞避免算法。

有两种拥塞的情况:发生超时接收到重复的确认。发生超时意味着网络拥塞比较严重。收到重复的确认意味着拥塞比较轻微,只是发生了包的失序或者丢失了少量包,但是网络还是畅通的。两种拥塞情况的处理过程分别为:

  • 发生超时:慢启动 -> 拥塞避免。
  • 接收到重复的确认:快速重传 -> 快速恢复 -> 拥塞避免。

另外注意ssthresh的值只在两种情况下会被改变:初始化时发生拥塞时。初始化时ssthresh被设置成65535字节,所以连接刚建立时会一直执行慢启动(不会达到慢启动门限),直到发生拥塞或者到达通告窗口。发生拥塞时ssthresh会设置成当前拥塞窗口的一半。

五、重新分组

如果丢失的那个报文段长度比较小,重传的时候可能会重新分组来发送一个较大的报文段,从而提高性能。

六、TCP定时器

前面讲到过TCP中用到的几个定时器,这里总结一下。TCP有4种的定时器:

  • 2MSL定时器:测量一个连接处于TIME_WAIT状态的时间。
  • 重传定时器:用来判断一个报文段是否超时没有得到确认,以便重传。
  • 保活定时器:TCP连接建立之后,即使连接的双方都不发送任何数据,连接也可以一直保持。有时候希望对方是否还在线,可以用保活定时器隔一段时间发一个探查消息。探查消息是一个没有数据的ACK报文,它的序号字段比接收方期望收到的序号小1,从而使接收方返回一个ACK来强调自己期望收到的序号大小。
  • 坚持定时器:当通告窗口为0时,发送方会停止发送,直到接收方返回ACK来更新窗口。这里有一个问题是:ACK的传输并不可靠。如果这个更新窗口的ACK丢失了,双方会因为都在等待对方而停止通信。所以发送方使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否已增大。

本文链接:易栈 - 运输层:TCP数据流与定时器