浅析 TCP 的流量控制和拥塞控制

WilliamGates
发布于 2023-10-8 11:02
浏览
0收藏

一、摘要

在上一篇​​TCP 滑动窗口原理解析文章​中,我们对 TCP 的滑动窗口原理进行一次总结,也提到了流量控制拥塞控制

本文我们重点来说说 TCP 的流量控制拥塞控制的实现。

话不多说,直接上干货!

二、流量控制

在上篇文章中我们提到,TCP 通过接受方实际能接收的数据量来控制发送方的窗口大小,从而实现所谓的流量控制。

理想的情况下,假设不受外界影响,两台计算机在整个传输过程中,可以保持基本相同的窗口大小值。

而实际的情况,很难做到这一点,因为外部环境比较复杂,可能会出现一些意想不到的问题。

下面我们一起来看看有哪些因素可能会影响窗口大小值。

2.1、影响滑动窗口大小的因素介绍

在实际的数据传输过程中,发送方的窗口大小主要依赖于接受方的可用窗口大小来计算。

因此如果接受方的可用窗口发生变化,发送方的窗口大小也必然会发生变化。

比较常见能影响接受方的可用窗口发生变化的因素,有如下几个:

  • 接受方的应用程序没办法及时读取数据,此时会影响滑动窗口大小值
  • 接受方的系统资源非常紧张,操作系统可能直接减少可用窗口大小的缓存空间

我们先看看第一个案例!

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

根据上图描述,大致的过程说明如下:

  • 1.发送窗口和接收窗口初始大小为 360,客户端发送 140 字节数据后,可用窗口变为 220
  • 2.服务端收到 140 字节数据后,可能因为某种阻塞,应用进程只读取了 40 个字节,还有 100 字节存在缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通过给客户端。
  • 3.客户端收到确认和窗口通告报文后,发送窗口减少为 260
  • 4.客户端再发送 180 字节数据,此时可用窗口减少到 80
  • 5.服务端收到 180 字节数据后,可能又因为某种阻塞,应用程序没有读取任何数据,这 180 字节就直接留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并再次发送确认信息时,通过窗口大小给客户端。
  • 6.客户端收到确认和窗口通告报文后,发送窗口减少为 80
  • 7.客户端发送 80 字节数据后,可用窗口变成 0
  • 8.服务端收到 80 字节数据后,应用程序依然没有读取数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端
  • 9.最后发送方和接收方,可用窗口都变成 0

当可用窗口都收缩为 0,会发生了窗口关闭,这个对数据传输会造成非常严重的影响,具体等会在说。

我们继续看看第二个案例!

当服务端系统资源非常紧张的时候,操心系统可能会直接减少了接收方的缓冲区大小,此时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

根据上图描述,大致的过程说明如下:

  • 1.客户端发送 140 字节的数据,于是可用窗口减少到了 220
  • 2.服务端收到 140 字节数据后,因为系统资源紧张,操作系统直接减少接收方的缓存空间,于是可用窗口大小收缩成 100 字节,此时又因为应用程序没有读取数据,收到的 140 字节一直留在了缓冲区中,最后发送确认信息时,通告窗口大小给对方。
  • 3.可能客户端因为还没有收到服务端的通告窗口报文,不知道此时接收方的窗口收缩成了 100,客户端只会看自己的可用窗口还有 220,所以客户端就发送了 180 字节数据,于是可用窗口减少到 40。
  • 4.服务端收到了 180 字节数据时,发现数据大小超过了接收窗口的大小,于是就把数据包丢弃掉。
  • 5.客户端此时收到第 2 步服务端发送的确认报文和通告窗口报文,尝试减少发送窗口到 100,把窗口的右端向左收缩了 80,此时可用窗口的大小就会出现的负值。

因此,从上面的分析中可以发现,如果发生了先减少缓存,再收缩窗口,就会出现数据丢包的现象。

为了防止这种情况发生,TCP 规定是不允许先减少缓存又收缩窗口的,而是采用先收缩窗口,然后告知发送方,过段时间再减少缓存,这样就可以避免了丢包情况。

2.2、窗口关闭的问题

上文中我们提到当可用窗口都收缩为 0,会发生了窗口关闭,此时发送方会停止向接受方传递数据,直到接受方可用窗口变成非 0,再次向发送方通告可用窗口的大小,最后发送方再次重新发起数据传输。

如果这个接受方向发送方通告窗口的 ACK 报文在网络中丢失了,可能会造成死锁!

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

根据上图描述,大致的过程说明如下:

  • 1.发送方向接受方发完 1001~2000 的数据,收到接受方的 ack 报文之后,可用窗口为 0
  • 2.发送方不在向接受方传递数据,并且等待接受方的非 0 窗口同志
  • 3.过了一段时间之后,接受方处理完数据,可用窗口增大,向发送方通告可用窗口的报文,很不幸,ack 报文丢失了
  • 4.最后发送方和接受方,等在默默的等待对方的报文数据,造成了死锁的现象

因此,当发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如果不采取措施,这种相互等待的过程,会造成了死锁的现象。

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

根据上图描述,大致的过程说明如下:

  • 1.如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器
  • 2.如果接收窗口不是 0,那么死锁的局面就可以被打破了

