深蓝黑背景封面:左侧大标题「两个窗口取小值,才是能发的量」,副标题「拥塞窗口管网络,通告窗口管对端」;右侧原创示意图——一条发送缓冲横条,上方罩着 16KB 的拥塞窗口大括号、下方罩着 8KB 的接收通告窗口小括号,可发送区域按 8KB 高亮,公式 min(cwnd, rwnd) 居中;左上标注「面试经典 · 网络」,右上标注「Day 02 · 10-30」 图 1:本文封面,两个括号谁小听谁的。

先说明属性:这是一篇历史笔试题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题面大致是:TCP 连接的拥塞窗口 16KB、对端通告的接收窗口 8KB、MSS 1KB,问发送方此刻还能发多少数据。这是一道定量计算题,但算对的前提是先搞懂两个窗口各管什么。

两个窗口,一个管网络,一个管对端。

拥塞窗口(cwnd)是发送方自己估算的网络容量:网络越堵,它越小,由慢启动、拥塞避免等算法动态调整。它的语义是「网络此刻大概能吞多少」,接收方根本看不见它。

通告窗口(rwnd)是接收方按自己剩余缓冲算出来、通过 ACK 报文带给发送方的:「我这里还放得下多少」。它的语义是对端的收纳能力,与网络路况无关。

RFC 5681 把两者怎么配合写成了规范原文:任何时刻,TCP 不得发送序号高于「已确认的最大序号 加 min(cwnd, rwnd)」的数据。翻译成一句话——发送窗口取两者的较小值,因为它要同时满足「不把网络打爆」和「不把对端撑爆」两个约束。

把题面参数放进公式。

画一条发送方的序号轴,它被窗口切成四段:已确认的、已发送未确认(在途)、可立即发送的、超出窗口不能发的。「还能发多少」= 有效窗口减去在途字节。

代回题面:有效窗口 = min(16KB, 8KB) = 8KB。如果此刻在途为 0,还能发 8KB,即 8 个 MSS 大小的报文段。如果已经有 3KB 在飞,还能发 8 − 3 = 5KB,即 5 个完整报文段——注意不是 16 − 3 = 13KB,cwnd 再大也得听 rwnd 的,这正是本题的陷阱选项所在。

两个边界也值得记住:rwnd 通告为 0 时发送方完全停发,进入零窗口探测(周期性发一字节探针等窗口重新打开);cwnd 在超时重传后会被打回 1 个 MSS 重新慢启动,此刻即使 rwnd 很大,能发的也只有一个报文段。

用代码把几种情形都算一遍。

下面这个计算器实现「min(cwnd, rwnd) − 在途」的公式,覆盖题面情形与四个变体。用 Node.js 22.15.0 直接运行(node tcp-window.js):

'use strict';

// 发送方可用窗口 = min(cwnd, rwnd),可再发字节数 = 可用窗口 - 在途字节
function canSend({ cwnd, rwnd, inFlight, mss }) {
  const effective = Math.min(cwnd, rwnd);
  const room = Math.max(0, effective - inFlight);
  const segments = Math.floor(room / mss);
  return { effective, room, segments };
}

const cases = [
  { name: '题面情形', cwnd: 16, rwnd: 8, inFlight: 0, mss: 1 },
  { name: '已有 3KB 在途', cwnd: 16, rwnd: 8, inFlight: 3, mss: 1 },
  { name: '在途顶满通告窗口', cwnd: 16, rwnd: 8, inFlight: 8, mss: 1 },
  { name: '拥塞窗口更小', cwnd: 4, rwnd: 8, inFlight: 1, mss: 1 },
  { name: '零窗口', cwnd: 16, rwnd: 0, inFlight: 0, mss: 1 },
];
for (const c of cases) {
  const r = canSend(c);
  console.log(
    `${c.name}:cwnd=${c.cwnd}KB rwnd=${c.rwnd}KB 在途=${c.inFlight}KB -> ` +
    `可用窗口 ${r.effective}KB,还能发 ${r.room}KB(${r.segments} 个 MSS)`,
  );
}

实测输出:

题面情形:cwnd=16KB rwnd=8KB 在途=0KB -> 可用窗口 8KB,还能发 8KB(8 个 MSS)
已有 3KB 在途:cwnd=16KB rwnd=8KB 在途=3KB -> 可用窗口 8KB,还能发 5KB(5 个 MSS)
在途顶满通告窗口:cwnd=16KB rwnd=8KB 在途=8KB -> 可用窗口 8KB,还能发 0KB(0 个 MSS)
拥塞窗口更小:cwnd=4KB rwnd=8KB 在途=1KB -> 可用窗口 4KB,还能发 3KB(3 个 MSS)
零窗口:cwnd=16KB rwnd=0KB 在途=0KB -> 可用窗口 0KB,还能发 0KB(0 个 MSS)

五行输出把公式的每个角色都演了一遍:rwnd 小的时候它是瓶颈(前三行),cwnd 小的时候它接棒(第四行),rwnd 归零直接停发(第五行)。无论哪个小,发送方都被「较小的那个」掐着脖子。

定量演算图:上方一条发送方序号轴,分成「已确认」「已发送未确认(在途)」「可立即发送」「超出窗口不可发」四段;拥塞窗口 16KB 与接收通告窗口 8KB 两个括号罩在轴上,有效窗口取二者较小值 8KB。中部算式卡:假设在途 3KB,则可再发 = min(16KB, 8KB) − 3KB = 5KB;若 MSS 为 1KB,即还能发 5 个完整报文段。下方两张注解卡:左「cwnd 是发送方自己估的网络容量,随拥塞控制动态变化」;右「rwnd 是接收方按剩余缓冲通告的,为 0 时触发零窗口探测」 图 2:序号轴四段与一次完整的定量演算。

工程里这个公式每天都在用。

流式推理、大文件上传这类长连接吞吐,天花板就是 min(cwnd, rwnd):抓包看到「带宽明明够却跑不满」,先看是 cwnd 被拥塞算法压住了(慢启动爬坡、丢包降窗),还是 rwnd 被对端应用消费太慢拖住了(接收缓冲满、零窗口)。两个瓶颈的处置方向完全不同,而区分它们只需要这一个公式。

回到笔试现场,建议的答题顺序:先声明两个窗口各自的语义与归属(一个管网络、一个管对端),再给出 min 公式与序号轴四段,然后代入数字算出 8KB 或 5KB,最后补零窗口与慢启动两个边界。计算题答出边界意识,才是满分答案。你抓包分析过窗口打满的传输吗?瓶颈最后定位在哪一侧?欢迎聊聊。