TCP 拥塞控制 — 慢开始 · 拥塞避免 · 快重传 · 快恢复

慢开始cwnd 从 1 开始,指数增长

初始 cwnd=1 MSS,每收到 1 个 新 ACK → cwnd += 1 MSS。效果:每经过 1 个 RTT,cwnd 翻倍。

轮次cwnd(发送数)发送报文段序号收到 ACK 后 cwnd 变化
11M0+1 → 2
22M1, M2+2 → 4
34M3~M6+4 → 8
48M7~M14+8 → 16

cwnd=16 = ssthresh。按本例处理:下一轮切换为拥塞避免算法

/* 慢开始算法 */
cwnd = 1;  // 初始 1 MSS
ssthresh = 16;  // 慢开始门限

/* 轮次 1: 发 M0 */
ACK(M0) → cwnd += 1;  // cwnd = 2

/* 轮次 2: 发 M1, M2 */
ACK(M1), ACK(M2) → cwnd += 2;  // cwnd = 4

/* 轮次 3: 发 M3~M6 */
ACK(M3..M6) → cwnd += 4;  // cwnd = 8

/* 轮次 4: 发 M7~M14 */
ACK(M7..M14) → cwnd += 8;  // cwnd = 16

// cwnd == ssthresh:教材例子按“下一轮切换”处理
1
R1
2
R2
4
R3
8
R4
16
R5

cwnd bar 图:指数增长,4 轮从 1 到 16

拥塞避免cwnd 线性增长,每 RTT +1

cwnd > ssthresh 后,改慢为稳:每经过 1 个完整 RTT → cwnd += 1 MSS。若 cwnd = ssthresh,慢开始和拥塞避免都可使用;例题常从下一轮按拥塞避免处理。

轮次cwnd发送报文段序号每轮结束后 cwnd
516M15~M3017
617M31~M4718
718M48~M6519
1324M171~M194超时!

轮次 13:24 个报文段中部分丢失 → 超时重传 → 判断网络拥塞

/* 拥塞避免算法 */
// cwnd > ssthresh → 每 RTT +1
for (rtt = 5; cwnd < infinity; rtt++) {
    cwnd += 1;  // 线性增长
    send(cwnd segments);
    wait_for_ack();
}

/* 当 cwnd = 24 时发生超时 */
// Tahoe 处理:
ssthresh = cwnd / 2;  // = 12
cwnd = 1;             // 重回起点
// 重新执行慢开始...
16
R5
17
R6
18
R7
19
R8
20
R9
24
R13

线性增长缓慢但稳定,直到超时

超时重传后cwnd→1,ssthresh←cwnd/2

Tahoe 版本的统一处理:不管什么原因丢包,一旦超时:

动作公式本例
更新 ssthreshssthresh ← max(cwnd/2, 2×MSS)24/2 = 12
重置 cwndcwnd ← 1 MSS1
重新执行慢开始算法

第二轮慢开始:1→2→4→8→12(新 ssthresh)→ 13→14→...

关键规律:ssthresh 总是拥塞发生前 cwnd 的一半,作为"安全窗口"的参考线。

/* Tahoe: 超时后的恢复 */
// 阶段 1: 拥塞检测
if (timeout) {
    ssthresh = max(cwnd / 2, 2 * MSS);
    cwnd = 1;           // 回到原点
}
// 阶段 2: 重新慢开始
while (cwnd < ssthresh) {  // 1→2→4→8→12
    per_ack: cwnd += 1;   // 指数级
}
// 阶段 3: 再次拥塞避免
while (no_loss) {           // 12→13→14→...
    per_rtt: cwnd += 1;    // 线性
}
/* 问题: 如果只是个别报文段丢失
   (非拥塞),Tahoe 也会把 cwnd
   砍到 1 —— 效率损失大 */

快重传收到 3 个重复 ACK → 立即重传

接收方收到失序报文段时,立即发送重复 ACK(不延迟)。发送方据此判断:

事件发送方动作
收到 ACK(M1)正常确认 M1 ✓
收到 ACK(M2)正常确认 M2 ✓
收到 ACK(M2) ×1dup ACK #1 — 可能是乱序
收到 ACK(M2) ×2dup ACK #2 — 注意
收到 ACK(M2) ×3dup ACK #3 → 立即重传 M3!不等超时

意义:收到 3 个重复 ACK 意味着"M3 丢了,但 M4/M5/M6 都到了" → 网络并未拥塞,只是个别丢包。

/* 快重传算法 */
int dup_count = 0;
int last_ack = 0;

void on_ack(int ack_num) {
    if (ack_num > last_ack) {
        // 新的累积确认
        last_ack = ack_num;
        dup_count = 0;
    } else if (ack_num == last_ack) {
        // 重复确认
        dup_count++;
        if (dup_count == 3) {
            // 立即重传丢失的报文段
            retransmit(last_ack);
        }
    }
}

/* 快重传避免了等待超时,使
   整个网络吞吐量提高约 20% */

快恢复Reno:跳过慢开始,直接线性增长

快重传后,Reno 不把 cwnd 降到 1,而是执行快恢复:

步骤TahoeReno(快恢复)
ssthreshcwnd/2cwnd/2
cwnd1ssthresh + 3
收到 dup ACKcwnd += 1(临时扩大)
收到新 ACK慢开始 1→2→4→...cwnd = ssthresh,进入拥塞避免

总结:拥塞控制的四个算法是逐步演进的——慢开始和拥塞避免是基础(Tahoe, 1988),快重传和快恢复是改进(Reno, 1990)。核心思想:试探着增加,丢包了就减,但不该减过头

/* Reno: 快重传 + 快恢复 */
void on_3_dup_ack() {
    ssthresh = cwnd / 2;
    cwnd = ssthresh + 3;
    // 快恢复阶段
    retransmit_lost_segment();
}
void on_dup_ack_during_recovery() {
    cwnd += 1;  // 每额外 dup ACK +1
}
void on_new_ack() {
    cwnd = ssthresh;  // 结束快恢复
    // 直接进入拥塞避免(线性增长)
}

/* 四种算法总结:
   ① 慢开始: cwnd 指数增长
   ② 拥塞避免: cwnd 线性增长
   ③ 快重传: 3 dup ACK → 立即重传
   ④ 快恢复: 不降到 1,继续线性
*/