Appearance
TCP 流量控制
概念
流量控制(flow control)是"接收方嫌发送方发得太快"时使用的机制——它的手段只有一句话:接收方在每个 ACK 报文里带上"我的窗口字段",告诉发送方"我还能再收多少字节"。 发送方据此收缩自己的发送窗口,绝不会发出超过这个量的数据。
★ 它与拥塞控制的根本区别(本章只做界分):
| 机制 | 谁在下命令 | 想解决什么 | 依据 |
|---|---|---|---|
| 流量控制 | 接收方 | "接收方的缓冲区会不会被撑爆"(点对点) | 接收窗口 rwnd |
| 拥塞控制 | 发送方自己 | "网络中转设备扛不扛得住"(全局) | 拥塞窗口 cwnd |
| ★ 实际能发多少 | 两者取小 | 见 net/44-congestion.md |
★ 一句话抓住本质:流量控制是"接收方给发送方画一条不能越过的线",这条线的长度随接收方应用层"读走多少"而伸缩。
与上一章的衔接:上一章(net/42-handshake.md)里第 2、3 个报文的窗口字段,就是接收方第一次通告自己的接收窗口;上一章(net/41-tcp.md)"窗口字段 16 位、最大 65535 B"这条结论,正是本章"零窗口"与"窗口扩大选项"的前提。
原理
一、接收窗口 rwnd 就是"缓存的空闲量"
★ rwnd 的定义(必背):
★ 发送方窗口的四段划分(划清楚,考试画图题全靠它):
text
发送方的"发送窗口"由接收方通告的 rwnd 决定, 窗口内的四个区段:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 已发送 │ 已发送 │ 可发送 │ 不可发送 │
│ 并已确认 │ 未确认 │ (尚未发送) │ (窗口之外) │
└──────────────┴──────────────┴──────────────┴──────────────┘
▲ ▲ ▲ ▲ ▲
窗口左边界 已用 可发边界 窗口右边界
★ 发送窗口 = 已发送未确认 + 可发送未发送 = rwnd
★ 收到新 ACK -> 左边界右移 (已发送未确认 -> 已发送已确认), 可发送量增加
★ 收到新窗口通告 -> 右边界移动; 右边界左移叫"窗口收缩", TCP 不推荐★ 锚点(本章定死):接收缓存 400 B,初始 rwnd = 400
| 步骤 | 接收方状态 | 通告的 rwnd | 发送方动作 |
|---|---|---|---|
| step1 | 缓存空 | 400 | 发 200 B(seq [1, 200]),在途 200 B,还可发 200 B |
| step2 | 占 200 / 400 | 200 | 发 200 B(seq [201, 400]),在途 400 B,还可发 0 |
| step3 | 占满 400 / 400 | 0 | ★ 停止发送,启动持续计时器 |
| step4 | 应用读走 50 B,空闲 50 B | 仍然 0 | 被 Clark 算法压住(见第三节) |
| step5 | 再读走 150 B,空闲 200 B | 200 | 恢复发送 |
★ 由锚点直接推出的结论:窗口不是"一次能发多少",而是"在途数据的上限"——step2 里发送方已经发了 400 B(两段各 200 B),但那 400 B 都还没被确认,所以"还可发"是 0 而不是 200。 ★ 这是最爱错的一步:算"还能发多少"必须用
二、零窗口与持续计时器:一个必须解开的死锁
★ 危险场景:接收方缓存满 → 通告 rwnd = 0 → 发送方停发。 此时若接收方应用层读走数据、发出了"窗口更新"(rwnd = 200),而这个通告报文在路上丢了——发送方仍在等"窗口更新",接收方在等"数据",双方永远互等。 这就是 TCP 里唯一的死锁,且不需要任何一方故障,仅仅丢一个报文就够了。
★ 解法:持续计时器(persist timer)
text
★ 发送方在收到 rwnd = 0 时, 除了停发, 还启动"持续计时器"
计时器到期 -> 发送一个"零窗口探测"报文段 (只带 1 个字节的数据或 1 个 ACK)
-> 接收方必须应答 (此时会捎带当前的真实窗口值)
-> 若窗口仍为 0, 重新计时; 若窗口已开, 发送方恢复发送
★ 锚点 (承接第一节 step6):
探测报文: seq = 401, 携带 1 字节
接收方应答: ACK, ack = 401, rwnd = 200
★ 那个"多出来的 1 字节"若接收方缓存真没空间, 会被丢弃, 但报文必须回
★ 与超时重传计时器 (见 net/41-tcp.md) 是两个不同的计时器, 别混⚠️ "零窗口"与"窗口探测"是成对出现的:只要发送方看到 rwnd = 0,就必须启用持续计时器;只要收到非零窗口通告,就撤销它。 考试常考"发送方收到 rwnd = 0 之后该做什么"——答"停止发送并启动持续计时器,定时发送探测报文"才算完整。
三、糊涂窗口综合症:两侧各有一半责任
★ SWS(Silly Window Syndrome,糊涂窗口综合症)指"窗口被切得太碎,于是每发一个报文只装很少的有效数据"这种病态——它有两个来源,所以也得两副药:
| 来源 | 病象 | 药 |
|---|---|---|
| 接收方 | 应用层每次只读走几个字(比如 50 B),接收方就把"只有 50 B 的空闲"通告出去 | Clark 算法:空闲量不到 |
| 发送方 | 应用层每次只交来几个字节(比如 1 B),发送方立刻发出去 | Nagle 算法:一个连接里最多一个"未被确认的小段" |
★ 两副药的精确表述(必背):
text
★ Clark 算法 (接收方侧):
阈值 = min(MSS, 接收缓存 / 2)
空闲量 < 阈值 -> 通告 0, 等空闲量够大再一次通告 (别一点点开)
锚点: 缓存 400 B, MSS 1460 B
-> 阈值 = min(1460, 200) = 200 B
-> 空闲 50 / 100 / 150 都通告 0; 空闲 200 才通告 200
★ Nagle 算法 (发送方侧):
规则 1: 只要还有"未被确认的数据", 就把应用层交来的零碎数据攒起来, 不急着发
规则 2: 攒到 >= 一个 MSS 立刻发
规则 3: 收到之前那段的 ACK 后, 立刻把攒着的发出
-> 效果: 同一时刻最多一个"未被确认的小段"在网里
★ 例外: 急用时可置 PSH 位或 TCP_NODELAY 选项绕过★ SWS 到底浪费在哪(算一笔账):
| 载荷 | 首部(IP 20 + TCP 20) | 总长 | 有效载荷占比 |
|---|---|---|---|
| 1 B | 40 B | 41 B | |
| 100 B | 40 B | 140 B | 71.43% |
| 1460 B | 40 B | 1500 B | 97.33% |
⚠️ Nagle 算法与"延迟确认"叠加是个著名的坑:接收方为了少发 ACK,会等 200 ms(上限 500 ms)才确认——于是"Nagle 等 ACK、ACK 被延迟确认拖住"互相卡住,应用层会感到明显延迟。 这就是实时小包应用(游戏、交互式终端)要设
TCP_NODELAY关掉 Nagle 的原因。 408 会问"Nagle 与延迟确认同时启用会有什么后果",答"可能产生最大 500 ms 的额外延迟"即可。
四、窗口大小对吞吐率的限制
★ 流量控制不只是"别撑爆缓存",它同时是一条吞吐率上限:
★ 锚点:rwnd = 400 B 时的三种 RTT:
| RTT | 吞吐率(B/s) | 吞吐率(kbit/s) |
|---|---|---|
| 20 ms | 20000 | 160 |
| 40 ms | 10000 | 80 |
| 100 ms | 4000 | 32 |
★ 结论:窗口小 + RTT 大 = 管子再粗也用不上。 这正是"高带宽时延积链路必须开窗口扩大选项"的动机——net/41-tcp.md 已算过:16 位窗口最大 65535 B,在 RTT 40 ms 的链路上只能跑 13.107 Mbps,离 1 Gbps 差 76.3 倍。
⚠️ 三个"窗口"要分清:接收窗口 rwnd(接收方按缓存通告的)、拥塞窗口 cwnd(发送方按网络状况估的)、发送窗口(实际用的,等于两者取小)。 题目里出现"窗口"二字,先看它说的是哪一个。
示例
例 1:C 实现——rwnd 推进账、零窗口探测与吞吐率
参数:接收缓存 400 B;MSS = 1460 B;探测报文 seq = 401。
#include <stdio.h>
#define MSS 1460
int main(void) {
const int BUF = 400; /* 接收缓存总大小 */
const int thr = MSS < BUF / 2 ? MSS : BUF / 2; /* Clark 阈值 */
double rtts[3] = {0.020, 0.040, 0.100};
int i;
printf("(1) 初始: 接收缓存 %d B, 服务端通告 rwnd = %d\n", BUF, BUF);
printf("\n");
printf("(2) 发送方按 rwnd 推进\n");
printf(" step1: rwnd=%d A 发 200 B [1,200] -> 在途 200 B, 还可发 200 B\n", BUF);
printf(" step2: 缓存占 200/%d, 通告 200 A 发 200 B [201,400] -> 在途 400 B, 还可发 0\n", BUF);
printf(" step3: 缓存满, 通告 rwnd=0 -> A 停发, 启动持续计时器\n");
printf(" step4: 应用读走 50 B, 空闲 50 < min(MSS %d, 缓存一半 %d) = %d -> 仍通告 0\n",
MSS, BUF / 2, thr);
printf(" step5: 再读走 150 B, 空闲 200 >= %d -> 通告 rwnd=200\n", thr);
printf(" step6: 若该通告丢失 -> 持续计时器到时, A 发 1 B 探测 (seq=401)\n");
printf(" -> B 回 ACK ack=401 rwnd=200\n");
printf("\n");
printf("(3) 吞吐率账 (rwnd = %d B)\n", BUF);
for (i = 0; i < 3; i++)
printf(" RTT = %3.0f ms -> %d / %.3f = %.0f B/s = %.0f kbit/s\n",
rtts[i] * 1000, BUF, rtts[i], BUF / rtts[i], BUF * 8 / rtts[i] / 1000);
printf(" ★ 吞吐率 = rwnd / RTT; 窗口太小会把粗管子限住\n");
printf("\n");
printf("(4) 糊涂窗口综合症 (SWS)\n");
printf(" 发送方: Nagle - 最多一个未确认的小段; 攒满 MSS 或收到 ACK 才发\n");
printf(" 接收方: Clark - 通告窗口小于 min(MSS, 缓存一半) = %d B 时通告 0\n", thr);
printf(" ★ 1 B 载荷 + 40 B 首部 -> 效率 1/41 = 2.44%\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
(1) 初始: 接收缓存 400 B, 服务端通告 rwnd = 400
(2) 发送方按 rwnd 推进
step1: rwnd=400 A 发 200 B [1,200] -> 在途 200 B, 还可发 200 B
step2: 缓存占 200/400, 通告 200 A 发 200 B [201,400] -> 在途 400 B, 还可发 0
step3: 缓存满, 通告 rwnd=0 -> A 停发, 启动持续计时器
step4: 应用读走 50 B, 空闲 50 < min(MSS 1460, 缓存一半 200) = 200 -> 仍通告 0
step5: 再读走 150 B, 空闲 200 >= 200 -> 通告 rwnd=200
step6: 若该通告丢失 -> 持续计时器到时, A 发 1 B 探测 (seq=401)
-> B 回 ACK ack=401 rwnd=200
(3) 吞吐率账 (rwnd = 400 B)
RTT = 20 ms -> 400 / 0.020 = 20000 B/s = 160 kbit/s
RTT = 40 ms -> 400 / 0.040 = 10000 B/s = 80 kbit/s
RTT = 100 ms -> 400 / 0.100 = 4000 B/s = 32 kbit/s
★ 吞吐率 = rwnd / RTT; 窗口太小会把粗管子限住
(4) 糊涂窗口综合症 (SWS)
发送方: Nagle - 最多一个未确认的小段; 攒满 MSS 或收到 ACK 才发
接收方: Clark - 通告窗口小于 min(MSS, 缓存一半) = 200 B 时通告 0
★ 1 B 载荷 + 40 B 首部 -> 效率 1/41 = 2.44%⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
- 第 2 步的"在途 400 B,还可发 0"是本例的关键:发送窗口是 400,但在途已占满 400——所以"还能发多少"永远等于
。 - 持续计时器与超时重传计时器是两个计时器:前者在收到零窗口通告时启动(条件:窗口为 0);后者在发出数据时启动(条件:RTO 到期未确认)。 两者都可能触发"重发",但目的完全不同——一个是问"你现在能收了吗",一个是问"我上次那段你收到了吗"。
例 2:Python——四段窗口账、SWS 判别表与取小
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
BUF, MSS = 400, 1460
THR = min(MSS, BUF // 2)
print('=== ① 接收窗口 rwnd 的账 ===')
print(' rwnd = 接收缓存空闲量 = 缓存总大小 - 已收未取走的字节')
for free in (0, 50, 100, 150, 200, 300, 400):
print(' ' + pad('缓存空闲 %3d B' % free, 16) + pad('占 %3d/%d' % (BUF - free, BUF), 16)
+ 'rwnd = %d' % free)
print(' ★ rwnd 随应用层"读走多少"实时变化, 不是固定值')
print()
print('=== ② 发送窗口的四段划分 (rwnd = 400, 已确认到 seq 400, 已发未确认 200 B) ===')
segs = [('已发送并已确认', 1, 400), ('已发送未确认', 401, 600),
('可发送未发送', 601, 800), ('不可发送(窗口外)', 801, 999)]
print(' ' + pad('区段', 20) + pad('序号区间', 18) + pad('字节数', 10) + '说明')
for name, a, b in segs:
note = ''
if name == '已发送未确认':
note = '★ 吃掉窗口额度'
elif name == '可发送未发送':
note = '★ rwnd - 在途 = 400 - 200'
elif name.startswith('不可'):
note = '窗口右边界之外'
print(' ' + pad(name, 20) + pad('[%d, %d]' % (a, b), 18) + pad(str(b - a + 1), 10) + note)
print(' ★ 发送窗口 = [401, 800] 共 400 B = 已发送未确认 200 + 可发送 200')
print()
print('=== ③ 零窗口与持续计时器 ===')
steps = [('缓存占满', '0', '停发 + 启动持续计时器'),
('应用读走 50 B, 空闲 50 < %d' % THR, '0', 'Clark 抑制, 不通告小窗口'),
('再读走 150 B, 空闲 200 >= %d' % THR, '200', '通告窗口更新'),
('窗口更新报文丢失', '200', '★ 双方互等 -> 死锁'),
('持续计时器到时, 发 1 B 探测 seq=401', '200',
'B 回 ACK ack=401 rwnd=200 -> 解开')]
print(' ' + pad('事件', 36) + pad('真实窗口', 12) + '发送方动作')
for ev, w, act in steps:
print(' ' + pad(ev, 36) + pad(w, 12) + act)
print(' ★ 死锁只在"窗口更新报文丢失"时出现, 靠探测报文解开')
print()
print('=== ④ SWS: 接收方通告判断 (阈值 = min(%d, %d/2) = %d) ===' % (MSS, BUF, THR))
print(' ' + pad('空闲量', 10) + pad('通告 rwnd', 12) + '原因')
for free in (0, 50, 100, 150, 199, 200, 400):
if free < THR:
print(' ' + pad(str(free), 10) + pad('0', 12) + '空闲 < 阈值 %d, 压住不通告' % THR)
else:
print(' ' + pad(str(free), 10) + pad(str(free), 12) + '空闲 >= 阈值, 一次通告到位')
print(' ★ 若不压: 空闲 50 就通告 50 -> 发送方只能发 50 B 的小包 -> 首部占 40 B')
print()
print('=== ⑤ SWS: 发送方 Nagle 算法 ===')
events = [('应用交来 1 B, 无在途数据', '立即发出 (此刻只有一个未确认小段)'),
('应用又交来 2 B, 有 1 个未确认小段', '攒住不发'),
('应用又交来 20 B, 已攒 22 B 远小于 MSS', '继续攒'),
('收到前面那 1 B 的 ACK', '★ 立刻把攒住的 22 B 发出'),
('应用交来 1600 B (> MSS 1460)', '★ 不等 ACK, 直接按 MSS 切段发出')]
print(' ' + pad('事件', 38) + '动作')
for ev, act in events:
print(' ' + pad(ev, 38) + act)
print(' ★ Nagle + 延迟确认叠加 -> 最多 500 ms 额外延迟 (实时应用要 TCP_NODELAY)')
print()
print('=== ⑥ 实际发送窗口 = min(rwnd, cwnd) ===')
print(' ' + pad('rwnd', 10) + pad('cwnd', 10) + pad('发送窗口', 12) + '瓶颈在哪')
for r, c in [(400, 1200), (4000, 1200), (65535, 1), (65535, 65535), (0, 1200)]:
m = min(r, c)
if r < c:
note = '接收方 (流量控制)'
elif c < r:
note = '网络 (拥塞控制)'
else:
note = '两者持平'
if m == 0:
note = '★ 零窗口, 停发 + 持续计时器'
print(' ' + pad(str(r), 10) + pad(str(c), 10) + pad(str(m), 12) + note)
print(' ★ 题目里只写"窗口"时, 先判断它说的是 rwnd 还是 cwnd')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 接收窗口 rwnd 的账 ===
rwnd = 接收缓存空闲量 = 缓存总大小 - 已收未取走的字节
缓存空闲 0 B 占 400/400 rwnd = 0
缓存空闲 50 B 占 350/400 rwnd = 50
缓存空闲 100 B 占 300/400 rwnd = 100
缓存空闲 150 B 占 250/400 rwnd = 150
缓存空闲 200 B 占 200/400 rwnd = 200
缓存空闲 300 B 占 100/400 rwnd = 300
缓存空闲 400 B 占 0/400 rwnd = 400
★ rwnd 随应用层"读走多少"实时变化, 不是固定值
=== ② 发送窗口的四段划分 (rwnd = 400, 已确认到 seq 400, 已发未确认 200 B) ===
区段 序号区间 字节数 说明
已发送并已确认 [1, 400] 400
已发送未确认 [401, 600] 200 ★ 吃掉窗口额度
可发送未发送 [601, 800] 200 ★ rwnd - 在途 = 400 - 200
不可发送(窗口外) [801, 999] 199 窗口右边界之外
★ 发送窗口 = [401, 800] 共 400 B = 已发送未确认 200 + 可发送 200
=== ③ 零窗口与持续计时器 ===
事件 真实窗口 发送方动作
缓存占满 0 停发 + 启动持续计时器
应用读走 50 B, 空闲 50 < 200 0 Clark 抑制, 不通告小窗口
再读走 150 B, 空闲 200 >= 200 200 通告窗口更新
窗口更新报文丢失 200 ★ 双方互等 -> 死锁
持续计时器到时, 发 1 B 探测 seq=401 200 B 回 ACK ack=401 rwnd=200 -> 解开
★ 死锁只在"窗口更新报文丢失"时出现, 靠探测报文解开
=== ④ SWS: 接收方通告判断 (阈值 = min(1460, 400/2) = 200) ===
空闲量 通告 rwnd 原因
0 0 空闲 < 阈值 200, 压住不通告
50 0 空闲 < 阈值 200, 压住不通告
100 0 空闲 < 阈值 200, 压住不通告
150 0 空闲 < 阈值 200, 压住不通告
199 0 空闲 < 阈值 200, 压住不通告
200 200 空闲 >= 阈值, 一次通告到位
400 400 空闲 >= 阈值, 一次通告到位
★ 若不压: 空闲 50 就通告 50 -> 发送方只能发 50 B 的小包 -> 首部占 40 B
=== ⑤ SWS: 发送方 Nagle 算法 ===
事件 动作
应用交来 1 B, 无在途数据 立即发出 (此刻只有一个未确认小段)
应用又交来 2 B, 有 1 个未确认小段 攒住不发
应用又交来 20 B, 已攒 22 B 远小于 MSS 继续攒
收到前面那 1 B 的 ACK ★ 立刻把攒住的 22 B 发出
应用交来 1600 B (> MSS 1460) ★ 不等 ACK, 直接按 MSS 切段发出
★ Nagle + 延迟确认叠加 -> 最多 500 ms 额外延迟 (实时应用要 TCP_NODELAY)
=== ⑥ 实际发送窗口 = min(rwnd, cwnd) ===
rwnd cwnd 发送窗口 瓶颈在哪
400 1200 400 接收方 (流量控制)
4000 1200 1200 网络 (拥塞控制)
65535 1 1 网络 (拥塞控制)
65535 65535 65535 两者持平
0 1200 0 ★ 零窗口, 停发 + 持续计时器
★ 题目里只写"窗口"时, 先判断它说的是 rwnd 还是 cwnd五条结论:
- ★ rwnd 就是"接收缓存的空闲量":缓存 400 B 时,占 200 → 通告 200、占满 → 通告 0;它随应用层读取实时伸缩。
- ★ "还能发多少" = rwnd − 已发送未确认:锚点 step2 里 rwnd 仍显示 200,但在途已有 400 B(两段各 200 B),所以实际还可发 0——这是本题型最容易错的一步。
- ★ 零窗口会死锁,靠持续计时器解开:窗口更新报文丢失时双方互等;发送方在 rwnd = 0 时必须启动持续计时器,定时发 1 字节探测。
- ★ SWS 两侧各一半责任:接收方用 Clark(不到
B 就通告 0);发送方用 Nagle(最多一个未确认的小段)——1 B 载荷配 40 B 首部,效率只有 2.44%。 - ★ 实际发送窗口 = min(rwnd, cwnd):谁小谁说话——rwnd = 0 时无论 cwnd 多大都必须停发。
考点
考点
1. 必背结论
- ★ 流量控制的手段只有一个:接收方通过报文段的窗口字段通告 rwnd;发送方的发送窗口不能超过 rwnd。
- ★★ 流量控制 vs 拥塞控制:前者由接收方决定(怕撑爆缓存,点对点);后者由发送方决定(怕压垮网络,全局);发送窗口
。 - ★ rwnd = 接收缓存总大小 − 已收到但应用层未取走的字节数。
- ★★ "还能发多少" = rwnd − 已发送未确认(不能直接用 rwnd)。
- ★ 发送窗口四段:已发送并已确认 / 已发送未确认 / 可发送未发送 / 不可发送;窗口 = 已发送未确认 + 可发送未发送。
- ★★ 零窗口处理:发送方停止发送并启动持续计时器;计时器到期发 1 字节(或 1 个 ACK)的"零窗口探测"报文;接收方必须应答并捎带当前窗口值。
- ★★ 零窗口死锁:窗口更新报文丢失时双方互等——这是 TCP 中唯一的死锁,靠持续计时器破解。
- ★ 持续计时器与超时重传计时器不同:前者由"窗口为 0"触发,后者由"RTO 到期未确认"触发。
- ★★ 糊涂窗口综合症 SWS 的两副药:接收方侧 Clark 算法(空闲量 <
就通告 0);发送方侧 Nagle 算法(同一时刻最多一个未被确认的小段)。 - ★ Nagle 三条规则:有未确认数据就攒;攒满 MSS 立即发;收到 ACK 立即把攒的发出。
- ★★ Nagle 与延迟确认叠加:可能造成最大 500 ms 的额外延迟(实时应用要设
TCP_NODELAY)。 - ★ SWS 的代价:1 B 载荷 + 40 B 首部 = 41 B,效率 2.44%。
- ★ 吞吐率上限
;锚点:rwnd 400 B、RTT 20/40/100 ms → 160 / 80 / 32 kbit/s。 - ★ 窗口扩大选项:16 位窗口最大 65535 B,高带宽时延积链路靠它放大到
(见net/41-tcp.md)。 - ★ 三个"窗口":接收窗口 rwnd、拥塞窗口 cwnd、发送窗口 = min(两者)。
2. 高频陷阱
- 把流量控制与拥塞控制说成一回事:错。一个由接收方决定(端到端),一个由发送方自己估(全局)——虽然最终都是"限速",但"依据"完全不同。
- 认为"发送窗口就等于 rwnd":不严谨。是
——漏掉 cwnd 就丢了拥塞控制那一半。 - 算"还能发多少"时直接用 rwnd:错。要减去已发送未确认的部分——锚点 step2 里 rwnd 200 但实际可发 0。
- 认为"rwnd 是固定值":错。它随应用层读取实时变化。
- 收到 rwnd = 0 后只是"停发":不完整。还必须启动持续计时器——否则窗口更新一丢就彻底死锁。
- 把持续计时器与重传计时器混为一谈:错。触发条件不同、目的不同。
- 认为"窗口更新报文一定会到达":错。零窗口死锁正是它丢失造成的;TCP 没有对"窗口更新"本身再确认。
- 把 Clark 算法的阈值记成 MSS:不完整。是
——锚点里缓存 400 B 时是 200 B 而不是 1460 B。 - 认为 Nagle 算法是接收方的机制:错。Nagle 是发送方侧;Clark 是接收方侧。 两者的名字与归属是最爱考的对应关系。
- 认为"关掉 Nagle 就没有延迟了":不对。延迟确认仍在,只是不再被 Nagle 放大。
- 把"窗口收缩"当成正常操作:要谨慎。TCP 不推荐让窗口右边界左移——会让发送方已经发出去的数据"落到窗口外",造成混乱。
- 认为"窗口字段可以表示任意大的窗口":错。只有 16 位、最大 65535 B;更大靠窗口扩大选项。
- 混淆"接收窗口"(rwnd)与"拥塞窗口"(cwnd)的符号:要背清。rwnd 的 r 是 receive,cwnd 的 c 是 congestion。
3. 解题模板("滑动窗口 / 流量控制题")
① 求"还能再发多少":
还能发 = rwnd - 已发送未确认的字节数
★ 一定要先看"上一批发出去的数据被确认了没有"
② 求"接收方能通告多大窗口":
rwnd = 缓存总大小 - 已收未取走
若 rwnd < min(MSS, 缓存/2) -> 按 Clark 通告 0
否则通告 rwnd
③ 画窗口移动题:
新 ACK 到达 -> 左边界右移到确认号处, 窗口长度不变 (若通告未变)
新窗口通告 -> 右边界 = 左边界 + 新 rwnd
★ 用"边界 + 长度"两个量定位, 别只记长度
④ 吞吐率题:
吞吐率 <= 窗口 / RTT (单位: B/s 与 bit/s 差 8)
拿"窗口 400 B / RTT 40 ms = 10000 B/s = 80 kbit/s"练手
⑤ 零窗口题:
收到 rwnd = 0 -> 停发 + 启动持续计时器
计时器到期 -> 发 1 字节探测 (seq = 上次发到的下一个字节)
收到应答 -> 按新 rwnd 恢复或重新计时4. 与相邻章节的接口
net/41-tcp.md(TCP 报文段):窗口字段(16 位)就是流量控制的载体;"吞吐率 = 窗口 / RTT"与"带宽时延积"两条结论直接复用。net/42-handshake.md(连接管理):握手第 2、3 个报文里的窗口字段是接收窗口的第一次通告;TIME_WAIT 与持续计时器都是"计时器管理"的内容,注意别混。net/44-congestion.md(拥塞控制):本章算出的 rwnd 与下一章的 cwnd 取小,才是真正能发的量;"接收窗口通告"与"拥塞窗口调整"是两条独立并行的控制环。net/22-window.md(链路层滑动窗口):链路层的停止-等待 / GBN / SR 是同一套"窗口"思想的帧级版本——但那里窗口按"帧"滑动,这里按"字节"滑动。os/10-process.md与os/24-thrashing.md:接收缓存的大小是内核参数(SO_RCVBUF);"接收方处理不过来"这件事在操作系统层面表现为"进程读得慢",正与本章的"应用层读走多少决定 rwnd"完全对应。ds/02-stack-queue.md:接收缓存就是一个 FIFO 队列——入队由网卡中断驱动,出队由应用层的read调用驱动。
小结
- ★ 流量控制 = 接收方用窗口字段告诉发送方"我还能收多少";发送窗口
。 - ★ rwnd = 接收缓存空闲量;★ "还能发多少" = rwnd − 已发送未确认。
- ★ 窗口四段:已发送已确认 / 已发送未确认 / 可发送 / 不可发送。
- ★★ 锚点:缓存 400 B → 发两段 200 B 后通告 0;应用读 50 B 仍通告 0(Clark),读够 200 B 才通告 200。
- ★★ 零窗口:停发 + 启动持续计时器;窗口更新丢失会死锁,靠 1 字节探测解开。
- ★★ SWS 两副药:接收方 Clark(阈值
)、发送方 Nagle(最多一个未确认小段);1 B 载荷效率只有 2.44%。 - ★ Nagle + 延迟确认 → 最多 500 ms 延迟;实时应用用
TCP_NODELAY。 - ★ 吞吐率上限 = rwnd / RTT:400 B / 40 ms 只有 80 kbit/s——窗口太小会把粗管子限住。
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。