窗口探查探测的次数一般为 3 次,每次大约 30-60 秒(不同的操作系统实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

2.3、糊涂窗口综合症

在上文中,我们提到可能因为接受方阻塞,来不及取走接收窗口里的数据,导致发送方的窗口越变越小。

到最后,可能接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症

就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车,这样玩下去,司机不迟早的破产。

要解决这个问题其实也不难,等乘客数量超过一半,也就是 25 个,就进行发车。

解决糊涂窗口综合症,主要从以下两个方向入手:

  • 让接收方不通告小窗口值给发送方
  • 让发送方避免发送小数据

接收方通常的解决策略如下

当窗口大小小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

发送方通常的解决策略如下

使用 Nagle 算法来处理,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

  • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
  • 收到之前发送数据的 ack 回包

如果没满足上面条件中的一条,发送方就一直在囤积数据,直到满足上面的发送条件。

三、拥塞控制

在上文中我们也提到,面对复杂的网络环境,TCP 的流量控制能解决的问题比较有限,尤其是当网络出现拥堵的时候,这个时候 TCP 会采用拥塞控制来解决。

拥塞控制,其目的就是避免发送方的数据填满整个网络!

为了在发送方调节所要发送的数据量,我们需要定义了一个叫做拥塞窗口的概念,使用​​cwnd​​来表示。

在前面我们有提到过发送窗口​​swnd​​​大小,取自于接收窗口​​rwnd​​​的大小,两者基本上是约等于的关系,由于入了拥塞窗口的概念后,此时发送窗口的值是 ​​swnd = min(cwnd, rwnd)​​,也就是拥塞窗口和接收窗口中的最小值。

拥塞窗口​​cwnd​​值的变化情况,取自于网络环境

  • 当网络出现拥堵,​​cwnd​​就会变小
  • 当网络没有出现拥堵,​​cwnd​​就会变大

那 TCP 是如何知道当前网络是否出现拥堵呢?

一般来说,只要发送方没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。

当网络出现拥塞时,TCP 主要有以下四种主要的算法来控制发送量。

  • 慢启动算法
  • 拥塞避免算法
  • 拥塞发生算法
  • 快速恢复算法

下面我们依次来看看具体的实现逻辑。

3.1、慢启动算法

TCP 在刚建立完连接后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,防止一下子发送大量的数据,填充整个网络。

慢启动的算法,就一个规则:当发送方每收到一个 ACK,拥塞窗口​​cwnd​​的大小就会加 1。

整个慢启动的过程,可以用下图来描述。

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

其中​​cwnd=1​​表示可以传一个 MSS 大小的数据。

可以看出慢启动算法,发包的个数是成指数性的增长。

那是不是可以无限的增长呢?

答案肯定不是,慢启动有个门限​​ssthresh​​变量值。

  • 当​​cwnd < ssthresh​​时,使用慢启动算法
  • 当​​cwnd >= ssthresh​​时,就会使用拥塞避免算法

下面我们再来看看拥塞避免算法!

3.2、拥塞避免算法

当拥塞窗口​​cwnd​​​超过慢启动门限​​ssthresh​​就会进入拥塞避免算法。

进入拥塞避免算法后,它的规则也比较简单:每当收到一个 ACK 时,​​cwnd​​​增加​​1/cwnd​​。

假定​​ssthresh​​为 8,可以用下图来描述增长过程。

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

当 8 个 ACK 应答确认到来时,每个确认增加​​1/8​​​,8 个 ACK 确认,​​cwnd​​一共增加 1,于是这一次能够发送 9 个 MSS 大小的数据,变成了线性增长。

所以,可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。

就这么一直增长着,网络也可能会出现拥塞,一旦出现拥塞,就可能会出现丢包现象,这时就需要对丢失的数据包进行重传。

当触发了重传机制,也就进入了拥塞发生算法阶段。

3.3、拥塞发生算法

当网络出现拥塞,也就是会发生数据包重传,此时会使用拥塞发生算法,重传机制主要上文介绍的两种:

  • 超时重传
  • 快速重传

当发生超时重传时,sshresh 和 cwnd 的值会发生如下变化:

  • ​ssthresh​​​设为​​cwnd/2​
  • ​cwnd​​​重置为​​1​

整个过程可以用如下图来描述!

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

当进入超时重传的时候,基本上就重新进入了慢启动过程,慢启动的过程就是会突然减少数据流的,然后重新开始。

除此之外,还有另一种快速重传的机制,当接收方发现丢了一个中间包的时候,发送方收到 3 次重复的 ACK,会进入快速重传阶段,不必等待超时重传。

当发生超时重传时,sshresh 和 cwnd 的值会发生如下变化:

  • ​cwnd​​​设为​​cwnd/2​
  • ​ssthresh​​​设置为​​cwnd​

然后进入快速恢复算法阶段。

3.4、快速恢复算法

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像超时重传那样重新开始。

进入快速恢复算法之后,主要的调整如下:

  • ​cwnd​​​设置为​​ssthresh + 3​​( 3 的意思是确认有 3 个数据包被收到了)
  • 重传丢失的数据包
  • 如果再收到重复的 ACK,那么​​cwnd​​增加 1
  • 如果收到新数据的 ACK 后,设置​​cwnd​​​为​​ssthresh​​,接着就进入了拥塞避免算法

整个过程可以用如下图来描述!

浅析 TCP 的流量控制和拥塞控制-鸿蒙开发者社区

采用快速恢复算法,依然可以保持比较高的数据传输,不至于向超时重传那样,重新开始!

四、小结

本文整理了一些优秀网友分享的知识,结合自己的理解比较全面的分析了 TCP 滑动窗口的原理,希望对大家有所帮助。

总的来说,TCP 滑动窗口主要有以下作用:

  • 1.TCP 在滑动窗口的基础上提供了流量控制,避免客户端发送的数据超过服务端的接受能力,从而导致数据包丢失。
  • 2.针对网络拥堵问题,TCP 提供了拥塞控制,避免发送的数据填满整个网络。

五、参考

1、​​小林coding - 图解 TCP 滑动窗口​

2、迹寒 - TCP滑动窗口原理介绍


文章转载自公众号:Java极客技术

标签
已于2023-10-8 11:02:50修改
收藏
回复
举报
回复
    相关推荐