TCP滑动窗口详解

这篇文章简洁的写出了TCP滑动窗口的原理,推荐给大家阅读

在 TCP 协议中,滑动窗口机制用于控制数据的传输速率和保证可靠性。滑动窗口包括发送窗口(Send Window)和接收窗口(Receive Window)。我们先从相对复杂的发送窗口说起。

发送窗口

下图是站在发送方视角的一张快照,我们可以将数据分为四类:

  1. 已发送且已被确认的字节(蓝色区域):这些字节已经成功送达接收方,并收到了来自接收方的确认(ACK)。它们在窗口外,不再受窗口限制。
  2. 已发送但尚未被确认的字节(黄色区域):这些数据已经发送出去,但发送方还没有收到接收方的确认。它们仍处于发送窗口之内。
  3. 尚未发送但接收方已经准备好接收的字节(绿色区域):这些字节尚未发送,但根据接收方通告的窗口大小,发送方已经获得“发送许可”。只要发送方愿意,随时可以发出这些数据。
  4. 尚未发送且接收方还未准备好接收的字节(灰色区域):这些数据还不能发送,因为接收方的接收窗口当前不够大,尚未准备好接收这些字节。

第 3 类(绿色部分)也被称为“可用窗口”,因为这是发送方当前可以用来发送数据的区域。

发送窗口由两部分组成:

  1. 已发送但未确认的数据(黄色部分)
  2. 可用窗口(绿色部分)

这意味着,发送窗口涵盖的数据要么已经发送出去,要么可以马上发送。

当发送方将 21–25 字节发送出去后,刚好用完了整个可用窗口,此时可用窗口为空,但由于还没有收到新的确认,发送窗口的大小和位置都暂时保持不变。

直到发送方收到接收方对第 16~19 字节的 ACK 确认后,发送窗口才会向右滑动 4 个字节。这样一来新的可用窗口也就生成了(绿色区域重新出现),供后续等待发送的数据使用。

为了更好地理解本文后面的内容,我们需要先知道以下几个术语的含义:

  • SND.WND:表示发送窗口的大小。
  • SND.UNA:表示发送未确认指针(Send Unacknowledged),它指向发送窗口中第一个尚未被确认的字节。这个位置之前的数据已经被确认,因此可以从缓存中移除。
  • SND.NXT:表示下一个待发送指针(Send Next),指向可用窗口中第一个可以发送的字节。

根据这些定义,我们可以用以下公式来表示“可用窗口”的大小。

接收窗口

接收窗口也可以分为三个部分:

  1. 已经接收并确认的字节:这些数据已经成功接收并处理,不再属于接收窗口的范围。
  2. 尚未接收但允许发送方发送的字节:这是接收方当前有能力接收的数据范围。
  3. 尚未接收且暂时不允许发送方发送的字节:由于接收方当前处理能力有限,这部分数据暂时不能接收,因此也不会授权发送方发送。

其中第 2 类被称为接收窗口,也常用 RCV.WND 来表示。

和发送窗口类似,接收窗口中也有一个指针 RCV.NXT(Receive Next),指向接收窗口中下一个期望接收的字节位置。

需要注意的是,接收窗口的大小不是固定不变的。如果接收方(如服务器)处理数据的速度较快,接收窗口可能会扩大;反之,如果处理较慢,接收窗口可能会收缩。

为了让发送方了解接收能力,接收方会在 TCP 报文头中的 Window 字段中通告自己的接收窗口大小。发送方在收到这个数值后,会将其作为可用窗口大小来决定可以发送多少数据。

不过,由于网络传输存在一定的延迟,发送方所了解的接收窗口大小可能并不完全反映接收方此刻的真实状态。因此,在某些时刻,发送方实际可用的窗口大小与接收方的窗口并不完全一致。

一个简化的示例

为了更好地理解 TCP 滑动窗口的工作原理,我们可以通过模拟一次完整的请求与响应过程来进行说明。

为了简化计算和说明,我们做出以下两个假设:

  1. 忽略最大报文段长度(MSS):在实际情况中,MSS(Maximum Segment Size)会根据网络路径的不同而有所变化,这里我们暂不考虑其影响。
  2. 假设接收窗口始终等于发送方的可用窗口,并且在整个过程中保持不变。

下图展示了这个示例的 10 个步骤。

假设客户端向服务器请求一个资源,服务器将通过 三个报文段来响应该请求:

  • 一个 50 字节的报文头
  • 一个包含 80 字节正文(第 1 部分) 的报文段,
  • 一个包含 100 字节正文(第 2 部分) 的报文段。

