TCP之滑动窗口原理_窗口大小

在我们当初学习网络编程的时候,都接触过TCP,在TCP中,对于数据传输有各种策略,比如滑动窗口、拥塞窗口机制,又比如慢启动、快速恢复、拥塞避免等。通过本文,我们将了解滑动窗口在TCP中是如何使用的。

滑动窗口实现了TCP流控制。首先明确滑动窗口的范畴:

  • TCP是双工的协议,会话的双方都可以同时接收和发送数据。
  • 会话的双方都各自维护一个发送窗口和一个接收窗口。各自的接收窗口大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各自的发送窗口则要求取决于对端通告的接收窗口,要求相同。

滑动窗口解决的是流量控制的的问题,就是如果接收端和发送端对数据包的处理速度不同,如何让双方达成一致。接收端的缓存传输数据给应用层,但这个过程不一定是即时的,如果发送速度太快,会出现接收端数据overflow,流量控制解决的是这个问题。

发送端窗口

TCP之滑动窗口原理_客户端_02上图是发送端滑动窗口的简图。我们可以将数据分为4个部分:

  • 发送和已确认的字节(蓝色部分)
  • 已发送但尚未确认的字节(黄色部分)
  • 未发送的字节和接收方准备接收的字节,即在缓冲区buffer中(绿色部分)
  • 未发送且接收方未准备接收的字节,即已经在缓冲区,但是该部分数据还未被处理(灰色部分)

其中第三部分,也就是绿色部分,也称为可用窗口,因为这是发送方可以使用的窗口。

发送窗口由黄色和绿色部分组成。这些字节要么已经发送,要么可以发送。

当发送方发送21-25字节并使用可用窗口中的所有字节时,可用窗口可能为空,发送窗口保持不变(如下图)。

TCP之滑动窗口原理_滑动窗口_03

当发送方收到第16-19字节的 ACK 时,发送窗口向右滑动 4 个字节。更新的可用窗口可用于队列中的以下字节(如下图)。

TCP之滑动窗口原理_滑动窗口_04

为了便于理解,我们后续将窗口名使用简称,即:

  • SND.WND,代表发送窗口
  • SND.UNA, 代表Send Unacknowledged指针,指向发送窗口的第一个字节
  • SND.NXT, 代表Send Next指针,指向可用窗口的第一个字节

使用简写后,如下图所示:

TCP之滑动窗口原理_滑动窗口_05

基于这些定义,我们可以用公式表示可用的窗口大小。

可用窗口(可用窗口)大小 = SND.UNA + SND.WND - SND.NXT

接收端窗口

TCP之滑动窗口原理_客户端_06

接收窗口有三种:

  • 1、接收并且已经向发送端发送确认ACK
  • 2、尚未接收但允发送端发送数据,即可用接收窗口
  • 3、尚未接收且不允许发送端发送数据,即已经在缓冲区中,但还未被处理

第二种称为接收窗口,也称为RCV.WND。类似于发送窗口,指针RCV.NXT,代表Receive Next指针,指向接收窗口的第一个字节。

TCP之滑动窗口原理_窗口大小_07

接收窗口不是静态的。如果服务端性能高,读取数据快,接收窗口可能会扩大。否则,它可能会缩小。

接收方通过在TCP段报头中的窗口字段中指示大小来传达其接收窗口。当发送方收到它时,这个窗口大小就成为可用窗口。

发送和接收数据需要时间。因此,接收窗口不等于特定时刻的可用窗口。

下面,为了更好的理解滑动窗口在TCP中的使用,我们将使用一个简单的例子进行模拟说明。

示例(大小不变)

我们模拟一个请求和响应,以更好地理解滑动窗口的工作原理。为了模拟起来简单,我们尽可能的简化里面的过程,比如:

  • 我们忽略最大段大小 (MSS)。MSS 因选择的网络路由而不同。
  • 使接收窗口等于可用窗口,并且在此过程中两者保持不变。

TCP之滑动窗口原理_客户端_08

上图示例中,有10个步骤。客户端请求资源,服务器分三段响应:

  • 1、一个 50 字节的包头
  • 2、一个 80 字节的数据1
  • 3、一个 100 字节的数据2

每一方都可以同时是发送方和接收方。

我们假设客户端的发送窗口 (SND.WND) 是 300 字节,接收窗口 (RCV.WND) 是 150 字节。因此,服务器的 SND.WND 为 150 字节,RCV.WND 为 300 字节。

TCP之滑动窗口原理_窗口大小_09

上图客户端的起始状态。

我们假设它之前已经从服务器接收了300个字节,所以RCV.NXT指向301。由于它还没有发送任何东西,SND.UNA和SND.NXT都指向1。

可用窗口(可用窗口)大小 = SND.UNA + SND.WND - SND.NXT

根据这个公式,客户端的可用窗口大小为 1 + 300 - 1 = 300。

TCP之滑动窗口原理_窗口大小_10

这是服务端的起始状态,镜像另一端即客户端的状态。

因为它已经发送了300个字节,所以SND.UNA和SND.NXT都指向301。

RCV.NXT指向1,因为客户端尚未发送任何请求。服务器的可用窗口是301 + 150 - 301 = 150。

现在,我们从步骤1开始:

TCP之滑动窗口原理_服务器_11

客户端发送它的第一个100字节请求。

此刻,窗户发生了变化。

  • 这 100 个字节已发送,但尚未收到 ACK。因此,SND.NXT 向右滑动 100 个字节。
  • 其他指针保持不变。

可用窗口更改为 1 + 300 - 101 = 200。

TCP之滑动窗口原理_数据_12

