“滑动窗口”被誉为如果你不能理解这个知识就不能理解TCP,是TCP实现流量控制的一个重要组成部分,所以在介绍了最简单的窗口概念之后,应该来看看这个设计的思路以及其可能导致的问题,如果突然看到窗口这两个字还没有概念,可以看看上一篇文章的第二部分。

滑动窗口

既然说到滑动窗口,那么当然是最先要做的就是介绍什么是滑动窗口,前面已经介绍了发送窗口的概念,滑动窗口在普通的发送窗口上又多了一层细节,没错,我又画了一张图。图中被乱七八糟的颜色分成了四个区域,下面来详细的讲述这幅图。

 TCP中的智慧 — 滑动窗口  - 图1

首先要明确的是,由于TCP在发送数据的过程中,两端都是对等,并不存在一方是发送端,另一端就是接收端,所以上面画的两个buffer在两边都同时存在但是互补干扰。好了,在明确前提的情况下就可以说说这两条五彩斑斓的线到底是啥了,假设在某一时刻是上面的向下面的发送消息,那么发送端就存在有一个发送窗口,而在接收端,存在的称之为接收窗口。根据TCP中每个包的状态,每个窗口大体分为以下几个区域用以区分在发送和接收的某个时刻,缓冲区的各种包的状态。

发送窗口:

绿色的表示已发送并且已经得到确认的包,也就是1-5的包都收到对端发送的1-5号应答包。

黄色的表示已经发出去但是还没有收到应答的包,6-8表示在发送端这些包已经发送出去了,但是还没有收到相应的回复。

蓝色的表示还未发送但是允许一次性发送但是还没有发送的包,也就是说在这个时刻6-15是允许一次性可以发送的最大的包。

红色的表示在这个时刻不允许发出的包。

接收窗口:

绿色的表示已经发送出去的应答包,综合发送端的快照,这里说明6和7号包在路上但是还没有到达对方。

紫色的表示这个包已经发送出去的,但是丢失了(当然,接收端是不知道它丢失了),这里只是为了说明有这么个状态。

蓝色的表示允许接收的包的范围。

红色的表示不允许接收的包的范围。

在明确这一切之后,假设在某一时刻,系统中TCP的发送窗口和接收窗口的快照如图。下面再想象一下更下一步的状态,假设6,7号确认包到达了对端,那么发送端发送窗口会发生如下的事情:

6,7号包变成绿色,蓝色的范围向后扩大两个包到17号包,这样看起来就像绿色窗口和蓝色窗口同时向右"滑动"了一般,这就是滑动窗口的来历。

那么就自然的产生了下一个问题,这两边的窗口怎么同步?因为发送端如何知道接收端可接收的窗口大小?如果你对TCP包头还有印象,那么就会发现里面有个字段叫做窗口大小,下面就是这个字段走上舞台的时刻了。

我会告诉你我的能耐

滑动窗口这个概念的发明不是仅仅为了便于管理,他还有更好的作用就是实现流量控制,接收端通过告知发送端自己的接收能力避免发送端一次性发送过多的包到对面,这样只会造成大量的重传,从而进一步加重网络负担,雪上加霜这个成语可以完美的解释这一现象。这个过程用一张图就可以清晰的表示出来:

 TCP中的智慧 — 滑动窗口  - 图2

假设每次传输的时候都是传输100包的数据,rwnd表示接收端可接受的窗口大小,就是上一节所描述的接收端的蓝色部分。假设在最开始的时候接受窗口是500个包长,那么当其接收到200个包后,在回复的TCP包头中,接收端会填上已经接收到的最后一个包的序号加上我还能接收多少数据包的窗口大小,这样一个智慧的发送端就不会发送比能接受的数据包还要多的数据包。于是就完成了两边同步的过程。