在这个过程中,客户端和服务器都会同时扮演“发送方”和“接收方”的角色:客户端发送请求,接收服务器的响应;服务器发送响应,接收客户端的确认。

假设网络连接建立后,双方的窗口大小如下:

  • 客户端的 发送窗口(SND.WND)为 300 字节,即客户端最多可以一次性发送 300 字节的数据;
  • 客户端的 接收窗口(RCV.WND)为 150 字节,即客户端最多可以一次性接收 150 字节的数据。

因此,对应地:

  • 服务器的发送窗口(SND.WND)为 150 字节,因为它必须遵守客户端的接收能力;
  • 服务器的接收窗口(RCV.WND)为 300 字节,因为它可以根据客户端的发送窗口来接收数据。

如下图所示,假设这是客户端的初始状态。

  • 客户端此前已经从服务器接收了 300 字节 的数据,因此其 RCV.NXT = 301,表示下一字节从第 301 字节开始接收。
  • 客户端还未发送任何数据,因此其发送相关的两个指针:
  1. SND.UNA(未确认的第一个字节) = 1
  2. SND.NXT(下一个待发送字节) = 1

根据发送窗口的可用大小计算公式:

客户端的可用窗口大小为:

可用窗口 = SND.UNA + SND.WND - SND.NXT
         = 1 + 300 - 1
         = 300 字节

即客户端当前可以立即发送最多 300 字节 的数据。

再来看服务器的初始状态,它与客户端的状态是对应的。

由于服务器已经发送了 300 字节的数据,所以服务器的 SND.UNA 和 SND.NXT 都指向 301。

而客户端还没有发送任何请求,因此服务器尚未接收到任何数据,此时 RCV.NXT 指向 1,表示服务器期待从第 1 字节开始接收客户端的数据。

根据公式计算,服务器的可用窗口大小为:

可用窗口 = RCV.NXT + RCV.WND - RCV.NXT
         = 301 + 150 - 301
         = 150 字节

因此,服务器当前最多可以接收 150 字节 的客户端数据。

第 1 步

现在开始第 1 步,客户端发送了第一个 100 字节的请求。此时,窗口状态发生了变化。

这 100 字节的数据已经发送出去,但尚未被确认(ACK),因此 SND.NXT 向右滑动了 100 字节。

其他指针保持不变。

此时客户端的可用窗口变为:

可用窗口 = SND.UNA + SND.WND - SND.NXT
         = 1 + 300 - 101
         = 200 字节

第 2 步

第 2 步,我们将关注点转向服务器端。

当服务器收到客户端发来的 100 字节请求后,RCV.NXT 向右滑动 100 字节。

接着,服务器发送一个包含 ACK 的 50 字节响应。由于这 50 字节已经发送但尚未被确认(ACK),因此 SND.NXT 向右滑动了 50 字节。

SND.UNA 保持不变。

此时服务器的可用窗口大小为:

可用窗口 = SND.UNA + SND.WND - SND.NXT
         = 301 + 150 - 351
         = 100 字节

第 3 步

第 3 步,我们将关注点转向客户端。

当客户端接收到服务器发来的 50 字节响应后,它的 RCV.NXT 向右滑动了 50 字节,从 301 变为 351,表示这些数据已被成功接收。

同时,由于收到了前面发送的 100 字节数据的确认 ACK,因此,客户端的 SND.UNA 从 1 向右滑动到 101,表示前 100 字节已被成功确认。

由于客户端没有发送新的数据,因此 SND.NXT 仍保持在 101。

此时客户端的可用窗口变为:

可用窗口 = SND.UNA + SND.WND - SND.NXT
         = 101 + 300 - 101
         = 300 字节

这一步清晰地展示了 TCP 滑动窗口的“滑动”特性:接收 ACK 会推动发送窗口前移,接收数据会推动接收窗口前移,从而维持稳定的数据流动和可靠传输。

第 4 步

第 4 步 接着来看服务器端的情况。

此时服务器的可用窗口为 100 字节,因此它可以发送一个 80 字节的报文段,作为响应正文的第 1 部分。

发送后,由于这 80 字节的数据已发送但尚未被确认(ACK),SND.NXT 向右滑动了 80 字节,从 351 变为 431。

由于之前发送的 50 字节数据还没有被确认,所以 SND.UNA 保持不变,仍为 301。

