在前面的 TCP 学习中,我们已经知道 TCP 为什么可靠。
发送方把数据发送出去以后,并不能立刻把这份数据从自己的发送缓冲区中删除,因为数据在网络中传输的过程中,可能发生丢包。如果发送方迟迟没有收到确认,就必须依靠这份还保留的数据进行重传。
这就产生了一个问题:
如果每发送一个数据段,都必须等到对方 ACK 到达以后才能继续发送,那么 TCP 的效率会非常低。
例如:
A B
发送数据1 ------------>
<------------ ACK
发送数据2 ------------>
<------------ ACK
发送数据3 ------------>
<------------ ACK
发送数据4 ------------>
<------------ ACK
每发送一次数据,都要等待一次网络往返。
如果 A 和 B 相距很远,网络往返一次需要几十毫秒甚至几百毫秒,那么大量时间都会浪费在"等待 ACK"上。
因此,TCP 实际通信时不会这么低效。
TCP 会允许发送方在没有收到前一个数据段 ACK 的情况下,继续发送后面的数据,一次可以连续发送多个数据段。
但是,发送多少并不是无限的。
一方面,接收方的接收能力有限;另一方面,发送出去但还没有得到确认的数据又必须保存在发送方,等待可能发生的重传。
因此,TCP 就引入了两个非常重要的机制:
流量控制 和滑动窗口。
流量控制解决的是:
对方现在还能接收多少?
滑动窗口解决的是:
我现在可以连续发送哪些数据,并且哪些数据还不能从发送缓冲区删除?
两者结合起来,才形成了 TCP 高效可靠的批量传输机制。
一、TCP流量控制
1. 为什么需要流量控制
假设主机 A 向主机 B 发送数据。
最简单地理解:
主机A发送缓冲区
|
| 发送数据
v
网络
|
v
主机B接收缓冲区
|
v
用户层程序处理
A 的发送速度可能非常快,但是 B 的应用程序处理数据的速度并不一定快。
例如 B 收到 HTTP 请求以后,还需要经过:
接收数据
↓
从内核接收缓冲区取数据
↓
HTTP解析
↓
反序列化
↓
业务逻辑处理
↓
查询数据库
↓
文件操作
↓
生成响应
所以,"数据已经到达 B"并不代表"B 已经处理完这些数据"。
如果 A 不管 B 的实际处理能力,只顾着高速发送:
A:疯狂发送
↓
B:接收缓冲区不断增长
↓
B:应用层来不及处理
↓
接收缓冲区越来越满
↓
最终没有空间
此时继续发送的数据就可能因为接收方没有足够空间而被丢弃。
这些数据虽然已经经过了网络传输,却最终无法被接收方正常保存,只能导致后续重传。
这显然是一种资源浪费。
所以 TCP 不希望等到"接收方已经撑不住了"之后再处理,而是希望:
接收方提前告诉发送方:我现在还能接收多少。
这就是 TCP 流量控制。
2. TCP窗口大小字段
TCP 报头中存在一个 16 位窗口大小字段。
这个字段用于通告:
接收方当前还能接收多少数据。
例如:
B收到A的数据
↓
B的接收缓冲区
↓
还剩3000字节空间
↓
B回复ACK
↓
窗口大小 = 3000
那么 A 收到这个 ACK 后,就知道:
B告诉我:
从它当前确认的位置开始,
我最多还可以继续发送3000字节。
因此,TCP 的 ACK 不只是"告诉你数据收到了"。
一个 TCP ACK 中通常还会携带当前的接收窗口信息。
也就是说:
ACK = 确认到哪里了
窗口大小 = 还能接收多少
这两个信息共同帮助发送方决定接下来应该怎么发送。
3. 窗口越大和越小分别意味着什么
假设 B 通告:
窗口 = 5000
说明 B 当前接收能力比较充足。
A 可以允许更多数据处于"已经发送但尚未确认"的状态。
如果 B 通告:
窗口 = 1000
说明 B 的接收缓冲区已经比较紧张。
那么 A 就必须减少自己能够继续发送的数据量。
如果 B 通告:
窗口 = 0
那么意思就非常明确:
我现在没有空间继续接收数据了,你先别给我发送数据。
因此:
窗口大
↓
可以发送更多数据
窗口小
↓
减少发送数据
窗口为0
↓
暂停发送数据
这就是 TCP 流量控制最核心的思想。
二、TCP窗口为0以后怎么办
1. 为什么不能简单地一直等待
假设 B 的接收缓冲区满了:
B接收缓冲区
┌──────────────────────┐
│██████████████████████│
│ 已经满了 │
└──────────────────────┘
剩余空间 = 0
于是 B 回复:
ACK = xxx
Window = 0
A 收到以后停止发送数据。
但是,接下来又出现一个问题:
B 的应用程序可能过了一会儿把数据取走了。
例如:
B接收缓冲区
原来:
┌──────────────────────┐
│██████████████████████│
└──────────────────────┘
Window = 0
应用程序读取了一部分数据
现在:
┌──────────────────────┐
│██████████████ │
└──────────────────────┘
Window > 0
那么 A 怎么知道 B 又有空间了?
因此 TCP 需要一种机制,让发送方能够重新获得最新的窗口信息。
2. 窗口探测
当发送方发现:
Window = 0
它不能继续正常发送数据,但并不意味着 TCP 什么都不能做。
发送方可以定期发送窗口探测。
可以简单理解为:
A B
窗口探测 ------------->
<------------- ACK + 最新窗口大小
A 通过这种方式询问:
"你现在有没有空间了?"
B 回复时携带当前最新的窗口大小。
如果 B 已经释放出了空间:
Window = 0
↓
Window = 3000
那么 A 就可以恢复发送。
3. 窗口更新通知
除了 A 主动探测之外,B 也可以主动告诉 A:
"我的接收缓冲区已经有空间了。"
例如:
B接收缓冲区满
Window = 0
↓
应用层取走数据
↓
接收缓冲区出现空间
↓
B发送窗口更新通知
↓
Window = 3000
于是 A 得知:
现在又可以继续发送了。
因此,窗口为零以后,可以把它理解成两种策略共同保证通信不会永久卡住:
发送方
│
├── 定期进行窗口探测
│
└── 等待接收方主动进行窗口更新
两者并不是同一个东西,但最终目的都是让发送方重新知道:
接收方现在到底还能接收多少。
三、三次握手为什么能够提供初始窗口
1. 第一次发送数据时,怎么知道对方能接收多少
现在又出现一个问题。
TCP 三次握手完成以后,A 第一次真正发送应用数据时:
A → B:数据
A 怎么知道 B 的接收能力?
难道还需要再询问一次?
实际上不需要。
因为 TCP 三次握手本身就已经交换过 TCP 报文。
而 TCP 报文头中本身就包含窗口大小信息。
因此:
三次握手
↓
双方交换TCP报头
↓
双方知道对方通告的窗口
↓
连接建立
↓
第一次发送数据时
已经知道对方初始接收能力
所以:
第一次发送数据,不等于第一次发送 TCP 报文。
在真正发送应用数据之前,双方已经通过三次握手交换过报文,也就已经交换过窗口信息。
2. 三次握手除了建立连接,还有什么作用
三次握手最核心的作用当然是建立 TCP 连接、同步序列号等。
但是从流量控制角度来看,它还有一个重要作用:
让双方在连接建立阶段就能够获得对方的初始接收窗口信息。
于是 TCP 后面就可以直接进入:
数据发送
↓
ACK确认
↓
获得新的窗口信息
↓
调整发送范围
↓
继续发送
四、滑动窗口到底是什么
流量控制解决了:
对方还能接收多少?
但是,还有一个问题没有解决。
假设 B 告诉 A:
Window = 4000
那么 A 能不能这样:
发送1000
等待ACK
发送2000
等待ACK
发送3000
等待ACK
发送4000
等待ACK
当然可以,但这样还是慢。
TCP 更希望:
发送1000 ───────>
发送2000 ───────>
发送3000 ───────>
发送4000 ───────>
等待ACK
也就是说:
允许多个数据段同时处于"已经发送但还没有收到 ACK"的状态。
那么到底哪些数据允许直接发送?
这就需要滑动窗口。
五、滑动窗口的本质:发送缓冲区中的一段区域
1. 先把发送缓冲区抽象成数组
真实的 Linux 内核中,TCP 数据并不是简单地放在一个连续的 C 数组里。
底层涉及 sk_buff、队列等复杂结构。
但是为了理解 TCP 的字节流和滑动窗口,我们可以先把发送缓冲区抽象成一个数组:
发送缓冲区
┌────┬────┬────┬────┬────┬────┬────┬────┐
│1001│1002│1003│1004│1005│1006│1007│1008│...
└────┴────┴────┴────┴────┴────┴────┴────┘
每一个位置可以理解成对应字节流中的数据。
这样,TCP 的序号就非常容易理解。
2. 滑动窗口就是这个数组中的一段区域
假设:
START = 1001
END = 5001
那么:
滑动窗口
<-------------->
1001 5001
↓ ↓
┌──────────────────────────┐
│ 可以直接发送的数据 │
└──────────────────────────┘
这部分数据就是当前允许发送、可以暂时不用等待前一个 ACK 的数据范围。
因此:
滑动窗口本质上就是发送缓冲区中,由起始位置和结束位置界定出来的一段逻辑区域。
也可以简单理解成:
滑动窗口 = START ~ END这一段区域
六、发送缓冲区为什么可以划分成三个区域
假设现在:
START = 1001
END = 5001
发送缓冲区可以逻辑上划分成三个区域。
┌──────────────┬──────────────────────┬─────────────────────┐
│ 已确认数据 │ 滑动窗口 │ 待发送数据 │
│ │ 可发送区域 │ │
└──────────────┴──────────────────────┴─────────────────────┘
↑ ↑
START END
1. 已确认数据区域
左边的数据:
已经发送
↓
已经收到ACK
↓
确认对方已经收到
既然已经确认,那么这部分数据就没有继续保留的必要了。
所以可以从发送缓冲区中释放。
2. 滑动窗口区域
中间这部分:
已经发送的数据
+
允许继续发送的数据
核心特点是:
可以在没有等待前面数据 ACK 的情况下继续发送。
例如:
窗口:
┌────────────────────────┐
│ 1001 ... 2000 │
│ 2001 ... 3000 │
│ 3001 ... 4000 │
│ 4001 ... 5000 │
└────────────────────────┘
那么这部分数据就可以批量发送。
3. 待发送区域
滑动窗口右侧的数据:
虽然已经准备好了
但是当前窗口没有覆盖到
所以暂时不能继续发送。
必须等待前面的数据被确认,窗口向右移动以后,这部分数据才能进入滑动窗口。
七、滑动窗口为什么叫"滑动"
假设最开始:
START = 1001
Window = 4000
END = 5001
那么:
1001 5001
↓ ↓
┌──────────────────────────────┐
│ 滑动窗口 │
└──────────────────────────────┘
现在 B 收到了前面的数据,并回复:
ACK = 2001
ACK 2001 的意思是:
2001 之前的数据已经连续收到,下一段期待从 2001 开始。
那么 A 的窗口左边界就可以向右移动:
原来:
1001 5001
↓ ↓
┌──────────────────────────────┐
│ Window │
└──────────────────────────────┘
收到ACK 2001以后:
2001 6001
↓ ↓
┌──────────────────────────────┐
│ Window │
└──────────────────────────────┘
窗口整体向右移动了。
这就是"滑动窗口"这个名字的由来。
八、滑动窗口的左边为什么只能向右移动
这是理解 TCP 滑动窗口最关键的地方之一。
假设现在:
1001 2001 3001 4001 5001
接收方返回:
ACK 1001
ACK 2001
ACK 3001
那么发送方可以依次确认:
1001之前确认
↓
2001之前确认
↓
3001之前确认
所以:
START:
1001
↓
2001
↓
3001
↓
4001
↓
5001
它只能不断向右。
为什么不能向左?
因为 TCP 的确认序号具有单调推进的特征。
已经确认过的数据,不会因为后面发生什么事情,又重新变成"未确认"。
所以:
滑动窗口的左边界不会向左移动。
它可以:
不动
或者
向右
但不会:
向左
九、滑动窗口大小为什么会变大、变小或者不变
这里特别容易把两个概念混在一起:
窗口的位置
和:
窗口的大小
不是一回事。
假设:
START = 1001
Window = 4000
END = 5001
后来收到了:
ACK = 2001
Window = 4000
那么:
START:1001 → 2001
Window:4000 → 4000
窗口向右移动了,但是大小没变。
1. 窗口大小不变
例如:
第一次:
START = 1001
Window = 4000
END = 5001
第二次:
START = 2001
Window = 4000
END = 6001
可以看到:
START 向右移动
Window大小不变
END也向右移动
所以整个窗口向右移动,但宽度保持不变。
2. 窗口变小
假设:
第一次:
START = 1001
Window = 4000
END = 5001
随后:
ACK = 2001
Window = 3000
那么:
START = 2001
END = 5001
可以看到:
START 向右
END 没有继续向右
于是:
窗口宽度:
4000 → 3000
窗口变小了。
这通常意味着接收方当前剩余空间减少了。
3. 窗口变大
例如:
START = 2001
Window = 3000
后来 B 的应用程序快速取走了一批数据,接收缓冲区释放出更多空间,于是 B 通告:
Window = 5000
那么:
START = 2001
Window = 5000
END = 7001
窗口就扩大了。
因此:
窗口的位置由 ACK 推动,窗口的大小由接收方通告的接收能力决定。
这句话非常重要。
十、ACK到底表示什么
TCP 的 ACK 不是简单地表示:
"我收到了这个数据包。"
它的真正意义是:
ACK=N,表示 N 之前的数据已经按序连续收到,下一段希望从 N 开始。
例如:
发送:
1001~2000
2001~3000
3001~4000
4001~5000
如果全部按序收到:
ACK = 5001
意思就是:
5001之前的数据
全部已经连续收到
因此 ACK 本身具有累计确认的特性。
十一、为什么ACK丢了不一定需要重传
假设 A 连续发送:
1001~2000
2001~3000
3001~4000
4001~5000
B 都收到了。
正常情况下可能产生:
ACK 2001
ACK 3001
ACK 4001
ACK 5001
但是网络中可能出现:
ACK 2001 丢失
ACK 3001 丢失
ACK 4001 到达
ACK 5001 到达
A 最后收到了:
ACK 5001
那么 A 需要重传前面的数据吗?
不需要。
因为:
ACK 5001
已经明确告诉 A:
5001之前的数据已经全部按序收到。
所以:
ACK 2001丢了
↓
ACK 3001丢了
↓
ACK 4001丢了
↓
ACK 5001到达
↓
前面的数据全部确认
这就是 TCP 累计确认的重要意义。
因此可以记住一句话:
收到更大的 ACK,就意味着更小 ACK 所对应的确认范围已经包含在其中。
十二、如果真正的数据丢了怎么办
ACK 丢失和数据丢失是两回事。
现在假设 A 发送:
1001~2000
2001~3000
3001~4000
4001~5000
5001~6000
6001~7000
其中:
2001~3000
丢失了。
但是:
3001~4000
4001~5000
5001~6000
6001~7000
都到达了 B。
这时候 B 能不能直接回复:
ACK 7001
不能。
因为 TCP 的累计 ACK 要求数据必须连续。
B 的实际情况是:
1001~2000 已收到
2001~3000 丢失
3001~4000 已收到
4001~5000 已收到
5001~6000 已收到
6001~7000 已收到
中间出现了一个缺口:
1001~2000
↓
2001~3000 ← 缺失
↓
3001~4000
4001~5000
5001~6000
6001~7000
所以 B 只能确认到:
ACK = 2001
因为:
2001之前的数据已经连续收到,但从2001开始的数据没有连续收到。
十三、重复ACK与快重传
如果后面的数据不断到达:
3001~4000
4001~5000
5001~6000
6001~7000
但是:
2001~3000
始终没有到达。
那么接收方的累计确认点就卡在:
ACK = 2001
于是发送方可能连续收到多个:
ACK 2001
ACK 2001
ACK 2001
...
这些 ACK 虽然没有继续向前推进,但它们传递出了一个非常重要的信息:
后面的数据已经到了,但是前面有一个缺口。
当发送方收到足够数量的重复 ACK 后,就可以推断前面的某个数据段很可能丢失,于是提前进行重传。
这就是:
快速重传(Fast Retransmit)。
它的核心思想就是:
发现重复ACK
↓
判断前面存在缺失数据
↓
不用等超时
↓
立即重传
十四、为什么还需要超时重传
有人可能会问:
既然有快速重传,为什么还需要超时重传?
因为快速重传有前提:
必须能够收到足够的重复 ACK。
假设:
最后一个数据段丢失
后面已经没有更多数据可以到达接收方。
那么:
ACK
ACK
ACK
可能根本凑不出来。
此时快速重传无法触发。
所以 TCP 还需要最后一道保险:
数据发送
↓
等待ACK
↓
没有收到确认
↓
计时器超时
↓
重新发送
这就是:
超时重传。
因此可以把两者理解成:
快速重传
↓
发现异常比较快
↓
提前补发
超时重传
↓
快速重传无法判断
↓
最终兜底
十五、为什么各种丢包最终都可以归结为"最左侧未确认数据"
这是理解滑动窗口非常重要的一步。
假设当前窗口:
┌──────┬──────┬──────┬──────┬──────┐
│ 1001 │ 2001 │ 3001 │ 4001 │ 5001 │
└──────┴──────┴──────┴──────┴──────┘
↑
START
如果最左边的:
1001
对应的数据丢了。
那么后面的数据即使都到了:
2001
3001
4001
5001
窗口左侧仍然不能越过去。
因为:
1001之前
还没有被确认。
所以:
START = 1001
一直不动。
最终:
重复ACK
↓
快速重传1001
↓
或者等待超时
↓
超时重传1001
当这个数据补回来以后:
1001
2001
3001
4001
5001
终于连续了。
于是 ACK 可以一下子跳到:
ACK = 5001/5001之后的下一个序号
窗口也随之向右移动。
十六、中间丢包为什么最终也会变成最左侧丢包
假设:
1001
2001
3001 ← 丢失
4001
5001
最开始:
ACK 2001
说明:
1001~2000已经确认
于是窗口左边界从:
1001
移动到了:
2001
继续接收数据。
但是:
3001~4000
没有到达。
那么后面的:
4001
5001
即使已经收到,也不能让 ACK 越过这个缺口。
于是:
ACK = 3001
窗口卡在:
3001
此时对于发送方来说:
1001 已确认
2001 已确认
3001 ← 当前最左侧未确认数据
4001 已到达但等待前面的缺口
5001 已到达但等待前面的缺口
所以原本的"中间丢包"已经被转换成:
当前窗口最左侧的数据没有得到确认。
接下来还是:
重复ACK
↓
快重传
↓
或者
↓
超时重传
十七、最右侧丢包也是同样的道理
再看:
1001
2001
3001
4001
5001 ← 丢失
前面的数据全部正常到达:
ACK 2001
ACK 3001
ACK 4001
ACK 5001
最终窗口左边界移动到了:
5001
但是:
5001
丢了。
那么后面暂时没有新的数据可以帮助产生重复 ACK。
于是可能无法触发快速重传。
最终只能依靠:
超时重传
而一旦:
5001
被重新发送并成功到达,ACK 就继续向右推进。
所以从滑动窗口的角度来看:
无论原始丢包发生在最左边、中间还是最右边,随着前面的数据不断被确认,最终都会转化成"当前窗口最左侧存在一个尚未确认的数据"。
这也是理解 TCP 滑动窗口丢包处理非常重要的一种抽象方式。课程材料也明确采用了"不同位置的丢包最终坍缩为最左侧丢包问题"的理解方式。
十八、未确认的数据为什么不能从发送缓冲区删除
现在就可以回答前面留下的问题:
一个数据已经发送出去了,但是还没有收到 ACK,发送方能不能直接删除?
不能。
例如:
A发送:
1001~2000
数据已经进入网络:
A
|
| 1001~2000
v
网络
|
v
B
但是 A 还没有收到 ACK。
那么 A 不能认为:
"数据应该到了,我删了吧。"
因为:
数据可能丢失
一旦数据真的丢失:
A发现没有ACK
↓
需要重传
↓
但是原数据已经被删除
↓
没有数据可以重传
所以:
已经发送但尚未确认的数据,必须暂时保存在发送缓冲区中。
这也是滑动窗口存在的重要原因之一。
十九、ACK到达以后,数据才可以释放
假设发送缓冲区:
┌──────────┬──────────────────────┬───────────────┐
│已确认数据 │ 滑动窗口 │ 待发送数据 │
└──────────┴──────────────────────┴───────────────┘
如果收到 ACK:
ACK = 3001
说明:
3001之前的数据
已经连续确认
那么:
1001~2000
2001~3000
这些数据就不再需要参与重传。
因此可以从发送缓冲区中释放。
窗口左边界移动:
原来:
1001
↓
┌─────────────────────────┐
│ 滑动窗口 │
└─────────────────────────┘
收到ACK 3001:
3001
↓
┌─────────────────────────┐
│ 滑动窗口 │
└─────────────────────────┘
于是释放出来的空间又可以重新用于保存新的待发送数据。
这就形成了一个不断循环的过程:
应用层写入数据
↓
进入发送缓冲区
↓
进入滑动窗口
↓
发送
↓
等待ACK
↓
收到确认
↓
窗口向右移动
↓
左侧数据释放
↓
释放出的空间重新利用
↓
新的数据进入
二十、滑动窗口和流量控制到底是什么关系
到这里可以把两个概念彻底串起来。
流量控制回答:
对方现在还能接收多少?
而:
滑动窗口负责把这个"还能接收多少"具体落实到发送方当前可以发送的数据范围。
例如 B 告诉 A:
Window = 4000
那么 A 就可以把自己的发送缓冲区抽象成:
当前允许发送
<-------------------->
START END
┌────────┬────────────────────┬───────────┐
│已确认 │ 滑动窗口 │ 待发送 │
└────────┴────────────────────┴───────────┘
所以:
接收方剩余空间
↓
通告Window
↓
发送方调整滑动窗口大小
↓
决定当前最多允许发送多少
↓
实现流量控制
因此可以说:
滑动窗口是 TCP 流量控制的重要实现机制。
二十一、滑动窗口为什么只能向右滑动
现在再总结一下窗口的位置和大小。
窗口左边界:
START = 最新确认位置
随着 ACK 不断向前:
START:
1001
↓
2001
↓
3001
↓
4001
↓
5001
因此它只能:
不动
或者向右
不能向左。
而窗口大小:
Window = 对方当前通告的接收能力
则可能:
变大
变小
不变
所以一定要把这两个概念分开:
START
↓
决定窗口在哪里
Window Size
↓
决定窗口有多宽
可以记成一句话:
ACK决定窗口向哪里移动,接收窗口决定窗口有多大。
二十二、滑动窗口会不会越界
如果把发送缓冲区简单看成普通数组:
0 1 2 3 4 5 6 7
那么 START 和 END 不断向右移动,最终当然可能超过数组末尾。
例如:
0 1 2 3 4 5 6 7
↑
END
继续滑动:
0 1 2 3 4 5 6 7
↑
越界
实际的缓冲区不能真的无限向右扩张。
因此在理解 TCP 缓冲区时,可以把它抽象成一个环形结构。
二十三、用环形数组理解发送缓冲区
例如一个简单的环形缓冲区:
┌───────┐
┌───>│ 0 │───┐
│ ├───────┤ │
│ │ 1 │ │
│ ├───────┤ │
│ │ 2 │ │
│ ├───────┤ │
│ │ 3 │ │
│ ├───────┤ │
│ │ 4 │ │
│ ├───────┤ │
│ │ 5 │ │
│ ├───────┤ │
│ │ 6 │ │
│ ├───────┤ │
└────│ 7 │<──┘
└───────┘
当 END 到达最后:
END = 7
继续移动以后:
END = 0
物理位置回到数组开头。
逻辑上的数据序号仍然继续增长。
因此:
逻辑位置:
1001 → 2001 → 3001 → 4001 → 5001 → ...
物理位置:
0 → 1 → 2 → 3 → 4 → 5 → ...
↓
回绕
这就是环形缓冲区的思想。
实际 Linux 内核中的 TCP 缓冲管理比简单的环形数组复杂得多,这里的环形数组主要用于建立滑动窗口的直观模型。
二十四、把整个TCP滑动窗口过程串起来
现在假设:
A向B发送大量数据
B 当前告诉 A:
接收窗口 = 4000
于是 A 建立当前发送窗口:
START = 1001
END = 5001
发送缓冲区可以理解成:
┌────────────┬────────────────────────┬───────────────┐
│ 已确认数据 │ 滑动窗口 │ 待发送数据 │
└────────────┴────────────────────────┴───────────────┘
↑ ↑
START END
A 可以连续发送:
1001~2000
2001~3000
3001~4000
4001~5000
而不需要每发送一个都等待对应 ACK。
1. 正常接收
B 全部收到:
1001~2000
2001~3000
3001~4000
4001~5000
于是:
ACK = 5001
A 收到 ACK 后:
START = 5001
如果 B 同时通告:
Window = 3000
那么:
END = 5001 + 3000
新的窗口继续向右移动。
2. ACK丢失
假设:
ACK 2001 丢失
ACK 3001 丢失
ACK 4001 到达
ACK 5001 到达
A 最终收到:
ACK 5001
于是直接知道:
5001之前的数据
全部已经确认
前面的 ACK 即使丢失,也不需要重传数据。
3. 中间数据丢失
假设:
1001~2000 到达
2001~3000 丢失
3001~4000 到达
4001~5000 到达
B 不能确认到 5001。
只能不断告诉 A:
ACK 2001
于是 A 发现:
窗口左侧卡住
随后根据重复 ACK 或超时机制:
快速重传
或者
超时重传
补发:
2001~3000
一旦这个缺口补上:
1001~2000
2001~3000
3001~4000
4001~5000
数据重新连续。
ACK 就可以向前推进。
二十五、滑动窗口、流量控制、ACK、重传之间的完整关系
现在可以把整个 TCP 机制串成一条链:
TCP可靠通信
│
┌────────────┴────────────┐
│ │
流量控制 确认应答
│ │
↓ ↓
接收方告诉发送方 ACK告诉发送方
还能接收多少 数据确认到哪里
│ │
└────────────┬────────────┘
↓
滑动窗口
│
┌────────────┼────────────┐
↓ ↓ ↓
批量发送 保存未确认 窗口移动
│ 数据 │
│ │ │
↓ ↓ ↓
提高效率 丢包可重传 释放旧数据
│
↓
发现数据缺失
│
┌───────┴───────┐
↓ ↓
快速重传 超时重传
│ │
└───────┬───────┘
↓
数据补齐
↓
ACK继续前进
↓
窗口继续右移
所以 TCP 的这些知识点并不是互相独立的。
它们实际上是一整套机制。
二十六、最后总结:真正理解滑动窗口只需要抓住几个核心点
第一,流量控制解决的是接收方来不及接收的问题。
接收方通过 TCP 报头中的窗口信息告诉发送方:
我现在还能接收多少。
第二,滑动窗口解决的是 TCP 如何批量发送数据的问题。
它允许:
前一个数据还没有收到ACK
↓
后面的数据也可以继续发送
第三,发送出去但没有收到 ACK 的数据不能立即删除。
必须保存在发送缓冲区中:
未确认
↓
继续保留
↓
必要时重传
收到 ACK 后:
确认
↓
窗口向右移动
↓
旧数据释放
↓
空间重新利用
第四,ACK 是累计确认。
ACK = N
表示:
N之前的数据已经连续收到
所以即使中间几个 ACK 丢失,只要后面收到了更大的 ACK,也不需要因此重传前面的数据。
第五,如果数据中间出现缺口:
1001
2001 ← 丢失
3001
4001
5001
后面的数据即使已经到达,也不能让累计 ACK 越过缺口。
窗口左边界最终会卡在:
2001
这就是滑动窗口处理中最核心的异常状态。
第六,快速重传和超时重传共同负责解决丢包:
重复ACK足够
↓
快速重传
重复ACK不足
↓
等待超时
↓
超时重传
第七,窗口有两个非常重要的属性:
窗口位置
↓
由ACK确认位置推动
↓
只能向右移动
窗口大小
↓
由接收方通告的接收能力决定
↓
可以变大、变小或者不变
最后,把 TCP 滑动窗口浓缩成一句话:
TCP通过接收方通告的窗口大小限制发送方能够同时发送的数据量,再利用ACK不断确认已经连续收到的数据,使发送窗口向右移动;未确认的数据始终保留在窗口中,发生丢包时通过快速重传或超时重传补齐缺口,从而同时实现流量控制、高效批量传输和可靠传输。
而从整个 TCP 学习链路来看,可以把它进一步记成:
接收方有多少空间
↓
通告接收窗口
↓
发送方确定滑动窗口
↓
一次批量发送多个数据段
↓
ACK累计确认
↓
窗口向右滑动
↓
已确认数据释放
↓
新的数据进入窗口
↓
继续批量发送
如果发生丢包
↓
ACK卡住
↓
快速重传 / 超时重传
↓
补齐缺口
↓
ACK继续向右
↓
滑动窗口继续向右
这就是 TCP"滑动窗口 + 流量控制 + 确认应答 + 重传机制"之间真正的完整关系。