一、前言

  这篇博客来讲讲TCP的拥塞控制机制,这是TCP中比较复杂的一个部分,它与TCP的很多内容都有关联,但是这里不可能将这些内容都说一遍,所以以下描述将建立在读者对TCP的机制有一定了解的基础之上。这一部分内容确实有些复杂,我尽量在少涉及TCP其他内容的条件下将它叙述清楚。


二、正文

 2.1 什么是拥塞控制

  我们都知道,网络错综复杂,在这个复杂的网络中,很少有两台主机是直接相连。尽管如此,我们还是可以通过网络与其他主机通信,这是为什么?因为我们发送到网络中的数据,当达到网络中的一个节点时(假设是路由器),它会根据数据包含的地址,帮我们将数据转发到离目的地更近的路由器,或直接转发到目的地。但是,这些路由器不是直接就可以转发的,它们需要先将接收到的数据放入自己的内存(可能还要做一些处理),再从中取出进行转发。

  这里就面临一个问题:路由器的内存是有限的,若同一时间到达某个路由器的数据太多,这个路由器将无法接收所有的数据,只能将一部分丢弃;或者同一台路由器数据太多,后面到达的数据将要等待较长的时间才会被转发。网络中的数据太多,导致某个路由器处理不过来或处理地太慢,这就是网络拥塞。若是对于TCP这种有重传机制的传输协议,当发生数据丢失时,重传数据将延长数据到达的时间;同时,高频率的重传,也将导致网络的拥塞得不到缓解。拥塞控制,就是在网络中发生拥塞时,减少向网络中发送数据的速度,防止造成恶性循环;同时在网络空闲时,提高发送数据的速度,最大限度地利用网络资源。说的简单点,就和堵车差不多,路就这么宽,来的车多了,自然过的就慢,所以在必要的时候要限号。


 2.2 拥塞控制的方法

  在理论中,拥塞控制有两种实现方式:

  • 端到端拥塞控制:在这种拥塞控制方法中,由发送数据的端系统自己来判断是否拥塞,然后调整传输速率。比如说发送的数据已经超时却还没有接收到确认报文,数据往返时延过高,接收到对同一个数据段的重复确认……都可以认为是网络拥塞的现象;若发送端检测到这种现象,就应该降低发送数据的速率,若没有,则可以慢慢提高速率;
  • 网络辅助的拥塞控制:由网络中的路由器来告诉发送方,网络的拥塞情况。一般有两种方式:(1)路由器直接向发送端发送报文,告知网络拥塞情况;(2)路由器更改数据段中的某个标志,来提示网络中的拥塞情况,然后数据将这个标志携带到目的主机,再由目的主机根据这个标志,向发送端发送报文,告知拥塞情况(被包含在确认报文中);

 2.3 TCP的拥塞控制方法

  因为网络层不会提供拥塞的反馈信息,所以TCP协议采用的是第一种方式——自己判断网络的拥塞情况。当TCP检测到网络拥塞,则降低数据的发送速率,否则增加数据的发送速率。这里就将面临三个问题:

  1. TCP如何限制数据的发送速率;
  2. TCP如何检测网络中是否拥塞;
  3. TCP采用什么算法来调整速率(什么时候调整,调整多少);

  首先来回答第一个问题。了解TCP的应该知道,TCP不是发送一个数据段,接收到确认后再发送另一个数据段,它采用的是流水线的方式。TCP的每一个数据段都有一个序号,而TCP维护一个发送窗口来发送是数据,这个窗口就是一个区间。所有序号位于这个窗口内的数据段都会被一次性发送,而不需要等待之前发送的数据段被确认。而每当最早发送出去的数据段被确认,窗口就会向前移动,直到移动到第一个没有被确认的序号,这时候又会有新的数据段序号被包含在窗口中,然后被发送出去。所以限制数据发送速率最好的方式就是限制窗口的大小。在发送方的TCP程序会跟踪和维护一个叫做拥塞窗口的变量,用来进行拥塞控制。拥塞窗口被称为cwnd。在TCP发送端,所有被发送但是还没收到确认的数据段必须落在这个窗口中,所有,当网络拥塞时,TCP程序将减小cwnd,而网络通畅时,增大cwnd,以此来控制数据发送的速率。

  接着来回答第二个问题。TCP程序将通过数据发送的一些现象来推测网络是否拥塞,比如:

  • 若发送一条数据段后,成功接收到了接收方的确认报文,则可以认为网络没有拥塞;

  • 若发送出一条数据段后,在规定时间内没有收到确认报文(丢失或时延太大),则可以认为网络出现了拥塞;

  • 若连续收到接收方对同一条报文的三次确认,则可以推测那条报文丢失,即发生了拥塞;

  上面的第三种情况,与TCP快速重传机制有关,我这里不详细说明,只是大致介绍一下。TCP无法保证数据能够按顺序到达接收端,所以,可能出现序号靠后的数据报反倒先到达的情况。而TCP接收方并不是接收到哪一条报文,就向发送方发送哪条报文的确认,它是通过发送当前应该接收到的序号最小的报文进行确认。举个例子:

  1. 发送方同时发送了1,2,3,4,5这五个序号的报文,假设接收方接收到序号1的报文,于是将向发送方发送一个确认号为2的确认报文(TCP发送方通过确认号来判断接收方接收到的报文),告诉发送方我已经收到2之前的报文了,下一条报文我想要2
  2. 接收方接收到2号报文后,发送确认号3,告诉发送方我接收到了3之前的所有报文;
  3. 这时候因为网络的不确定性,在3号没有到达前,4号报文先到达了,接收方接收到后,将它放入缓存,并依旧回复确认号3,表示它需要的是3号报文;
  4. 可是,之后到达的却5,于是它将5放入缓存,并依旧回复3;
  5. 直到3迟迟的到来,这时候接收方接收3,同时发现缓存中存在4和5,于是回复发送方确认号6,表示自己已经接收到了6之前的全部报文,下一条需要6

  在这里,发送方一共接收到了三条确认号为3的确认报文(确认号为3是对2号报文的确认),第一条正常,后面两条冗余。而所谓的快速重传就是指:若发送方接收到对同一条报文的三次冗余确认(也就是四次确认),就认为这条报文的下一条已经丢失,于是不管计时器是否超时,都直接重传这条报文的下一条。上面的例子中,2号报文被冗余确认了两次,还不构成快速重传的条件。而为什么是三次,其实就是概率和时间长短的折中选择。

  下面回到正题,当快速重传的条件发生,发送方将认为出现了拥塞导致丢包。所有归根到底,TCP判断拥塞的方式就是检测有没有丢包。于是我们就可以回答第三个问题了,TCP如何调整发送速率——在没有丢包时慢慢提高拥塞窗口cwnd的大小,当发生丢包事件时,减少cwnd的大小。当然,具体的算法要复杂的多,TCP调整拥塞窗口的主要算法有 慢启动拥塞避免 以及 快速恢复 ,其中前两个是TCP规范要求必须实现的,而第三个则是推荐实现的,TCP根据情况在这三者之间切换。 下面我就来一一介绍。


 2.4 慢启动

  在讲解之前,首先得知道一些名词:

  • MSS:最大报文段长度,TCP双方发送的报文段中,包含的数据部分的最大字节数;
  • cwnd:拥塞窗口,TCP发送但还没有得到确认的报文的序号都在这个区间;
  • RTT:往返时间,发送方发送一个报文,到接收这个报文的确认报文所经历的时间;
  • ssthresh:慢启动阈值,慢启动阶段,若cwnd的大小达到这个值,将转换到拥塞避免模式;

  慢启动是建立TCP连接后,采用的第一个调整发送速率的算法(或叫模式)。在这个阶段,cwnd通常被初始化为1MSS,这个值比较小,在这个时候,网络一般还有足够的富余,而慢启动的目的就是尽快找到上限。在慢启动阶段,发送方每接收到一个确认报文,就会将cwnd增加1MSS的大小,于是:

  • 初始cwnd=1MSS,所以可以发送一个TCP最大报文段,成功确认后,cwnd = 2MSS
  • 此时可以发送两个TCP最大报文段,成功接收后,cwnd = 4 MSS
  • 此时可以发送四个TCP最大报文段,成功接收后,cwnd = 8 MSS……

  由于TCP是一次性将窗口内的所有报文发出,所以所有报文都到达并被确认的时间,近似的等于一个RTT(记住这个结论,后面所述的RTT都是基于这个结论)。所以在这个阶段,拥塞窗口cwnd的长度将在每个RTT后翻倍,也就是发送速率将以指数级别增长(所以不要被慢启动这个名字误导了)。那这个过程什么时候改变呢,这又分几种情况:

  • 第一种:若在慢启动的过程中,发生了数据传输超时,则此时TCPssthresh的值设置为cwnd / 2,然后将cwnd重新设置为1MSS,重新开始慢启动过程,这个过程可以理解为试探上限;
  • 第二种:第一步试探出来的上限ssthresh将用在此处。若cwnd的值增加到>= ssthresh时,此时若继续使用慢启动的翻倍增长方式可能有些鲁莽,所以这个时候结束慢启动,改为拥塞避免模式;
  • 第三种:若发送方接收到了某个报文的三次冗余确认(即触发了快速重传的条件),则进入到快速恢复阶段;同时,ssthresh = cwnd / 2,毕竟发生快速重传也可以认为是发生拥塞导致的丢包,然后cwnd = ssthresh + 3MSS

  以上就是慢启动的过程,下面来介绍拥塞避免。


 2.5 拥塞避免

  刚进入这个模式时,cwnd的大小近似的等于上次拥塞时的值的一半(这是由进入这个模式的条件决定的),也就是说当前的cwnd很接近产生拥塞的值。所以,拥塞避免是一个速率缓慢且线性增长的过程,在这个模式下,每经历一个RTT(请注意2.4中有关RTT的结论),cwnd的大小增加1MSS,也就是说,假设cwnd包含10个报文的大小,则每接收到一个确认报文,cwnd增加1/10 MSS。这个线性增长的过程什么时候结束,分为两种情况:

  • 第一种:在这个过程中,发生了超时,则表示网络拥塞,这时候,ssthresh被修改为cwnd / 2,然后cwnd被置为1MSS,并进入慢启动阶段;
  • 第二种:若发送方接收到了某个报文的三次冗余确认(即触发了快速重传的条件),此时也认为发生了拥塞,则,ssthresh 被修改为 cwnd / 2,然后cwnd被置为ssthresh + 3MSS,并进入快速恢复模式;

  我们可以看到,慢启动和拥塞避免在接收到三个冗余的确认报文时,处理方式是一样的:判断发生了拥塞,并减小ssthresh的大小,但是cwnd的大小却不见得有减小多少,这一点让人疑惑。我个人认为是这样,虽然发送方通过接收三次冗余确认报文,判断可能存在拥塞,但是既然可以收到冗余的确认报文,表示拥塞不会太严重,甚至已经不再拥塞,所以对cwnd的减小不是这么剧烈。


 2.6 快速恢复

  快速恢复和上面两种模式不太一样,这种模式在TCP规范中并没有强制要求实现,只是一种推荐实现的模式。在快速恢复阶段,每接收到一个冗余的确认报文,cwnd就增加1MSS,其余不变,而当发生以下两种情况时,将退出快速恢复模式:

  • 第一种:在快速恢复过程中,计时器超时,这时候,ssthresh被修改为 cwnd / 2,然后cwnd被置为1MSS,并进入慢启动阶段;
  • 第二种:若发送方接收到一条新的确认报文(不是冗余确认),则cwnd被置为ssthresh,然后进入到拥塞避免模式;

  这里有一个疑问,进入到此模式的条件就是接收到三次冗余的确认报文,判断报文丢失,那为什么再次接收到冗余确认报文时,cwnd还是要增长呢?我搜遍网上博客,只在一篇博客中找到一种说法,我认为还是有一定道理的:此时再次收到一条冗余的确认报文,表示发送端发出的报文又有一条离开网络到达了接收端(虽然不是接收端当前想要的一条),这说明网络中腾出了一条报文的空间,所以允许发送端再向网络中发送一条报文。但是由于当前序号最小的报文丢失,导致拥塞窗口cwnd无法向前移动,于是只好将cwnd增加1MSS,于是发送端又可以发送一条数据段,提高了网络的利用率。


 2.7 三种模式相互转换状态图

  下面给出一张三种模式互相转换的状态图,途中箭头上的是转换的条件,条件有上下两部分,横线上方的是上面事件引起的转换,而下方是转换时发生的操作。


三、总结

  拥塞控制应该是TCP中相对比较复杂的一个部分了,理解起来不是那么容易。在写这篇博客之前,对于上面所说的三种模式,我只知道具体的流程,却有很多地方不清楚为什么要这么做。但是在写这篇博客时,反复阅读书本,找博客阅读的过程让我对慢慢理解了它们的意图。这就是我写博客的理由,通过复述,来加深自己的理解,同时也希望这篇博客对看到的人有所帮助。


四、参考