滑动窗口:TCP是如何进行流量控制和拥塞控制的?

789 阅读14分钟

TCP 协议

TCP(传输控制协议)和UDP(用户数据报协议)都是互联网协议族中的传输层协议,它们在数据传输的可靠性、效率、连接方式等方面存在差异,适用于不同的应用场景。

连接方式:TCP是面向连接的协议。在数据传输之前,需要在发送端和接收端之间建立一条连接,就像打电话一样,需要先拨通对方号码,建立起连接后才能进行通话。

可靠性:具有高度的可靠性。它通过确认机制、重传机制来确保数据的准确传输。例如,发送端发送数据后,接收端会返回确认信息,如果发送端没有收到确认,就会重新发送数据。同时,TCP还能对数据进行排序,保证接收端按正确的顺序接收数据。

流量控制与拥塞控制:TCP具备流量控制和拥塞控制机制。流量控制可以防止发送方发送数据过快,导致接收方处理不过来而丢失数据。拥塞控制则是当网络出现拥塞时,降低发送方的数据发送速率,以避免网络进一步拥塞。

数据传输效率:由于需要建立连接、进行确认和重传等操作,TCP的传输效率相对较低,尤其在传输少量数据时,额外的开销可能较为明显。

UDP协议

连接方式:UDP是无连接的协议。它就像写信,不需要事先通知对方,直接将信件(数据)发送出去即可,发送方和接收方之间没有建立专门的连接。

可靠性:UDP不保证数据传输的可靠性。它不会对数据进行确认、重传,也不负责数据的排序。数据可能会丢失、重复或乱序到达接收端,但在某些对实时性要求高的场景中,少量的数据丢失或错误是可以接受的。

流量控制与拥塞控制:UDP没有内置的流量控制和拥塞控制机制,需要应用层自己来实现相关功能,如果应用层没有进行相应处理,可能会导致网络拥塞或数据丢失。

数据传输效率:UDP的传输效率较高,因为它不需要建立连接和进行复杂的确认等操作,额外开销小,适合传输实时性要求高的数据。

两者的异同

相同点:都是传输层协议,都为应用层提供服务,都可以实现主机之间的数据传输。

不同点 :

连接性:TCP面向连接,UDP无连接。

可靠性:TCP可靠,UDP不可靠。

有序性:TCP保证数据有序,UDP不保证。

传输效率:TCP效率相对低,UDP效率高。

首部开销:TCP首部开销大,通常为20字节,UDP首部开销小,只有8字节。

TCP的应用场景

文件传输:如FTP(文件传输协议),需要确保文件完整、准确地传输,TCP的可靠性可以保证文件在传输过程中不出现错误或丢失。

电子邮件:SMTP(简单邮件传输协议)等邮件协议使用TCP,以确保邮件的内容和附件能够准确无误地到达收件人的邮箱。

网页浏览:HTTP(超文本传输协议)基于TCP,保证网页的各种资源,如HTML代码、图片、脚本等能够完整、有序地传输到浏览器,使得网页能够正确显示。

UDP的应用场景

实时视频和音频流:如视频会议、在线直播、IP电话等,这些应用对实时性要求极高,允许一定程度的数据丢失,UDP的低延迟和高效率能够保证音视频的流畅播放,少量数据丢失可能只会导致短暂的画面卡顿或声音中断,不会影响整体体验。

游戏:游戏中的实时数据,如玩家的位置、动作、游戏场景的更新等,需要快速传输,UDP能够满足游戏对实时性的要求,即使部分数据丢失,也可以通过游戏的补偿机制来尽量减少对游戏体验的影响。

DNS(域名系统):DNS用于将域名转换为IP地址,它通常只需要传输少量的数据,并且对响应速度要求较高,UDP能够快速地完成域名查询和响应,提高域名解析的效率。

TCP 中包的发送

在复杂网络环境下,TCP 为了能保证每个包真的送达了,并且接收端收到包的顺序和发送端是一致的,每发出一个包,需要一个类似回信的机制。

tcp_1.jpg

这个回信就是 ACK 包,每个包发送的时候会有一个序列号,接收端回 ACK 包的时候会把序列号 +1 发送回来,发送端如果没有收到某个包的 ACK 包,会在一段时间之后尝试重新发送,直到收到 ACK 为止。这其实也是在网络和各种分布式系统中能确保消息可达的唯一方式。

