
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》 《Linux系统从入门到实践》《Linux网络从入门到实践》 《Qt 方寸极境》 《MySQL》
✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游
目录
前言
一、滑动窗口机制:TCP 性能的基石
1.1 引出问题:为什么要有滑动窗口?
1.2 滑动窗口的工作原理
1.2.1 窗口大小协商
1.2.2 滑动原理
1.3 滑动窗口的核心细节
1.3.1 窗口大小的决定因素
1.3.2 窗口的动态变化
1.4 丢包处理:快重传与超时重传
1.4.1 ACK 丢失
1.4.2 数据包丢失
1.5 滑动窗口常见核心问题分析
问题 1:滑动窗口可以向左滑动吗?
问题 2:窗口有哪几种变化状态?
问题 3:如果丢包了,滑动窗口会不会跳过报文直接应答?(重点问题)
问题 4:窗口一直向右滑动,会不会溢出缓冲区?
二、TCP 流量控制机制
2.1 流量控制基本原理
2.2 零窗口处理(窗口探测与窗口更新通知)
2.3 窗口扩大因子:突破 64KB 上限
结束语
前言
在上一篇文章中,我们深入学习了 TCP 连接的建立与断开,吃透三次握手、四次挥手、TIME_WAIT、CLOSE_WAIT 等连接状态,并且从内核视角认识 TCP 连接底层的数据结构。连接建立完成之后,TCP 就开始传输业务数据,而保障数据可靠、高效传输的核心就是滑动窗口。
本篇我们就围绕滑动窗口展开学习,从滑动窗口的由来、工作原理入手,分析窗口动态变化、丢包场景下的重传逻辑,解答滑动窗口相关高频疑问。在此基础上,我们进一步引出流量控制机制 —— 滑动窗口正是流量控制的实现载体,用来限制发送方速率,保护接收端缓冲区。同时讲解零窗口探测、窗口扩大因子等配套机制,帮你完整理解 TCP 如何做到既可靠又高效地传输数据。
一、滑动窗口机制:TCP 性能的基石
1.1 引出问题:为什么要有滑动窗口?
在介绍滑动窗口之前,我们先回顾基础的确认应答机制:发送方每发送一个数据段,就必须等待接收方返回 ACK 确认,收到确认之后,才能继续发送下一段数据。这种一问一答的停等方案逻辑简单,可以保障可靠传输,但传输性能存在明显短板,当网络往返时间 RTT 较大时,吞吐效率会变得很低。
举个例子:假设 RTT 为 100ms,单个数据段大小 1KB,采用停等模式,每秒最多只能发送 10 个报文,速率上限仅有 10KB/s,无法发挥现代网络的带宽能力。

滑动窗口的核心思路,就是允许发送方一次性连续发送多个报文,将多段数据的等待确认时间重叠起来,充分利用网络链路,提升传输吞吐量。

这种批量化的一组发送多个报文,批量化的接收对应的多个应答的实现方案,固然比起上面基础的确认应答机制发一个就必须等待接收一个才能继续发,在效率上要高的多,但是有优点就必然会带来一些需要解决的新问题。
1.2 滑动窗口的工作原理
1.2.1 窗口大小协商
TCP 连接建立的三次握手阶段,双方会在 SYN 报文中交换各自的初始窗口大小。因此,当第一次发送应用数据时,发送方已经知道了接收方的初始接收能力,不需要再进行额外的探测。
1.2.2 滑动原理
滑动窗口本质是发送缓冲区里一块可以动态调整的数据区间,落在窗口范围内的数据,无需等待上一段报文的 ACK 确认,就可以持续发送。 窗口大小:代表当前无需等待确认应答,能够继续发送的最大字节数量。
我们可以把发送缓冲区看作一段线性数组,依靠win_left和win_right两个边界指针划定窗口范围:
- win_left:窗口左边界,严格等于接收方返回的最新确认序号 (指向已发送但还未收到确认的第一个字节序号)。确认序号的含义是 “该序号之前的所有字节都已经被成功接收”。
- win_right:窗口右边界,等于win_left + 当前窗口大小。指向允许发送的最后一个字节的下一个序号。
滑动窗口将发送缓冲区划分成三块区域:
窗口滑动逻辑:当发送方收到一个确认序号为 2001 的 ACK 时,win_left会立即移动到 2001 的位置,同时win_right会根据新的窗口大小重新计算。整个窗口向右滑动,将已确认的数据移出窗口,同时将新的待发送数据纳入窗口。


