一文了解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滑动窗口原理_TCP/IP_06

接收窗口有三种:

  • 1、接收并且已经向发送端发送确认ACK
  • 2、尚未接收但允发送端发送数据
  • 3、尚未接收且不允许发送端发送数据

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

一文了解TCP滑动窗口原理_窗口大小_07

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

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

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

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

示例(大小不变)

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

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

一文了解TCP滑动窗口原理_TCP/IP_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 个字节。
  • 然后它发送一个带有 ACK 的 50 字节回复。 这 50 个字节已发送,但尚未发送 ACK,因此 SND.NXT 向右移动 50 个字节。
  • SND.UNA不动。

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

一文了解TCP滑动窗口原理_窗口大小_13

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

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

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

一文了解TCP滑动窗口原理_服务器_14

再次移动到服务器端。

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

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

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

一文了解TCP滑动窗口原理_服务器_15

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

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

可用窗口大小仍为300。

一文了解TCP滑动窗口原理_服务器_16

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

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

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

一文了解TCP滑动窗口原理_服务器_17

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

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

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

一文了解TCP滑动窗口原理_客户端_18

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

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

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

一文了解TCP滑动窗口原理_TCP/IP_19

继续转到客户端。

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

可用窗口大小保持不变。

一文了解TCP滑动窗口原理_窗口大小_20

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

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

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

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

示例(大小变化的窗口)

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

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

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

一文了解TCP滑动窗口原理_客户端_21

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

一文了解TCP滑动窗口原理_TCP/IP_22

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

一文了解TCP滑动窗口原理_窗口大小_23

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

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

发送窗口保持在300字节。

一文了解TCP滑动窗口原理_客户端_24

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

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

一文了解TCP滑动窗口原理_服务器_25

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

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

一文了解TCP滑动窗口原理_TCP/IP_26

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

一文了解TCP滑动窗口原理_客户端_27

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

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

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

一文了解TCP滑动窗口原理_TCP/IP_28

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

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

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

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

结语

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

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

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

更多文章,请关注公众号:高性能架构探索

一文了解TCP滑动窗口原理_客户端_29