如果发生了丢失会怎么样呢?比如说301到400的数据包在传输中丢失了,那么按照前面一篇已经学习过的逻辑,对端只能确认到最先发生丢失的包,所以虽然401到500的包已经到达对端,但是在重传计时器到期之后,发送端还是得传输301到500的所有的包。这里主要需要注意的是,每次传输完毕,接收端都会相应的更新自己尚能接收的接收窗口大小,发送端是不可以一次性发送比接收窗口更大的数据包的。而在接收窗口空间全部使用完毕的时候,接收窗口会告知对方,自己的接收窗口为0,这个0意味着作为发送端的对方不能再发送任何数据了。

这里自然的会出现一个问题,既然接收端通知发送端窗口大小为0以使得发送端无法发送数据了,那么什么时候发送端才能再一次的发送数据呢?总不能一直这样僵持下去吧,发送端会发出包含很小字节的探测包(通常是一个字节),如果对端此时回复的接收窗口大小大于0了,那么很显然就可以正常的发包了,如果还是0,一般发送端会尝试三次,如果三次还不行,一般发送端就会发送RST包终结这个链接了。

糊涂窗口综合症

滑动窗口会产生一个十分神奇的现象,而计算机科学家们对于这种现象也起了一个神奇的名字,叫做滑动窗口综合症,不要以为这个是神奇的名字,其英文名字Silly Window syndrome,基本就是直译。

那么什么叫糊涂窗口综合症呢?想象一下,按照前面已经介绍过的知识,想象一下如果接收端只能非常缓慢的消耗数据,那么接收端发出来的接收窗口就会越来越小,甚至出现窗口大小都不够发送一个TCP包头的,使得滑动窗口每次只能滑动很小的范围并且几乎没有办法增大,直到收缩到“愚蠢”的大小。除此之外,因为处理每一个包也需要一定的工作量,随着滞留未处理的包越来越大,就很容易导致“抖动”。同样的问题也会出现在发送端,如果发送端创建数据的过程很慢,那么就会导致滑动窗口退化成为最原始最消耗资源的停等模式了,同样会导致一种“愚蠢”的现象。

那么怎么解决这些问题呢?这里就要提到两个在TCP领域伟大的算法了,第一个叫做纳格算法,用来解决由发送端产生的糊涂窗口综合症,第二个叫做David D Clack算法,用来解决由接收端产生的糊涂窗口综合症问题。

那么首先看看纳格算法,纳格算法如果用一句话描述就是,如果目前发送出去的包还没有收到确认,那么发送端就持续缓存新产生的包,当累计到一定的数量时候再一次性送出。纳格算法非常简单,但是同时非常有效,如果具体的来说其过程就是这样

  • 如果是第一块数据,那么直接发送出这个数据,哪怕只有一个字节。
  • 在这块数据之后,发送端TCP缓存数据并且开始累积,有个通俗的词语可以描述这一过程,叫做hold住,hold到如果收到了对端的确认或者攒到了TCP最大报文段大小(MSS),也就是TCP中允许一次发送的最大数据包大小。这个时候再发出报文。
    然后一直重复步骤2,就是整个的纳格算法。这个算法简单,但是同时考虑了数据产生速率和网络运输速率之间的关系,如果应用程序产生数据的速率比网络传输速率快,那么就会产生大的报文段,反之就使用小报文段,最大程度的减小瓶颈的阻碍。

如果是接收端造成的糊涂窗口综合症,那么David D Clack算法就是解决这个问题的,这个算法是在接收端控制流量,如果接收端的缓慢造成能接收的数据越来越少,那么这个算法就会启用。其算法用一句话描述就是关闭窗口通知,直到接收端消耗掉缓冲区的内容使得窗口达到最大报文段大小的一半再给对端发送窗口大小通知。

可以看到这两种算法都非常简单,但是很有效果,这也是工程中实用算法的特点,最佳的算法都是易于实现,有效而又简洁,和中国武侠小说里,大剑无锋的哲学有某种类似。

关于滑动窗口,我推荐一个非常好的演示网站,可以很直观的了解整个过程:

http://www.ccs-labs.org/teaching/rn/animations/gbn_sr/