1.3 滑动窗口的核心细节
1.3.1 窗口大小的决定因素
TCP 头部的16位窗口大小字段,由接收方在应答报文中填写,代表接收方当前剩余缓冲区能接收的数据字节上限,也叫接收方通告窗口。
在现阶段的学习中,我们暂时认为:实际发送窗口大小 = 接收方通告窗口。
注意:这个结论是简化模型,存在缺陷。后续学习拥塞控制章节时,会引入拥塞窗口,重新修正发送窗口计算公式。发送窗口最终会取通告窗口与拥塞窗口两者中的较小值。
发送方会严格参照这个窗口值来发送数据,不会一次性发送超出窗口容量的数据,以此实现流量控制,避免发送方发送速率过快,把接收端缓冲区填满造成数据丢失。

1.3.2 窗口的动态变化
窗口大小不是固定不变,会随接收缓冲区状态动态改变:
- 接收端应用读取数据速度快:接收缓冲区剩余空间增多,ACK Win 变大,滑动窗口随之扩大
- 接收端应用读取速度慢:接收缓冲区逐步填满,ACK Win 减小,滑动窗口收缩
- 接收缓冲区完全被占满:ACK Win 置 0,发送方暂停发送数据,后续会定期发送窗口探测报文,等待接收方更新窗口
1.4 丢包处理:快重传与超时重传
丢包处理:快重传与超时重传
1.4.1 ACK 丢失
数据包已经送达接收端,但是 ACK 确认报文在传输途中丢失。TCP 具备累积确认的特性,后续到达的 ACK 可以一次性确认前面所有字节。 确认序号的含义是 “该序号之前的所有字节都已经被成功接收”。
举例:发送方依次发送 1~1000、1001~2000、2001~3000 三段报文,前两段对应的 ACK 丢失,只要收到确认序号 3001 的 ACK,发送方就知道前面全部数据都已经接收成功。

1.4.2 数据包丢失
报文本身在网络传输中丢失,会触发快重传机制:
快重传可以大幅缩短丢包之后的等待时间,保障传输性能; 超时重传作为兜底方案,在无法触发快重传的场景下(例如丢失的是窗口末尾报文,并且没有后续报文产生三次重复 ACK),保障数据最终可靠交付。 快重传和超时重传互为补充:快重传保障传输性能上限,超时重传守住可靠性底线。

1.5 滑动窗口常见核心问题分析
问题 1:滑动窗口可以向左滑动吗?
绝对不能。 确认序号的含义是,该序号之前所有字节都已经被接收。确认一旦生效,就是不可回退的历史记录。win_left只能单调递增,窗口只能整体向右移动,不能向左回退。 窗口左移意味着已经确认的数据需要重新判定为未接收,违背 TCP 可靠传输的设计。

问题 2:窗口有哪几种变化状态?
窗口一共有四类变化场景:

问题 3:如果丢包了,滑动窗口会不会跳过报文直接应答?(重点问题)
核心结论:TCP 确认是累积确认,确认序号永远标记连续收到的最大字节,存在报文缺口时,确认序号不会跳过缺口。 无论丢包发生在窗口最左侧、中间位置,还是窗口最右侧,接收方只会持续返回缺口起始位置对应的 ACK。
- 窗口最左侧报文丢失:接收方反复返回缺口位置 ACK,发送方收到 3 次重复 ACK 触发快重传。重传成功后,窗口左边界一次性跳转到最新确认序号。

- 中间报文丢失:后续到达的报文会暂存在接收缓冲区,但是确认序号不会前进,持续返回缺口位置 ACK,等待缺失报文补齐。

- 窗口最右侧报文丢失:如果后续还有新报文抵达,会产生重复 ACK 触发快重传;如果没有后续报文,无法触发重复 ACK,只能等待超时重传。

我们可以结合三种丢包场景得到统一结论: 无论丢包发生在滑动窗口的左侧、中间还是右侧位置,站在发送方的视角,所有丢包现象最终都会体现为滑动窗口最左侧 Start 指针卡住不再移动。
发送方不需要关心窗口内部具体是哪一个报文丢失,它只需要掌握两点信息:
当重传成功之后,Start 会一次性滑动到最新的累积确认号。