RCV.NXT 也保持不变,仍指向 101。因为服务器并未接收到新的数据。

此时服务器的可用窗口变为:

可用窗口 = SND.UNA + SND.WND - SND.NXT
         = 301 + 150 - 431
         = 20 字节

这意味着服务器当前还能发送的最大数据量只有 20 字节。

第 5 步

第 5 步,我们将关注点再次转向客户端。

客户端接收到服务器发送的 80 字节的正文第一部分后,RCV.NXT 向右滑动 80 字节,从 301 移动到 381,表示它已经成功接收并处理了这些数据。

接着,客户端立即发送一个 ACK 报文段,告知服务器这部分数据已成功接收。

由于客户端没有发送新的数据,发送窗口相关的指针(SND.UNA 和 SND.NXT)保持不变。

根据窗口大小的计算公式,客户端的可用窗口为:

SND.UNA + SND.WND - SND.NXT
= 101 + 300 - 101
= 300 字节

第 6 步

第 6 步又到了服务器这端了。

此时,服务器收到了客户端对第 2 步(50 字节回复)的 ACK 确认。

这时,服务器的SND.UNA 向右移动了 50 字节,从 301 更新为 351,释放了窗口中的一部分空间。

由于此时服务器没有接收到新数据,因此 RCV.NXT 保持不变。而 SND.NXT 在第 4 步中已滑动到 431,也保持不变。

此时服务器的可用窗口计算如下:

SND.UNA + SND.WND - SND.NXT
= 351 + 150 - 431
= 70 字节

也就是说,服务器现在还可以继续发送 70 字节的数据。

第 7 步

第 7 步继续看服务器这端。

服务器在发送第一个 80 字节的文件数据时,又收到了客户端对第 4 步的 ACK 确认。

这表示客户端已经成功接收了这部分数据。因此,服务器将 SND.UNA 向右滑动了 80 字节,从 351 移动到 431,释放了发送窗口中已确认的数据区域。

此时,SND.NXT 和 RCV.NXT 均保持不变,因为服务器暂时没有发送新的数据,也没有接收到新的请求。

此时的可用窗口变为:

SND.UNA + SND.WND - SND.NXT
= 431 + 150 - 431
= 150 字节

所以,此时服务器的可用窗口为 150 字节,可以发送更多的数据。

第 8 步

第 8 步,服务器发送了文件的第二部分内容,共 100 字节。

由于这部分数据已经发送但尚未被确认,服务器将 SND.NXT 向右滑动了 100 字节,从 431 移动到 531。

SND.UNA 和 RCV.NXT 此时保持不变,因为尚未收到新的 ACK 或数据。

此时的可用窗口变为:

SND.UNA + SND.WND - SND.NXT
= 431 + 150 - 531
= 50 字节

也就是说,在这一轮数据发送之后,服务器还剩下 50 字节 的可用窗口。

第 9 步

第 9 步轮到客户端了。

当客户端收到服务器发送的这 100 字节的数据时,客户端将 RCV.NXT 向右滑动了 100 字节,从 431 移动到 531,表示这些数据已经成功接收并准备交付给上层应用。

由于客户端此时没有发送新的数据,也未收到新的 ACK,SND.UNA 和 SND.NXT 保持不变。

可用窗口大小不变,仍为 300 字节。

第 10 步

最后,我们回到服务器端。

服务器收到了客户端对第 8 步(100 字节文件数据)的 ACK 确认。这意味着客户端已经成功接收了这部分数据。

于是,服务器将 SND.UNA 向右滑动了 100 字节,从 431 移动到 531,释放了这部分数据所占用的发送窗口。

SND.NXT 和 RCV.NXT 保持不变,因为此时服务器既没有发送新数据,也没有接收到新的数据。

可用窗口变为:

可用窗口 = SND.UNA + SND.WND - SND.NXT
         = 531 + 150 - 531
         = 150 字节

因此,此时服务器的可用窗口恢复为 150 字节,为下一轮的数据发送腾出了空间。

当窗口发生变化时

在前面的示例中,我们为了简化分析,假设发送窗口和接收窗口在整个过程中保持不变。但在实际网络通信中,这一假设并不成立。

原因在于,发送窗口和接收窗口的大小取决于操作系统分配的缓冲区空间,而这个缓冲区的大小是动态变化的。

举个例子:如果接收方的应用程序处理数据的速度较慢,导致还没有来得及从接收缓冲区中读取数据,那么缓冲区中尚未被读取的字节就会积压,可用空间随之减少。此时,接收窗口的大小也会随之缩小。

