💐个人主页:初晴~
📚相关专栏:计算机网络那些事
如果我们严格依照“确认应答”机制,针对每一个发送的数据段,都需要一个ACK确认应答,当收到ACK应答报文后,才继续发下一个报文。这样的发送效率势必是非常低下的,某个数据传输时间过长时,就会直接影响后续所有数据包的传输,传输性能非常差。
为了应对这一问题,尽可能地提高tcp传输的效率,于是就引入了“滑动窗口”机制。
一、何为滑动窗口
说的通俗一点就是把 “发送一个等待一个” 变成 “发送一批等待一批” 。相当于将多次等待的时间重叠在一起了。将等待时间从所有报文等待时间之和转变为了这批报文中最长的等待时间,效率肉眼可见地提升了许多。
理论上,批量发送的数据越多,效率也越高。但是理想很美好,现实很骨感。受网络带宽、接收方数据处理能力等的影响,不可能会让发送端无限制的扩大窗口的。
于是,实际应用中,在TCP头部的 “窗口大小” 字段中,接收端会告知发送端其当前可用的缓冲区空间。这个值是以字节为单位的,表示接收端还能接受多少字节的数据。通常来讲,窗⼝越⼤, 则⽹络的吞吐率就越⾼,传输的效率越高。
上图中的窗口大小就为4000字节。
那么发送端是等到这一批数据全部传输完再传下一批吗?
事实上并不是这样的。这样也体现不出滑动窗口的 “滑动” 了。我们接着以窗口大小为4000,每个数据段长度为1000时举例:
- 发送前四个数据段的时候,不需要等待任何ACK,直接发送
- 当第四个数据段发送完后,发送端发现 发送缓冲区 内的数据已经达到窗口大小了,就会停止继续发送,开始等待 ACK
- 收到第⼀个 2001 ACK后,说明 1001~2000 的数据得到应答了,滑动窗⼝就会向后滑动,继续发送第五个段 5001~6000 的数据,依次类推
操作系统内核为了维护这个滑动窗⼝,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区删掉
像这样,就保证了在传输过程中,窗口的大小保持一定,窗口所在的位置不断滑动的效果了。最大程度上利用了窗口的资源,达到最佳效率。
二、滑动窗口的一些优化机制
1、快速重传
我们得注意,完成上述滑动窗口的前提条件是可靠性。但现实中网络传输受种种因素的影响,很可能会出现丢包的现象。应对丢包问题,又该做出哪些优化?让我们接着往下看
情况一:数据包成功抵达,但ACK丢包了
事实上,这种情况对后续传输的进行不会有啥影响。
为啥呢?让我们回忆一下确认序号的含义:表示的是接收端接收到的数据的最后一个字节的序号+1。也就是说,确认序号之前的所有数据,都已经成功送达了。
假如说此时确认序号为 1001 的ACK丢包了,但是 2001 的ACK成功送到。发送方收到 2001 后,就意味着着序号为 2001 之前的所有数据都已经收到了。
我们不难发现,后一个 ACK 可以涵盖前一个 ACK 的意义。不管返回的 ACK 再怎么丢包,也不可能全部都丢了,总有包能成功送达 发送端,只要接收到某一个 ACK 包,就能确定在其之前的数据全都送达了,这样在这之前无论丢了多少 ACK包 都不会有啥影响了。
情况二:数据包出现丢包
如上图,B 收到了数据 1~1000、2001~3000,其中 1001~2000 的数据发生了丢失。
此时,当 B 收到数据 2001~3000 的时候,返回的 ACK 的确认序号不是3001,而是1001。
这也很好理解,因为1001~2000数据的丢包,3001之前的数据并没有全部都收到,只有序号 1001 之前的所有数据才全部被收到了
而接下去,B 收到的 3001~4000/4001~5000等数据返回的确认序号也都是 1001,就好像在提醒发送端“我要的是1001”一样。
这样,作为发送端的 A,所看到的情况就是连续收到了好几个确认序号为1001的ACK。当连续三次收到确认序号相同的 ACK时,就会重新发送对应数据。
当 1001~2000 的数据重传过来后,由于之前的 2001~7000的数据都以成功接收,1001~2000 相当于补全了之前的空缺。此时对于接收端而言,就意味着1~7000 的数据都收到了,于是接下来返回的 ACK中的确认序号就为 7001 了。
上述操作行云流水,快速地定位到了是哪个数据包发生了丢包,并针对性地重传,其它顺利到达的数据会在接收端的缓冲区中等待,无序重传。这种机制就被称之为 "快速重传"(也叫 "⾼速重发控制")
注意:
我们之前介绍过 确认应答/超时重传 机制,与之对应的就是刚刚介绍的 滑动窗口/快速重传 机制。事实上,这两者是并不冲突的。有着各自适合的应用场景:
- 单位时间内发送的数据量比较少:采用 确认应答/超时重传 机制
- 单位时间内发送的数据量比较多:采用 滑动窗口/快速重传 机制
2、流量控制
我们在上文也有提到过一点,虽然窗口越大,传输的效率越高,但窗口也不可能无限大下去。发送方一个劲儿的发是没有意义的,得确保接收方能处理的过来。
其实,在接收端就相当于是一个生产者消费者模型,一边接收着发送端发来的数据作为生产者,一边应用程序又会调用read 等方法读取数据作为消费者,中间通过一个接收缓冲区(相当于是阻塞队列)来协调:
如果发送端发送的太快,就会导致接收端的缓冲区被快速填满,此时如果发送端继续发送,就会发生丢包。接着又会引发丢包重传等一系列补救措施,反而得不偿失了。
因此TCP⽀持根据接收端的处理能⼒, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)
那么具体该如何协调呢?
其实仔细想想,会发现这个模型与我们小学时都做过的一道应用题,游泳池一边放水,一边接水十分相似。放水就相当于是消费者消费数据,接收就相当于是生产者生产数据。我们可以通过“定量”的方式,来实现制约:
可以通过“水位”来表示接收缓冲区中有多少数据。通过空闲空间的变化来控制合理的窗口大小:
- 如果空闲空间越大,就可以认为是应用程序处理速度比较快,就可以让发送方发的更快一些,即设定一个更大的窗口大小
- 如果空闲空间越小,就可以认为是应用程序处理速度比较慢,就可以让发送方发的更慢一些,即设定一个更小的窗口大小
在TCP中,接收方收到数据时,就会把接收缓冲区剩余空间大小通过 ACK 报文返回给发送端。接着发送端就可以根据这个数据来设置合适的窗口大小了。
注意:
这里记录窗口大小的数据位只有16位,,但并不意为着发送端窗口大小最大只有64KB。
在tcp报头中的选项中,可以设置一个特殊的选项 “窗口扩展因子M” 。实际窗⼝⼤⼩是 窗⼝字段的值左移 M 位。注意每左移一位,就相当于原数据*2,这么一调整,窗口大小就十分可观了。
3、拥塞控制
这个机制的主要目的与上文介绍的 流量控制 是类似的。
- 流量控制是站在接收方的视角来限制发送方的速度
- 拥塞控制是站在传输链路层视角来限制发送方的速度的
试想以下假如接收端处理的速度非常快,此时发送端的发送速度难道也能无限大下去吗?
显然不能。因为在传输过程中的结点设备可能会承受不住。如果发送端只是根据接收端的处理速度来设定窗口大小,其发送量很可能会超过一些中间节点的承受能力,导致该节点负载过高而可能会发生丢包问题。
不过这好像也不太好把控。毕竟接收方的需求是比较好确定的,但传输链路层就非常复杂了。因为每次传输经过的结点数非常多,并且每次传输走的线路可能都是不同的,其拥塞状况可能也是截然不同的。
于是,拥塞调整并不是“一锤子买卖”,而是引入了慢启动机制,先发少量的数据探路,再根据每次传输反馈进行动态调整发送速度:
1、先按照一个比较小的速度发送数据
2、数据非常畅通,没有丢包,说明此时网络上传输数据整体上是比较流畅的,可以加快传输速度
3、增大到一定速度后,出现了丢包,说明网络上可能存在拥堵了,就减慢传输数据的速度
4、减速之后,发现又不丢包了,就继续加速
5、加速之后又发现丢包了,就继续减速
就这样循环往复,使传输速度动态变化,以适应瞬息万变的网络环境
此时引入一个概念“拥塞窗口”,来描述拥塞控制下的窗口大小变化。我们接着看在实际传输中,拥塞窗口的变化过程吧:
1、刚开始传输的时候,拥塞窗口非常小,用一个很小的速度来发送数据
一般定义初始拥塞窗口大小为1。相当于在试探当前网络环境拥塞程度
2、每次收到一个ACK应答,拥塞窗口大小加一
此时的增长速度是指数级的,是非常快的
在实际发送数据包时,发送端会将拥塞窗口与接收端反馈的窗口大小进行比较,取较小的作为实际发送窗口
3、当增长速度达到一定阈值时,会停止指数增长,变成线性增长
一定程度上减缓增长速度,避免过快的增长速度对传输链路造成压力,不至于过快让传输结点负载过高,出现丢包问题
这里的初始阈值一般为 TCP 开始慢启动是窗口的最大值
4、线性增长,也会使发送速度越来越快。当速度快到一定量时,就难免会出现丢包,触发重传
一旦出现丢包,慢启动阈值就会变成原来的一半,并减小拥塞窗口大小。有两种减小方式:
- 经典方案:回归慢启动开始时非常小的初始值,接着重复之前的增长流程
- 现在方案:回归到新的阈值上,接着继续线性增长。重复此步骤,不会再进行一开始的指数增长了
总结
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