补充:TCP 正是采用了累积确认机制,接收方不会单独确认乱序到达的后面报文,必须等前面缺失的报文补齐,才会回复更大的 ACK,因此窗口不会跳过丢失的报文直接向右滑动。
问题 4:窗口一直向右滑动,会不会溢出缓冲区?
不会。 TCP 序号是 32 位无符号数字,序号空间有限,序号用尽之后会回绕。TCP 将发送缓冲区抽象成环形队列,通过指针与模运算实现循环复用。 缓冲区内存大小固定,已确认数据对应的内存空间释放之后,可以写入新数据,物理内存不会耗尽。 为了解决序号回绕带来的新旧报文混淆问题,TCP 引入 PAWS 机制,防止序号回绕之后,旧报文干扰当前连接。

二、TCP 流量控制机制
滑动窗口本身不只是用于确认和重传,滑动窗口就是 TCP 流量控制的具体实现方案。流量控制的目标,是防止发送方发送速率超过接收方的处理能力,避免接收缓冲区溢出而产生丢包。
2.1 流量控制基本原理
接收方每次回复 ACK 报文时,会在 TCP 头部的窗口大小字段填入自身接收缓冲区剩余空间,把自己当前能接收数据的上限告知发送方。发送方依据这个通告窗口的值,调整自身发送速度。
- 接收方缓冲区剩余空间充足,通告窗口数值大,发送方就可以持续发送更多数据;
- 接收方缓冲区快要占满,就填入更小的窗口值,通知发送方降低发送速率;
- 接收方缓冲区完全满了,就将窗口大小置 0,通知发送方暂停发送数据。
现阶段简化理解:发送窗口 = 接收方通告窗口。 这个模型存在局限,后续学习拥塞控制时会补充拥塞窗口,修正公式:发送方可发送未确认数据量 = min (接收通告窗口,拥塞窗口)

2.2 零窗口处理(窗口探测与窗口更新通知)
当接收方B的接收缓冲区满了,它会在ACK中把16位窗口大小设为0,告知发送方A:"我收不下了,别再发了!” 这里存在一个问题:A的数据发送被暂停了,而B又没法主动联系A,那A怎么知道B的缓冲区之后的状态呢?这会形成一个死锁风险。TCP 采用两套机制共同规避该问题:


- 窗口更新通知:接收方缓冲区空闲后,主动向发送方发送报文,携带新的窗口大小;

- 窗口探测:发送方会周期性发送仅 1 字节数据的窗口探测报文,接收方必须回复 ACK,并携带当前最新窗口大小。 两个机制并行工作,哪个报文先到达,发送方就按照该报文携带的窗口值处理。

2.3 窗口扩大因子:突破 64KB 上限
TCP 头部窗口大小字段只有 16bit,原生最大只能描述 65535 字节,也就是 64KB。 在如今的高速网络场景下,这个上限会严重限制传输吞吐量。 TCP 引入窗口扩大因子(Window Scale Factor)选项,该选项在三次握手的 SYN 报文中协商。 实际窗口大小计算公式: 实际窗口大小 = 窗口字段值 << 窗口扩大因子M
举例:窗口字段值 3000,窗口扩大因子 M=2,实际窗口 = 3000 << 2 = 12000 字节。 窗口扩大因子最大值为 14,此时 TCP 最大窗口可以达到 65535 × 2¹⁴ = 1GB。

结束语
本篇我们围绕 TCP 如何做到既可靠又高效地传输数据,完整梳理了滑动窗口机制。从停等确认的低效引出窗口并发的设计思路,讲清窗口的动态滑动逻辑、缓冲区环形抽象与丢包时的快重传、超时重传互补机制,也解答了窗口能否左移、是否会溢出、丢包是否跳过应答等高频疑问。
在此基础上,我们认识到滑动窗口正是流量控制的具体实现载体。通过接收方通告窗口反馈缓冲区剩余空间,发送方据此调整速率,避免接收端缓冲区溢出。同时补充了零窗口下的窗口探测与窗口更新机制,以及窗口扩大因子如何突破 64KB 的窗口上限限制。
值得说明的是,本篇文章采用的是简化模型,暂将发送窗口等同于接收方通告窗口。真实网络中,发送方还要感知链路拥塞状况,这部分我们会在下一篇文章的拥塞控制机制中补充修正。