在第 2 步,我们的焦点转移到服务器上,从服务端的角度来分析。

  • 当服务器收到请求时,RCV.NXT 向右滑动 100 个字节。
  • 然后它发送一个对于50字节的ACK回复。这 50 个字节的确认ACK已发送至发送端,即客户端,但尚未收到回复ACK,因此 SND.NXT 向右移动 50 个字节。
  • SND.UNA不动。

可用窗口大小变为301 + 150 - 351 = 100。

TCP之滑动窗口原理_客户端_13TCP之滑动窗口原理_服务器_14

让我们现在继续转向客户端。

  • 当收到50字节的回复时,RCV.NXT向右移动50字节。
  • SND.UNA 在收到前一个发送的 100 个字节的 ACK 时向右滑动。
  • SND.NXT保持不变,因为客户端不发送任何数据。

可用窗口更改为101 + 300 - 101 = 300。

TCP之滑动窗口原理_窗口大小_15

再次移动到服务器端。

可用窗口为 100 字节。服务器可以发送 80 字节的段。

  • SND.NXT 向右滑动 80 个字节。
  • SND.UNA 保持不变,因为上一次的50 字节尚未得到确认。
  • RCV.NXT 保持不变,因为服务器没有收到任何数据。

可用窗口更改为 301 + 150 - 431 = 20。

TCP之滑动窗口原理_窗口大小_16TCP之滑动窗口原理_服务器_17

客户端收到数据的第一部分并立即发送ACK。

  • 当客户端接收到 80 字节的数据时,RCV.NXT 向右移动。
  • 其他部分不变。

可用窗口大小仍为300。

TCP之滑动窗口原理_窗口大小_18

此时,服务器在发送 50 字节的回复时收到了第 2 步的 ACK。

  • SND.UNA 向右移动 50 个字节。
  • 其他部分保持不变。

可用窗口大小变为351 + 150 - 431 = 70。


当服务器发送数据1即80字节部分时,再次收到第4步的另一个ACK。

  • SND.UNA 向右移动 80 个字节。
  • 其他部分保持不变。

可用窗口大小变为431 + 150 - 431 = 150。

TCP之滑动窗口原理_滑动窗口_19

在第 8 步,服务器数据2,大小为100字节。

  • SND.NXT向右移动 100 个字节。
  • 其他部分保持不变。

可用窗口大小变为431 + 150 - 531 = 50。

TCP之滑动窗口原理_滑动窗口_20

继续转到客户端。

  • 当客户端收到 100 字节时,RCV.NXT 向右移动 100 字节。
  • 其他部分保持不变。

可用窗口大小保持不变。

TCP之滑动窗口原理_数据_21

最后,服务器收到前一个响应的 ACK。

  • SND.UNA向右移动100个字节。
  • 其他部分保持不变。

可用窗口大小变为531 + 150 - 531 = 150。

至此,对于滑动窗口不变的示例,讲解完毕,那么对于滑动窗口大小变化的呢?在TCP中又是如果实现的呢?

示例(大小变化的窗口)

在前面的示例中,我们假设发送窗口和接收窗口保持不变。这个假设本身在实际中就是不成立的,因为不存在。

两个窗口中的字节都存在于操作系统缓冲区中,可以对其进行调整。例如,当我们的应用程序没有足够快地从中读取字节时,缓冲区中的可用空间就会缩小。

我们来介绍一下这种情况下的窗口变化,看看它是如何影响可用窗口的。

TCP之滑动窗口原理_服务器_22

我们简化了这种情况以将可用窗口集中在客户端上。在这个例子中,客户端始终是发送方,而服务器是接收方。

TCP之滑动窗口原理_客户端_23

当服务器发送 ACK 时,它也会在其中包含更新后的窗口大小。

TCP之滑动窗口原理_服务器_24

一开始,客户端发送第一个150字节的请求。

  • 这 150 个字节已发送,但尚未发送 ACK。
  • 可用窗口缩小到 150 字节。

发送窗口保持在300字节。

TCP之滑动窗口原理_窗口大小_25

当服务器收到请求时,应用程序读取前 50 个字节,还有 100 个字节仍在缓冲区中,从接收窗口中占用 100 个字节的可用空间。因此,接收窗口缩小到 200 字节。

接下来,服务器发送带有更新的 200 字节接收窗口的 ACK。

TCP之滑动窗口原理_服务器_26

客户端收到 ACK 并将其发送窗口大小更新为 200。

此时,可用窗口与发送窗口相同,因为所有 150 个字节都被确认。

TCP之滑动窗口原理_窗口大小_27

客户端再次发送另一个 200 字节的请求,使用可用窗口中的所有可用空间。

TCP之滑动窗口原理_数据_28

服务器接收到 200 字节后,应用程序仍然运行缓慢,总共只读取了 70 字节,并在缓冲区中留下了 280 字节。

这会导致接收窗口再次缩小。现在,我们只剩下 20 个字节了。

在 ACK 消息中,服务器与客户端共享更新的窗口大小。

TCP之滑动窗口原理_滑动窗口_29

同样,客户端在收到 ACK 后将其发送窗口更新为 20 字节。可用窗口也变为 20 字节。

在这种情况下,客户端停止发送任何大于 20 字节的请求,直到它收到以下消息中的另一个窗口更新。

如果没有更多来自服务器的消息,我们会被困在 20 字节的可用窗口吗?

我们不会。为了避免这种情况,客户端的 TCP 会定期检测窗口大小。一旦释放更多空间,可用窗口就会扩大,并且可以发送更多数据。

结语

可用窗口的计算是理解TCP滑动窗口的关键。

要学习可用窗口的计算,我们需要了解 3 个指针——SND.UNA、SND.NXT 和 RCV.NXT。

假设一个永不改变的窗口大小可以帮助我们了解进度。