You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 20滑动窗口TCP是如何进行流量控制和拥塞控制的
你好,我是微扰君。
过去几讲我们一起讨论了最短路算法在网络中的应用学习了从Dijkstra算法思想发展而来的链路状态选路算法以及从Bellman-Ford算法思想发展而来的距离矢量算法。
链路状态算法的每个节点通过通信都构建了完整的网络拓扑图然后根据Dijkstra算法独立地计算最短路径并依据计算结果维护动态路由表距离矢量算法则是通过节点间的通信了解邻居到每个不同节点的距离以此作为选路依据所以链路上传输的压力比链路状态算法小了很多但也因为没有全局的信息网络出现故障时很容易陷入无穷计算问题。
在计算机网络发展以来,类似单源最短路问题的图论算法应用,除了这两大经典算法,其实还有很多,比如最小生成树问题、网络流问题等等,它们都在不同的场景下发挥着巨大的作用,但我们要知道,图论算法也只是解决了网络传输中和“拓扑结构”相关的一小部分问题。
**这些算法并不足以让我们的数据在环境复杂的网络上稳定传输,也并没有办法去控制流量传输的快慢,来避免接受方对数据处理不过来,或者网络上数据包太多产生拥塞的情况**。为了解决传输本身的问题自然也有一些经典的算法思想和协议被提出来TCP中的滑动窗口、拥塞窗口就是经典的例子。
在学习具体算法之前我们先简单复习一下TCP协议做到了什么。
## TCP协议和UDP协议
TCP作为最常用的两大传输层协议之一无疑是久经生产环境检验的。传输层有两个我们广泛使用的协议UDP协议、TCP协议我们一般会说前者是面向无连接的后者是面向连接的。
这里的“连接”具体是什么意思呢?
简单来说UDP协议是一种没有状态的协议节点之间如果采用UDP协议通信两个节点能做的就是在UDP协议上发送一个个数据包协议本身不会关心这些包之间的关系所以在复杂的网络下包的顺序和可达性都是没有保证的应用层需要自己处理这些包的丢失和乱序问题。
当然UDP协议也因此可以设计的非常轻量所以**在网络传输本来就比较稳定的内网环境下或者对丢包可以容忍但对时延要求较高的场景下UDP协议有许多应用**。
而TCP很不一样它在节点之间建立了真正的连接的概念。相信你肯定听过TCP的三次握手吧UDP就没有这个过程三次握手完成时TCP连接就建立了结束通信的时候通信双方往往也需要主动断开连接。
TCP的传输基于字节流也引入了状态明确记录着每个包是否发送、是否被接受到、包本身的序列号等状态所以**TCP为我们提供了可靠有序的传输能力也被设计的相当复杂**。TCP协议不止考虑了包的可靠传输同时也兼顾了效率更提供了对流量控制和拥塞避免的能力滑动窗口和拥塞窗口就是为了这两个目的而设计的。
那TCP的具体是如何做到让网络中的包传输可靠且效率高的呢
## TCP中包的发送
一个包在网络中发送出去,其实就像古时候你给别人用信鸽寄了一封信一样,在外界环境非常复杂的情况下,你完全没有把握这封信能不能真的送达收信人那边,除非有一天你收到了收信人的回信。
类似的在复杂网络环境下TCP为了能保证每个包真的送达了并且接收端收到包的顺序和发送端是一致的我们每发出一个包自然也需要一个类似回信的机制。
![图片](https://static001.geekbang.org/resource/image/c1/d5/c1c706c86938683bf47e6dfd166848d5.jpg?wh=1498x594)
这个回信也就是ACK包**每个包发送的时候会有一个序列号接收端回ACK包的时候会把序列号+1发送回来**发送端如果没有收到某个包的ACK包会在一段时间之后尝试重新发送直到收到ACK为止。这其实也是在网络和各种分布式系统中能确保消息可达的唯一方式。
那问题来了我们为了确保消息保序可达难道每次发送一个新的包都等待上一个包的ACK回来之后才能发送吗这样一来一回的效率显然是很低的也就是每经过一个RTT的时间我们只能发送一个包假设一个RTT是100ms那在一秒中我们甚至只能发送10个包这完全是不可接受的。
其实我们**在等待ACK的时候没有必要停止后续包的发送因为网络传输虽然不稳定但大部分包往往还是可达的**这样我们就可以获得数倍的传输效率提升。如果真的不幸遇到了丢包接收端ACK姗姗来迟的时候也就告诉了我们某个序列号之前的所有包全部收到我们再根据一定的策略尝试重新发送对应丢失的包就可以了。
![图片](https://static001.geekbang.org/resource/image/yy/02/yydf3be24ebe4e0f99c131affbf3ed02.jpg?wh=1498x871)
所以自然而然的发送方需要缓存已发出但尚未收到ACK的包接收方收到包但没有被用户进程消费之前也得把收到的包留着。
但是,缓存是有大小限制的,程序消费数据和链路传输数据的能力也是有限的,发送端和接受端都需要一种机制来限制可发送或者可接收数据的最大范围。
于是,滑动窗口和拥塞窗口应运而生。
这两个算法核心都是为了防止像网络中发送的包太多。不同的是两者的目的,**滑动窗口机制,可以用来控制流量,防止接收方处理不过来消息;同样基于窗口机制的拥塞控制算法,则用来处理网络上数据包太多的情况,以避免网络中出现拥塞**。
## 流量控制
我们先来看看如何用滑动窗口控制流量。
这里说流量控制,主要就是为了防止接收方处理数据的速度跟不上发送方,避免随着时间推移,数据自然溢出接收方的缓冲区。
虽然协议可以保证发送方没有收到ACK最终会重试重新发送但如果需要大量反复发送冗余的数据所占用的网络资源就被白白浪费了在网络资源很紧缺的时候这也会造成网络环境的恶化。
TCP控制流量的方式也很简单就是滑动窗口机制。
**接收端会建立一个滑动窗口,由接收方向发送方通告**TCP首部里的window字段就是用来表示窗口大小的窗口表示的就是接收方目前能接收的缓冲区的剩余大小。
![图片](https://static001.geekbang.org/resource/image/5c/dc/5c2062c93db157984cf4b3be75c3d7dc.png?wh=438x158)
**但是发送方也会根据这个通告窗口的大小建立自己的滑动窗口**。为了兼顾效率和可靠性在发送方所有未收到ACK的消息虽然可以发送但是在收到ACK之前是一定要在缓冲区中保存的。
我们一起来看一下。
### 发送端的窗口
发送窗口根据三个标准来划分是否发送、是否收到ACK、是否在接收方通告处理范围内分成了四个部分。
![图片](https://static001.geekbang.org/resource/image/63/50/634d8b4f2fa99291ed72bcf80e6bc250.jpg?wh=1920x1129)
* 第一部分就是已经发送且收到ACK的部分这一块我们知道已经成功发送所以不需要在缓冲区保留了。
* 第二部分是已发送但尚未收到ACK的部分。
* 第三部分是还没有发送,但是还在接收方通告窗口也就是处理范围内的数据,这块我们也可以称为可用窗口;第二、第三部分一起构成了我们的整个发送窗口。
* 最后一部分则是我们**需要发送,但已经超过接收方通告窗口范围的部分**这一部分在没有收到新的ACK之前发送方是不会发送这些数据的。通过这个限制发送的数据就一定不会超过接收方的缓冲区了。
但如果发送方一直没有收到ACK随着数据不断被发送很快可用窗口就会被耗尽。在这种情况下发送方也就不会继续发送数据了这种发送端可用窗口为零的情况我们也称为“零窗口”。
![图片](https://static001.geekbang.org/resource/image/82/ba/820e30d38ff913e36610b1a871e55dba.jpg?wh=1920x1129)
正常来说等接收端处理了一部分数据又有了新的可用窗口之后就会再次发送ACK报文通告发送端自己有新的可用窗口因为发送端的可用窗口是受接收端控制的
**但是万一要是ACK消息在网络传输中正好丢包了那发送端还能感知到接收端窗口的变化吗**?其实是不会的,在这个情况下,接收端就会一直等着发送端发送数据,而发送端也还会以为接收端仍然处于零窗口的状态,这样一直互相等待,就好像进入了死锁状态。
解决办法也很简单,我们可以再引入一个零窗口定时器,如果发送端陷入零窗口的状态,就会启动这个定时器,去定时地询问接收端窗口是否可用了,这也是在分布式系统中常见的处理丢包的方式之一。
### 接收端的窗口
相对发送端来说,接收端要简单的多,主要就分为已经接收并确认的数据和未收到但可以接收的数据,这一部分也就是接收窗口;剩下的就是缓冲区放不下的区域,也就是不可接收的区域。
![图片](https://static001.geekbang.org/resource/image/8f/50/8ffceab0920305f11e12fe45c2946250.jpg?wh=1920x1129)
**如果进程读取缓冲区速度有所变化,接收端可能也会改变接收窗口的大小,每次通告给发送端,就可以控制发送端的发送速度了**。这就是所谓的滑动窗口,也就是流量控制机制。
而之所以是滑动窗口也很好理解随着ACK或者进程读取数据窗口也会顺次往后移动。比如在发送端的窗口中如果我们在某次通信中收到了一条ACK消息表示36之前的消息都已经被收到了那么整个可用的窗口就会顺次往右移动。
![图片](https://static001.geekbang.org/resource/image/28/ee/28b5235e4c093b05376bfc1289b0b4ee.jpg?wh=1920x1129)
总的来说,滑动窗口(流量控制机制)解决了发送端消息可能淹没接收端,导致处理跟不上的情况。
## 流量拥塞
那TCP协议如何解决流量拥塞的情况呢也就是网络中由于大量包传输导致吞吐量下降甚至为0的情况。这和我们的道路交通很像当车流越来越大的时候整体的行车速度可能会不断下降导致拥堵最后吞吐量反而不如车少的时候。
![图片](https://static001.geekbang.org/resource/image/cf/6b/cf30f1508bbf10e3a9db838f56df926b.jpg?wh=1920x1145)
在实际网络中因为大量的包传输可能导致中间某些节点的缓冲区满载从而多余的包被丢弃需要重新发送情况越发恶化最差的时候网络上的包都是重传的包并且反复地丢弃整个网络传输能力甚至可以降低为0。
这当然是一个很严重的问题TCP协议同样提出了另外一个叫拥塞窗口的机制很好地解决了这个问题。具体是怎么做的呢我们一起来看一下。
### 拥塞控制
网络中每个节点不会有全局的网络通信情况,唯一能发现的就是自己的部分包丢了,这种时候它就有理由怀疑网络环境劣化,可能产生了拥塞。
TCP是一个比较无私的协议在这种情况下会选择减少自己发送的包。当网络上大部分通信协议传输层都采用的是TCP协议时在出现拥塞的情况下大部分节点都会不约而同地减少自己传输的包这样网络拥塞情况就会得到极大的缓解一直处于比较好的网络状态。
所以我们就需要**在发送端定义一个窗口CWNDcongestion window也就是拥塞窗口发送端能发送的最多没有收到ACK的包也不会超过拥塞窗口的范围。**
引入拥塞控制机制的TCP协议发送端最大的发送范围是拥塞窗口和滑动窗口中小的一个。拥塞窗口会动态地随着网络情况的变化而进行调整大体上的策略是如果没有出现拥塞我们扩大窗口大小否则就减少窗口大小。
具体是如何实现的呢?经典拥塞控制算法主要包括四个部分:
* 慢启动
* 拥塞避免
* 拥塞发生
* 快速恢复
我们一个个来看。首先是慢启动在不确定拥塞是否会发生的时候我们不会一上来就发送大量的包而是会采用倍增的方式缓慢增加窗口的大小窗口大小从1开始尝试然后尝试2、4、8、16等越来越大的窗口。
![图片](https://static001.geekbang.org/resource/image/d5/8a/d5b79c7a1c4eb16be465e9a7e28fea8a.jpg?wh=1920x1145)
整个慢启动的过程看起来就像图中这样,指数型的增加拥塞窗口的大小。
这样倍增的方式窗口就会很快扩大我们会在窗口大到一定程度时减慢增加的速度转成线性扩大窗口的方式也就是每次收到新的ACK没有丢包的话只比上次窗口增大1。整个过程看起来就像这样
![图片](https://static001.geekbang.org/resource/image/fe/7b/fedc491aaa6f9797abcf8231c6ed9f7b.jpg?wh=1920x1145)
这两个慢启动阶段和拥塞避免阶段的分界点我们就叫“慢启动门限ssthresh”。
随着窗口进一步缓慢增加,终于有一天,网络还是遇到了丢包的情况,我们就会假定这是拥塞造成的。
这个时候我们一方面会进行超时重传或者快速重传,另一方面也会把窗口调整到更小的范围。
* 超时重传往往意味着拥塞情况更严重我们的策略也会更激进一些会直接将ssthresh设置为重传发生时窗口大小的一半而窗口大小直接重置为0再进入慢启动阶段。像这样
![图片](https://static001.geekbang.org/resource/image/18/e3/188691ece41fcde20a3f34ed3faed8e3.jpg?wh=1920x1145)
* 快速重传如果我们连续3次收到同样序号的ACK包还能回传说明这个时候可能只是碰到了部分丢包网络阻塞还没有很严重我们就会采用柔和一点的策略也就是快速恢复策略。
![图片](https://static001.geekbang.org/resource/image/06/ec/068652391d574e588465c1a8ce98a1ec.jpg?wh=1920x1145)
我们会先把拥塞窗口变成原来的一半ssthresh也就设置成当前的窗口大小然后开始执行拥塞避免算法。有些实现也会把拥塞窗口直接设置为ssthresh+3本质上区别不大。
# 总结
总体而言TCP就是通过滑动窗口、拥塞窗口这两个简单的窗口实现了流量控制和拥塞控制。
滑动窗口由接收端控制,向发送端通告,这样就可以保证发送端发出的包数量上限是明确的,也就不会存在淹没接收端导致来不及处理的情况。
拥塞窗口由发送端控制,它会根据网络中的情况动态的调整,通过慢启动、拥塞避免、拥塞发生、快速恢复四个算法,就可以很好地调整窗口的大小。和滑动窗口一起限制了发送端最大的发送范围,从而保证了拥塞在网络上不会发生。
### 思考题
最后也给你留一个思考题。前面提到了一个叫快速重传的机制也就是连续3次收到相同的ACK发送端就会意识到丢包的产生我们没有详细讨论你能说说看为什么这样比直接超时重传的方式更好吗它有没有什么其他问题呢会不会有更好的方式解决呢
欢迎你留言与我一起讨论,如果觉得这篇文章对你有帮助的话,也欢迎转发给你的朋友一起学习。我们下节课见~