那问题来了,为了确保消息保序可达,难道每次发送一个新的包,都等待上一个包的 ACK 回来之后才发送吗?这样一来一回的效率显然是很低的,也就是每经过一个 RTT 的时间,我们只能发送一个包,假设一个 RTT 是 100ms,那在一秒中我们甚至只能发送 10 个包,这完全是不可接受的。其实我们在等待 ACK 的时候没有必要停止后续包的发送,因为网络传输虽然不稳定,但大部分包往往还是可达的,这样我们就可以获得数倍的传输效率提升。如果真的不幸遇到了丢包,接收端 ACK 姗姗来迟的时候,也就告诉了我们某个序列号之前的所有包全部收到,我们再根据一定的策略,尝试重新发送对应丢失的包就可以了。

tcp_2.jpg

所以发送方需要缓存已发出但尚未收到 ACK 的包,接收方收到包但没有被用户进程消费之前也得把收到的包留着。但是,缓存是有大小限制的,程序消费数据和链路传输数据的能力也是有限的,发送端和接受端都需要一种机制来限制可发送或者可接收数据的最大范围。于是,滑动窗口和拥塞窗口应运而生。

这两个算法都是为了防止网络中发送的包太多。不同的是两者的目的,滑动窗口机制,可以用来控制流量,防止接收方处理不过来消息;同样基于窗口机制的拥塞控制算法,则用来处理网络上数据包太多的情况,以避免网络中出现拥塞。

流量控制

这里说流量控制,主要就是为了防止接收方处理数据的速度跟不上发送方,避免随着时间推移,数据自然溢出接收方的缓冲区。虽然协议可以保证发送方没有收到 ACK,最终会重试重新发送,但如果需要大量反复发送冗余的数据,所占用的网络资源就被白白浪费了,在网络资源很紧缺的时候,这也会造成网络环境的恶化。TCP 控制流量的方式也很简单,就是滑动窗口机制。

接收端会建立一个滑动窗口,由接收方向发送方通告,TCP 首部里的 window 字段就是用来表示窗口大小的,窗口表示的就是接收方目前能接收的缓冲区的剩余大小。

tcp_3.jpg

发送方也会根据这个通告窗口的大小建立自己的滑动窗口。为了兼顾效率和可靠性,在发送方,所有未收到 ACK 的消息虽然可以发送,但是在收到 ACK 之前是一定要在缓冲区中保存的。

发送端的窗口

发送窗口根据三个标准来划分:是否发送、是否收到 ACK、是否在接收方通告处理范围内,分成了四个部分。

tcp_4.jpg

  • 第一部分是已经发送且收到 ACK 的部分,这一部分我们知道已经成功发送,所以不需要在缓冲区保留了。
  • 第二部分是已发送但尚未收到 ACK 的部分。
  • 第三部分是还没有发送,但是在接收方通告窗口也就是处理范围内的数据,这块我们也可以称为可用窗口;第二、第三部分一起构成了我们的整个发送窗口。
  • 最后一部分则是我们需要发送,但已经超过接收方通告窗口范围的部分,这一部分在没有收到新的 ACK 之前,发送方是不会发送这些数据的。通过这个限制,发送的数据就一定不会超过接收方的缓冲区了。

但如果发送方一直没有收到 ACK,随着数据不断被发送,很快可用窗口就会被耗尽。在这种情况下,发送方也就不会继续发送数据了,这种发送端可用窗口为零的情况我们也称为“零窗口”。

tcp_4.jpg

正常来说,等接收端处理了一部分数据,又有了新的可用窗口之后,就会再次发送 ACK 报文通告发送端自己有新的可用窗口(因为发送端的可用窗口是受接收端控制的)。

但是,万一要是 ACK 消息在网络传输中正好丢包了,那发送端还能感知到接收端窗口的变化吗?其实是不会的,在这个情况下,接收端就会一直等着发送端发送数据,而发送端也还会以为接收端仍然处于零窗口的状态,这样一直互相等待,就好像进入了死锁状态。解决办法也很简单,我们可以再引入一个零窗口定时器,如果发送端陷入零窗口的状态,就会启动这个定时器,去定时地询问接收端窗口是否可用了

接收端的窗口

相对发送端来说,接收端要简单的多,主要就分为已经接收并确认的数据和未收到但可以接收的数据,这一部分也就是接收窗口;剩下的就是缓冲区放不下的区域,也就是不可接收的区域。

tpc_4.jpg

如果进程读取缓冲区速度有所变化,接收端可能也会改变接收窗口的大小,每次通告给发送端,就可以控制发送端的发送速度了。这就是所谓的滑动窗口,也就是流量控制机制。