反之,如果应用程序快速处理数据并及时清空缓冲区,窗口则可能扩大。

接下来,我们将为这个案例引入窗口大小动态变化的情况,看看它是如何影响客户端的可用窗口的。

为了简化说明,我们将场景适当简化:只关注客户端的可用窗口变化。在这个例子中,客户端始终作为发送方,而服务器始终作为接收方。

在实际通信过程中,服务器每次发送 ACK 确认时,通常会在 ACK 报文中携带当前的窗口大小信息。客户端在接收到这个 ACK 后,就会更新自己的可用发送窗口大小,从而决定后续能够发送的数据量。

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

由于这 150 字节已经发出但尚未收到服务器的确认(ACK),所以此时客户端的可用发送窗口从原本的 300 字节缩小为 150 字节。需要注意的是,发送窗口的总大小仍然保持在 300 字节不变。

服务器接收到这 150 字节的数据后,其应用程序从中读取了 50 字节,而剩余的 100 字节仍保留在接收缓冲区中,暂时未被处理,因此占用了接收窗口中的 100 字节空间。这意味着服务器的接收窗口被压缩为 200 字节。

随后,服务器向客户端发送了一个 ACK 确认报文,并在报文中附带了最新的接收窗口大小信息(200 字节),以便客户端及时调整其发送策略。

客户端收到服务器发回的 ACK 后,从中提取出最新的接收窗口大小,并将自己的发送窗口更新为 200 字节。

与此同时,之前发送的 150 字节数据已全部被确认,因此客户端的SND.UNA 向前滑动,此时可用窗口恢复为发送窗口的最大值,即 200 字节。

随后,客户端再次发送了一个 200 字节的请求,刚好用尽了当前的可用窗口空间。

服务器收到这 200 字节的数据后,由于应用程序处理数据的速度较慢,仅读取了其中的 70 字节,其余130 字节仍然滞留在接收缓冲区中。再加上前面残留的 150 字节数据,总共有 280 字节未被处理。

这导致服务器的接收缓冲区几乎被占满,接收窗口进一步收缩,仅剩下 20 字节的可用空间。

服务器在发送 ACK 报文时,会将更新后的接收窗口大小(20 字节)一并告知客户端。

客户端收到这个 ACK 后,立即将自身的发送窗口更新为 20 字节,此时可用窗口也变为 20 字节。这意味着,客户端最多只能再发送 20 字节的数据。

在这种情况下,客户端会暂停进一步发送较大数据块,直到后续从服务器收到新的窗口更新信息。

那么问题来了:如果服务器长时间没有发送任何消息,客户端是否就会一直“卡”在这 20 字节的可用窗口上,无法继续发送?答案是不会。

为了避免这种“窗口冻结”问题,TCP 协议设计中引入了窗口探测机制(Window Probing)。客户端的 TCP 实现会定期发送小的“窗口探测报文”,用于确认接收方的窗口是否有变化。

一旦服务器的应用程序释放了缓冲区空间(即处理并清除了部分数据),窗口大小随之扩大,服务器会在 ACK 中将这个更大的窗口值通知客户端。

客户端收到新的窗口信息后,可用窗口随之扩大,便可以继续发送更多的数据,通信也得以顺利进行。

总结

理解 TCP 滑动窗口的关键在于掌握可用窗口(Usable Window)的计算方法。

要弄清楚可用窗口的计算,就必须理解三个关键指针:

  • SND.UNA(Send Unacknowledged):表示已经发送但尚未被确认的第一个字节的序号。它指向的是发送端尚未收到 ACK 的数据的起点。
  • SND.NXT(Send Next):表示下一个准备发送的字节序号,也就是发送端的“前沿”位置。
  • RCV.NXT(Receive Next):表示接收方期望接收到的下一个字节序号,是接收窗口中的起始位置。

发送方的可用窗口(Usable Window),就是在当前窗口大小的限制下,还能继续发送多少数据。

其计算公式通常为:

Usable Window = SND.UNA + 窗口大小 - SND.NXT

这个值反映了:在未收到确认之前,发送端还能发送多少字节的数据。

如果你掌握了这三个指针的含义,配合窗口大小,就能够准确地理解 TCP 滑动窗口机制的动态变化过程,并判断数据何时可以继续发送、何时必须等待接收方的 ACK 或窗口更新。

转自:https://zhuanlan.zhihu.com/p/1919298151182997299

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值