在我们当初学习网络编程的时候,都接触过TCP,在TCP中,对于数据传输有各种策略,比如滑动窗口、拥塞窗口机制,又比如慢启动、快速恢复、拥塞避免等。通过本文,我们将了解滑动窗口在TCP中是如何使用的。
滑动窗口实现了TCP流控制。首先明确滑动窗口的范畴:
滑动窗口解决的是流量控制的的问题,就是如果接收端和发送端对数据包的处理速度不同,如何让双方达成一致。接收端的缓存传输数据给应用层,但这个过程不一定是即时的,如果发送速度太快,会出现接收端数据overflow,流量控制解决的是这个问题。
上图是发送端滑动窗口的简图。我们可以将数据分为4个部分:
其中第三部分,也就是绿色部分,也称为可用窗口,因为这是发送方可以使用的窗口。
发送窗口由黄色和绿色部分组成。这些字节要么已经发送,要么可以发送。
当发送方发送21-25字节并使用可用窗口中的所有字节时,可用窗口可能为空,发送窗口保持不变(如下图)。
当发送方收到第16-19字节的 ACK 时,发送窗口向右滑动 4 个字节。更新的可用窗口可用于队列中的以下字节(如下图)。
为了便于理解,我们后续将窗口名使用简称,即:
使用简写后,如下图所示:
基于这些定义,我们可以用公式表示可用的窗口大小。
可用窗口(可用窗口)大小 = SND.UNA + SND.WND - SND.NXT
接收窗口有三种:
第二种称为接收窗口,也称为RCV.WND。类似于发送窗口,指针RCV.NXT,代表Receive Next指针,指向接收窗口的第一个字节。
接收窗口不是静态的。如果服务端性能高,读取数据快,接收窗口可能会扩大。否则,它可能会缩小。
接收方通过在TCP段报头中的窗口字段中指示大小来传达其接收窗口。当发送方收到它时,这个窗口大小就成为可用窗口。
发送和接收数据需要时间。因此,接收窗口不等于特定时刻的可用窗口。
下面,为了更好的理解滑动窗口在TCP中的使用,我们将使用一个简单的例子进行模拟说明。
我们模拟一个请求和响应,以更好地理解滑动窗口的工作原理。为了模拟起来简单,我们尽可能的简化里面的过程,比如:
上图示例中,有10个步骤。客户端请求资源,服务器分三段响应:
每一方都可以同时是发送方和接收方。
我们假设客户端的发送窗口 (SND.WND) 是 300 字节,接收窗口 (RCV.WND) 是 150 字节。因此,服务器的 SND.WND 为 150 字节,RCV.WND 为 300 字节。
上图客户端的起始状态。
我们假设它之前已经从服务器接收了300个字节,所以RCV.NXT指向301。由于它还没有发送任何东西,SND.UNA和SND.NXT都指向1。
可用窗口(可用窗口)大小 = SND.UNA + SND.WND - SND.NXT
根据这个公式,客户端的可用窗口大小为 1 + 300 - 1 = 300。
这是服务端的起始状态,镜像另一端即客户端的状态。
因为它已经发送了300个字节,所以SND.UNA和SND.NXT都指向301。
RCV.NXT指向1,因为客户端尚未发送任何请求。服务器的可用窗口是301 + 150 - 301 = 150。
现在,我们从步骤1开始:
客户端发送它的第一个100字节请求。
此刻,窗户发生了变化。
可用窗口更改为 1 + 300 - 101 = 200。
在第 2 步,我们的焦点转移到服务器上,从服务端的角度来分析。
可用窗口大小变为301 + 150 - 351 = 100。
让我们现在继续转向客户端。
可用窗口更改为101 + 300 - 101 = 300。
再次移动到服务器端。
可用窗口为 100 字节。服务器可以发送 80 字节的段。
可用窗口更改为 301 + 150 - 431 = 20。
客户端收到数据的第一部分并立即发送ACK。
可用窗口大小仍为300。
此时,服务器在发送 50 字节的回复时收到了第 2 步的 ACK。
可用窗口大小变为351 + 150 - 431 = 70。
当服务器发送数据1即80字节部分时,再次收到第4步的另一个ACK。
可用窗口大小变为431 + 150 - 431 = 150。
在第 8 步,服务器数据2,大小为100字节。
可用窗口大小变为431 + 150 - 531 = 50。
继续转到客户端。
可用窗口大小保持不变。
最后,服务器收到前一个响应的 ACK。
可用窗口大小变为531 + 150 - 531 = 150。
至此,对于滑动窗口不变的示例,讲解完毕,那么对于滑动窗口大小变化的呢?在TCP中又是如果实现的呢?
在前面的示例中,我们假设发送窗口和接收窗口保持不变。这个假设本身在实际中就是不成立的,因为不存在。
两个窗口中的字节都存在于操作系统缓冲区中,可以对其进行调整。例如,当我们的应用程序没有足够快地从中读取字节时,缓冲区中的可用空间就会缩小。
我们来介绍一下这种情况下的窗口变化,看看它是如何影响可用窗口的。
我们简化了这种情况以将可用窗口集中在客户端上。在这个例子中,客户端始终是发送方,而服务器是接收方。
当服务器发送 ACK 时,它也会在其中包含更新后的窗口大小。
一开始,客户端发送第一个150字节的请求。
发送窗口保持在300字节。
当服务器收到请求时,应用程序读取前 50 个字节,还有 100 个字节仍在缓冲区中,从接收窗口中占用 100 个字节的可用空间。因此,接收窗口缩小到 200 字节。
接下来,服务器发送带有更新的 200 字节接收窗口的 ACK。
客户端收到 ACK 并将其发送窗口大小更新为 200。
此时,可用窗口与发送窗口相同,因为所有 150 个字节都被确认。
客户端再次发送另一个 200 字节的请求,使用可用窗口中的所有可用空间。
服务器接收到 200 字节后,应用程序仍然运行缓慢,总共只读取了 70 字节,并在缓冲区中留下了 280 字节。
这会导致接收窗口再次缩小。现在,我们只剩下 20 个字节了。
在 ACK 消息中,服务器与客户端共享更新的窗口大小。
同样,客户端在收到 ACK 后将其发送窗口更新为 20 字节。可用窗口也变为 20 字节。
在这种情况下,客户端停止发送任何大于 20 字节的请求,直到它收到以下消息中的另一个窗口更新。
如果没有更多来自服务器的消息,我们会被困在 20 字节的可用窗口吗?
我们不会。为了避免这种情况,客户端的 TCP 会定期检测窗口大小。一旦释放更多空间,可用窗口就会扩大,并且可以发送更多数据。
可用窗口的计算是理解TCP滑动窗口的关键。
要学习可用窗口的计算,我们需要了解 3 个指针——SND.UNA、SND.NXT 和 RCV.NXT。
假设一个永不改变的窗口大小可以帮助我们了解进度。
END
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8