而之所以是滑动窗口,也很好理解,随着 ACK 或者进程读取数据,窗口也会顺次往后移动。比如在发送端的窗口中,如果我们在某次通信中收到了一条 ACK 消息,表示 36 之前的消息都已经被收到了,那么整个可用的窗口就会顺次往右移动。

tcp_4.jpg

总的来说,滑动窗口(流量控制机制)解决了发送端消息可能淹没接收端,导致处理跟不上的情况。

流量拥塞

那 TCP 协议又如何解决流量拥塞的情况呢?也就是网络中由于大量包传输,导致吞吐量下降甚至为 0 的情况。这和我们的道路交通很像,当车流越来越大的时候,整体的行车速度可能会不断下降,导致拥堵,最后吞吐量反而不如车少的时候。

a.jpg

在实际网络中,因为大量的包传输,可能导致中间某些节点的缓冲区满载,从而多余的包被丢弃,需要重新发送,情况越发恶化,最差的时候,网络上的包都是重传的包并且反复地丢弃;整个网络传输能力甚至可以降低为 0。

这当然是一个很严重的问题,TCP 协议同样提出了另外一个叫拥塞窗口的机制,很好地解决了这个问题。

拥塞控制

网络中每个节点不会有全局的网络通信情况,唯一能发现的就是自己的部分包丢了,这种时候它就有理由怀疑网络环境劣化,可能产生了拥塞。

TCP 是一个比较无私的协议,在这种情况下,会选择减少自己发送的包。当网络上大部分通信协议传输层都采用的是 TCP 协议时,在出现拥塞的情况下,大部分节点都会不约而同地减少自己传输的包,这样网络拥塞情况就会得到极大的缓解,一直处于比较好的网络状态。

所以我们就需要在发送端定义一个窗口 CWND(congestion window),也就是拥塞窗口;发送端能发送的最多没有收到 ACK 的包,也不会超过拥塞窗口的范围。

引入拥塞控制机制的 TCP 协议,发送端最大的发送范围是拥塞窗口和滑动窗口中小的一个。拥塞窗口会动态地随着网络情况的变化而进行调整,大体上的策略是如果没有出现拥塞,我们扩大窗口大小,否则就减少窗口大小。

具体是如何实现的呢?经典拥塞控制算法主要包括四个部分:

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

首先是慢启动,在不确定拥塞是否会发生的时候,我们不会一上来就发送大量的包,而是会采用倍增的方式缓慢增加窗口的大小,窗口大小从 1 开始尝试,然后尝试 2、4、8、16 等越来越大的窗口。

b.jpg

整个慢启动的过程看起来就像上图这样,指数型的增加拥塞窗口的大小。

这样,倍增的方式窗口就会很快扩大;我们会在窗口大到一定程度时,减慢增加的速度,转成线性扩大窗口的方式,也就是每次收到新的 ACK 没有丢包的话只比上次窗口增大 1。整个过程看起来就像这样:

c.jpg

慢启动阶段和拥塞避免阶段的分界点,我们就叫“慢启动门限(ssthresh)”。

随着窗口进一步缓慢增加,终于有一天,网络还是遇到了丢包的情况,我们就会假定这是拥塞造成的。

这个时候我们一方面会进行超时重传或者快速重传,另一方面也会把窗口调整到更小的范围。

  • 超时重传,往往意味着拥塞情况更严重,我们的策略也会更激进一些,会直接将 ssthresh 设置为重传发生时窗口大小的一半,而窗口大小直接重置为 0,再进入慢启动阶段。像这样:

d.jpg

  • 快速重传,如果我们连续 3 次收到同样序号的 ACK,包还能回传,说明这个时候可能只是碰到了部分丢包,网络阻塞还没有很严重,我们就会采用柔和一点的策略,也就是快速恢复策略。

e.jpg

我们会先把拥塞窗口变成原来的一半,ssthresh 也就设置成当前的窗口大小,然后开始执行拥塞避免算法。有些实现也会把拥塞窗口直接设置为 ssthresh+3,本质上区别不大。

总体而言,TCP 就是通过滑动窗口、拥塞窗口这两个简单的窗口实现了流量控制和拥塞控制。

滑动窗口由接收端控制,向发送端通告,这样就可以保证发送端发出的包数量上限是明确的,也就不会存在淹没接收端导致来不及处理的情况。

拥塞窗口由发送端控制,它会根据网络中的情况动态的调整,通过慢启动、拥塞避免、拥塞发生、快速恢复四个算法,很好地调整窗口的大小。和滑动窗口一起限制了发送端最大的发送范围,从而保证了拥塞在网络上不会发生。