0%

引言

本篇文章主要是介绍一下 TCP 中的慢启动、拥塞避免算法、快速启动以及快速恢复间的基本概念,也便于加强记忆。

滑动窗口



在开始讲解之前,我们需要了解几个基本概念,首先要介绍的就是滑动窗口协议。什么是滑动窗口协议?以及为什么要有它的存在呢?我们知道 TCP 是基于流的协议,发送方每次要发送多少数据给接收方呢?如果发送一个 MSS 效率太低;如果发送太多,接收方消化不了(接收缓冲区满),又或者网络带宽低,造成数据丢包。

所以发送方需要知道,它每次最多要发送多少数据给接收方比较合适,这里就用到了滑动窗口。滑动窗口就是接收方通告的窗口。

从上图中,有 11 个字节需要发送给服务端(滑动窗口的单位是:字节),其中滑动窗口为 6 字节大小。1、2、3 前 3 个字节已经被接收并且完成确认。4、5、6 三个字节已经接收,但是还没有进行确认。所以此时发送分可以发送 7、8、9 三个字节给接收方,但是 10、11 不能发送。

窗口运动



  1. 窗口左边沿向右运动,称为合拢,发生在数据被发送和确认时。
  2. 窗口右边沿向右运动,称为张开,此时发送端可以发送更多的数据,主要发生在接收端确认了数据并且释放了 TCP 接收缓冲区。
  3. 窗口右边沿向做移动,称为收缩,这种方式是强烈不推荐的。

慢启动

发送端开始向接收端发送多个报文段,直到接收方通告的窗口大小为止。如果发送方跟接收方处于同一个局域网时,这是成立的。但是,如果两者之间存在多个路由器和速率较慢的链路时,就可能出现一些问题。一些中间路由器必须缓存分组,并有可能耗尽存储器空间,降低 TCP 连接的吞吐量。

慢启动算法出现了,该算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。

慢启动为发送方 TCP 增加另一个窗口:拥塞窗口,记为 cwnd。当连接建立之后,拥塞窗口被初始化为 1 个报文段。每收到一个 ACK,拥塞窗口就增加一个报文段(cwnd 以字节为单位,但是慢启动以报文段大小为单位增加)。发送方取拥塞窗口与滑动窗口中的最小值作为发送上限。

📚 Tips

拥塞窗口是发送方使用的流量控制,而滑动窗口(通告窗口)则是接收方使用的流量控制。

发送方开始发送一个报文段,然后等待 ACK。当收到该 ACK 时,拥塞窗口从 1 增加到 2,即可以发送两个报文段。当收到这两个报文段的 ACK 时,拥塞窗口增肌为 4。也就是说,拥塞窗口增加是一种指数的关系。

💡 总结

慢启动的出现就是为了避免发送方一下充满滑动窗口大小而间接造成 TCP 吞吐量下降的问题。它从一个报文段开始,根据 ACK 的接收情况,以指数的方式增加发送的报文数,直到拥塞窗口或者滑动窗口最小值为止。

拥塞避免

慢启动算法是一个连接上发起数据流的方法。但有时候我们会达到中间路由器的极限,此时分组将被丢弃。拥塞避免算法就是一种处理丢失分组的方法。

拥塞避免算法假定由于分组受到损坏引起的丢失是非常少的(远小于 1%),因此分组丢失就意味着在源主机和目的主机之间的某处网络上发生了拥塞。有两种分组丢失的指示:发生超时接收到重复的确认

拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当发生拥塞时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来做到这一点。但是实际中,这两个算法通常一起实现。

拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口 cwnd 和一个慢启动门限 ssthresh。算法的工作过程如下:

  1. 对一个给定的连接,初始化 cwnd 为 1 个报文段,ssthresh 为 65535 个字节。
  2. TCP 输出例程的输出不能超过 cwnd 和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的力量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
  3. 当拥塞发生时(超时或收到重复的确认),ssthresh 被设置为当前窗口大小的一半(cwnd 和接收方通告窗口的最小值,但最少为 2 个报文段)。此外,如果是超时引起的拥塞,则 cwnd 被设置为 1 个报文段(这就是慢启动)。
  4. 当新的数据被对方确认时,就增加 cwnd,但增加的方法依赖于我们是否在进行慢启动或拥塞避免。如果 cwnd 小于或等于 ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止,然后转为执行拥塞避免。

慢启动算法初始设置 cwnd 为 1 个报文段,此后以指数的形式增加。而拥塞避免算法要求每次收到一个 ACk 时,将 cwnd 增加 1/cwnd,也就是说线性的增加增长。也就是说,拥塞避免算法希望在一个往返时间内最多为 cwnd 增加 1 个报文段(不管和这个 RTT 中收到多少个 ACK),而慢启动根据 RTT 中所收到的 ACK 个数增加 cwnd。

以上图为例,当 cwnd 为 32 个报文段时就会发生拥塞。于是设置 ssthresh 为 16 个报文段,而 cwnd 为 1 个报文段。时刻 0 发送一个报文段,随着 ACK 的到来,cwnd 以指数的形式增加到 ssthresh 时才停止(慢启动阶段),从该时刻启,cwnd 以线性的方式增加,每个 RTT 内最多增加 1 个报文段(拥塞避免阶段)。

快速重传与快速恢复

我们不知道一个重复的 ACK 是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重排序造成的,因此我们等待少量重复的 ACK 到来。加入这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的 ACK 之前,只可能产生 1~2 个重复的 ACK。如果一连串收到 3 个或 3 个以上重复的 ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的报文段而不等待超时定时器溢出。这就是快速重传算法。接下来执行的不是慢启动算法而是拥塞避免算法,这就是快速恢复算法

这种情况下为什么没有执行慢启动是因为收到重复的 ACK 不仅仅告诉发送端一个分组丢失了。由于接收方只有在收到另一个报文段时才会产生重复的 ACK,也就是说,在收发两端仍有数据流动,所以此时不应该执行慢启动来突然减少数据流。

该算法执行过程通常为:

  1. 当收到第 3 个重复的 ACK 时,将 ssthresh 设置为当前拥塞窗口 cwnd 的一半。重传丢失的报文段,设置 cwnd 为 ssthresh 加上 3 被的报文段大小。
  2. 每次收到另一个重复的 ACK 时,cwnd 增加 1 个报文段大小并发送 1 个分组(如果新的 cwnd 允许发送)。
  3. 当下一个确认新数据的 ACK 到达时,设置 cwnd 为 ssthresh(在第 1 步中设置的值)。这个 ACK 应该是在进行重传后的一个往返时间内对步骤 1 中重传的确认。另外这个 ACK 也应该是对丢失分组和收到的第 1 个重复 ACK 之间所有中间报文的确认。这一步采用的是拥塞避免,因为当分组丢失时,我们将当前的速率减半(ssthresh 为当前拥塞窗口的一半)。

参考

  • 【TCP/IP 详解】第 20、21 章。