专栏文章
专栏文章
网络算法系列
1. 网络算法 #01:轮询与加权轮询 2. 网络算法 #02:最小连接数 3. 网络算法 #03:Jump Consistent Hash 4. 网络算法 #04:Rendezvous Hashing 5. 网络算法 #05:TCP 拥塞控制(CUBIC 与 BBR) 6. 网络算法 #06:QUIC 丢包恢复与拥塞控制 7. 网络算法 #07:经典一致性哈希(虚拟节点) 8. 网络算法 #08:P2C(Power of Two Choices) 9. 网络算法 #09:TCP 滑动窗口与流量控制 10. 网络算法 #10:Maglev Hashing

网络算法 #06:QUIC 丢包恢复与拥塞控制

发布于 2026-06-04 04:51 👁 8 次阅读
#网络算法#拥塞控制#QUIC#丢包恢复#http3#udp

QUIC 是基于 UDP 的应用层可靠传输协议,解决 TCP 的队头阻塞、连接建立延迟和中间盒僵化三大问题,是 HTTP/3 的传输层,被 Google、Cloudflare、Meta 大规模部署。

相关文章Rendezvous Hashing · TCP 拥塞控制(CUBIC 与 BBR)

quic recovery

目录

章节 说明
问题背景 TCP 三大问题:队头阻塞、握手延迟、中间盒僵化
QUIC 核心设计 基于 UDP 的多路复用、Stream 独立传输
Packet Number 与 RTT 精确测量 PN 单调递增消除重传歧义,ACK Delay 精确 RTT
丢包检测算法 Packet Threshold + Time Threshold 双重机制
拥塞控制 NewReno 默认实现,可插拔 CUBIC/BBR
0-RTT 与 1-RTT 握手 TLS 1.3 集成,首次连接与会话恢复
连接迁移 Connection ID 与 IP/端口解耦
执行追踪 两个 Stream 并发传输,Stream1 丢包全过程
异常与边界场景 0-RTT 重放攻击、路径验证、UDP 封锁降级
参考资料 RFC、论文、官方文档

问题背景

TCP 的三大问题

问题一:队头阻塞(Head-of-Line Blocking)

HTTP/2 多路复用(应用层):
  Stream1: [req1][req2][req3] ─┐
  Stream2: [req4][req5]       ─┤─→ 一条 TCP 连接
  Stream3: [req6]             ─┘

TCP(传输层)视角:
  发送序列:[P1:Stream1][P2:Stream2][P3:Stream1][P4:Stream3]
                ↑
            P1 丢失!TCP 暂停交付所有后续数据
            P2(Stream2)、P3(Stream1)、P4(Stream3)全部等待
            即使 P2/P4 已到达,应用层也无法获取

后果:一个 Stream 的丢包阻塞了所有其他 Stream

问题二:连接建立握手延迟

TCP + TLS 1.2(HTTPS 传统):
  Client                    Server
    ──── TCP SYN ──────────→            // RTT 0 开始
    ←─── TCP SYN+ACK ───────            // RTT 0.5
    ──── TCP ACK ──────────→            // RTT 1(TCP 握手完成)
    ──── TLS ClientHello ──→            // RTT 1
    ←─── TLS ServerHello ───            // RTT 1.5
    ──── TLS 密钥交换 ──────→            // RTT 2
    ←─── TLS Finished ──────            // RTT 2.5(TLS 握手完成)
    ──── HTTP 请求 ──────────→           // RTT 3(数据才开始传输)

首次连接需要 3 RTT 才能发送第一个字节(TCP 1-RTT + TLS 1.2 2-RTT)
TLS 1.3 优化到 TCP 1-RTT + TLS 1-RTT = 2 RTT

问题三:中间盒(NAT/防火墙)僵化

TCP 头部格式是 RFC 793(1981)固化的,NAT/防火墙/负载均衡器
对 TCP 语义有深度解析和依赖:
  - NAT 盒子追踪 TCP 四元组(src_ip:port, dst_ip:port)
  - 防火墙识别 TCP 标志位(SYN/FIN/RST)
  - 中间盒可能修改 TCP 选项字段

后果:无法在不破坏现有中间盒的前提下修改 TCP 协议
      TCP Fast Open(TFO)、Multipath TCP(MPTCP)部署极慢

QUIC 的解决思路

问题 QUIC 解决方案
队头阻塞 多路复用在 Stream 层实现,每个 Stream 独立流控;一个 Stream 丢包只阻塞该 Stream
握手延迟 集成 TLS 1.3,首次连接 1-RTT,会话恢复 0-RTT
中间盒僵化 运行在 UDP 之上,协议逻辑在应用层,对中间盒透明

QUIC 核心设计

2.1 协议栈对比

HTTP/2 栈:                    HTTP/3 栈(QUIC):
  ┌─────────────┐               ┌─────────────┐
  │    HTTP/2   │               │    HTTP/3   │
  ├─────────────┤               ├─────────────┤
  │    TLS 1.3  │               │    QUIC     │  ← 集成 TLS 1.3
  ├─────────────┤               ├─────────────┤
  │    TCP      │               │    UDP      │
  ├─────────────┤               ├─────────────┤
  │    IP       │               │    IP       │
  └─────────────┘               └─────────────┘

2.2 Stream 层多路复用解决队头阻塞

QUIC 连接中并发两个 Stream:
  Stream1: [Packet#1][Packet#3][Packet#5]   // 每个包独立编号
  Stream2: [Packet#2][Packet#4][Packet#6]

Packet#3(Stream1 的第2段)丢失:
  Stream1 接收缓冲:[Packet#1=收到] [Packet#3=缺失] [Packet#5=收到]
                    → Stream1 在 Packet#3 重传前无法向上交付 Packet#5
  Stream2 接收缓冲:[Packet#2=收到] [Packet#4=收到] [Packet#6=收到]
                    → Stream2 完全不受影响,立即向应用交付

对比 TCP(HTTP/2):
  Packet#3 丢失后,TCP 接收方停止交付后续所有数据
  Stream2 的 Packet#4/6 虽已到达,也必须等待 Packet#3 重传

2.3 核心数据结构

// QUIC 连接状态(发送方)
struct quic_connection {
    // 连接标识(与 IP/端口解耦,支持连接迁移)
    uint64_t connection_id;

    // Packet Number 空间(QUIC 有三个 PN 空间)
    uint64_t next_pn_initial;    // Initial 包 PN(握手阶段)
    uint64_t next_pn_handshake;  // Handshake 包 PN
    uint64_t next_pn_1rtt;       // 1-RTT 数据包 PN(最常用)

    // 已发送但未确认的包(用于丢包检测和重传)
    map<uint64_t, sent_packet> sent_packets;  // PN → 包信息

    // RTT 估算(比 TCP 更精确)
    uint64_t latest_rtt;         // 最新 RTT 样本(纳秒)
    uint64_t smoothed_rtt;       // 平滑 RTT(EWMA)
    uint64_t rttvar;             // RTT 方差
    uint64_t min_rtt;            // 连接生命周期内的最小 RTT

    // 拥塞控制
    uint64_t cwnd;               // 拥塞窗口(字节,非 MSS 单位)
    uint64_t ssthresh;           // 慢启动阈值
    uint64_t bytes_in_flight;    // 当前在途字节数

    // 丢包检测
    uint64_t loss_detection_timer;     // 丢包检测定时器
    uint32_t pto_count;               // PTO(Probe Timeout)重传次数
};

// 已发送包的元数据
struct sent_packet {
    uint64_t pn;                 // Packet Number(单调递增,不复用)
    uint64_t send_time;          // 发送时间戳(纳秒)
    uint64_t size;               // 包大小(字节)
    bool     ack_eliciting;      // 是否期望 ACK(PING/数据包=true,ACK包=false)
    bool     in_flight;          // 是否计入 bytes_in_flight
    // 包含的 QUIC 帧列表(用于重传时重新发送帧内容)
    vector<quic_frame> frames;
};

// ACK 帧结构(接收方发送)
struct quic_ack_frame {
    uint64_t largest_acked;      // 已收到的最大 PN
    uint64_t ack_delay;          // 接收方处理延迟(微秒)
    // ACK Range:支持乱序确认
    // 例:largest=100, ranges=[(98-100),(95-96)] 表示 95,96,98,99,100 已收到
    vector<ack_range> ranges;    // 每个 range: [start, end]
};

Packet Number 与 RTT 精确测量

3.1 TCP 重传歧义问题(Karn 算法困境)

TCP 重传歧义:
  发送方:seq=100 ──────────────────→ 接收方
  丢包,超时重传:
  发送方:seq=100(重传)───────────→ 接收方
  接收方:ACK seq=101 ─────────────→ 发送方

  发送方的困惑:
    这个 ACK 是对"原始包 seq=100"的确认?
      → RTT = ack_time - original_send_time(偏大,因为包等了很久才重传)
    还是对"重传包 seq=100"的确认?
      → RTT = ack_time - retransmit_send_time(可能偏小)

  结论:TCP 无法区分,导致 RTT 估算不准确
  Karn 算法的妥协:忽略重传包的 RTT 样本(浪费信息)

3.2 QUIC 的解决方案:PN 单调递增,不复用

QUIC 处理:
  发送方:PN=100 ──────────────────→ 接收方(原始包)
  丢包,QUIC 重传(新 PN):
  发送方:PN=107 ──────────────────→ 接收方(重传,内容相同但 PN 不同)

  场景 A:原始包 PN=100 到达(被延迟,未丢失)
    接收方:ACK{largest=107, ranges=[100-100, 107-107]}
    发送方:明确知道 PN=100 已收到
    RTT_sample = ack_received_time - sent_packets[100].send_time  ✓

  场景 B:原始包 PN=100 真的丢失了
    接收方:ACK{largest=107, ranges=[107-107]}(不包含100)
    发送方:PN=100 未出现在 ACK 中 → 确认丢失,标记为 lost
    RTT_sample = ack_received_time - sent_packets[107].send_time  ✓

  结论:无论哪种情况,RTT 计算都是明确的

3.3 ACK Delay 精确 RTT 测量

// 问题:接收方处理延迟污染 RTT 测量
// 接收方收到 PN=107,在 ack_delay=500μs 后才发送 ACK
// 如果不消除这个延迟,RTT 会偏大 500μs

// RTT 计算公式(RFC 9002)
on_ack_received(ack_frame, ack_received_time):
    // 找到被 ACK 的最大 PN 对应的发送时间
    send_time = sent_packets[ack_frame.largest_acked].send_time

    // 原始 RTT 测量(含接收方处理延迟)
    latest_rtt = ack_received_time - send_time

    // 减去接收方 ACK 延迟(ACK 帧中携带)
    ack_delay = ack_frame.ack_delay  // 单位:微秒

    // 精确 RTT(消除接收方处理延迟)
    // 注意:ack_delay 不能超过 max_ack_delay(对端协商值,默认 25ms)
    adjusted_rtt = latest_rtt - min(ack_delay, max_ack_delay)

    // EWMA 平滑 RTT(与 TCP 相同算法,RFC 6298)
    if first_rtt_sample:
        smoothed_rtt = adjusted_rtt
        rttvar       = adjusted_rtt / 2
    else:
        rttvar       = 3/4 * rttvar + 1/4 * |smoothed_rtt - adjusted_rtt|
        smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * adjusted_rtt

    // 最小 RTT(用于丢包检测的 time_threshold)
    min_rtt = min(min_rtt, latest_rtt)  // 注意:用 latest_rtt(不减 ack_delay)

// 示例数值
// send_time=0ms, ack_received_time=22ms, ack_delay=2ms
// latest_rtt = 22ms - 0ms = 22ms
// adjusted_rtt = 22ms - 2ms = 20ms(真实网络 RTT)
// 首次:smoothed_rtt=20ms, rttvar=10ms

丢包检测算法

4.1 概述

QUIC 使用两种互补的丢包检测机制(RFC 9002 Section 6):

机制 触发条件 特点
Packet Threshold 后续 N 个更大 PN 已被 ACK 快速,适用于乱序到达
Time Threshold 超过 9/8 × smoothed_rtt 后仍无 ACK 兜底,处理极端乱序

4.2 完整丢包检测伪代码

// 常量定义
kPacketThreshold   = 3          // 丢包包阈值(QUIC 默认3,参考 TCP 3-dup-ACK)
kTimeThreshold     = 9.0 / 8.0  // 时间阈值因子(1.125)
kGranularity       = 1ms        // 时钟精度下限

// 收到 ACK 帧时执行丢包检测
detect_lost_packets(ack_frame):
    loss_time = INFINITE
    lost_packets = []

    // 遍历所有已发送但尚未 ACK 的包
    for pn, pkt in sent_packets:
        if pn >= ack_frame.largest_acked:
            continue    // 比最大 ACK 还大,不可能是丢包

        // 计算"已经有多少更大 PN 被确认"
        pn_gap = ack_frame.largest_acked - pn

        // 机制一:Packet Threshold
        // 如果 3 个及以上更大 PN 的包已被确认,认定丢失
        if pn_gap >= kPacketThreshold:
            lost_packets.append(pkt)
            continue

        // 机制二:Time Threshold
        // 计算该包允许等待的最长时间
        lost_send_time = pkt.send_time + max(
            kTimeThreshold * smoothed_rtt,    // 9/8 × smoothed_rtt
            kGranularity                      // 至少 1ms(防止时钟精度问题)
        )

        if now() >= lost_send_time:
            lost_packets.append(pkt)
        else:
            // 还没超时,记录最近的超时点,设置定时器
            loss_time = min(loss_time, lost_send_time)

    // 处理确认丢失的包
    for pkt in lost_packets:
        // 从 sent_packets 中移除
        remove(sent_packets, pkt.pn)
        // 减少 bytes_in_flight
        bytes_in_flight -= pkt.size
        // 通知拥塞控制模块
        congestion_control.on_packet_lost(pkt)
        // 将包内的帧加入重传队列(注意:用新 PN 发送)
        schedule_retransmit(pkt.frames)

    // 如果还有包可能超时,设置定时器
    if loss_time != INFINITE:
        set_loss_detection_timer(loss_time)

    return lost_packets

// 执行追踪示例
// 当前:smoothed_rtt=20ms,kGranularity=1ms
// 发送记录(1-RTT 包空间):
//   PN=95: send_time=0ms   (已发送,未 ACK)
//   PN=96: send_time=2ms   (已发送,未 ACK)
//   PN=97: send_time=4ms   (已发送,未 ACK)
//   PN=98: send_time=6ms   (已发送,未 ACK)
//   PN=99: send_time=8ms   (已发送,未 ACK)
//   PN=100: send_time=10ms (已发送,未 ACK)
//
// 收到 ACK{largest=100, ranges=[98-100]}(95/96/97 未被确认)
//
// 检测 PN=95:pn_gap = 100-95 = 5 >= 3 → Packet Threshold 丢失 ✓
// 检测 PN=96:pn_gap = 100-96 = 4 >= 3 → Packet Threshold 丢失 ✓
// 检测 PN=97:pn_gap = 100-97 = 3 >= 3 → Packet Threshold 丢失 ✓
//   → 95/96/97 标记为丢失,触发拥塞控制,加入重传队列

4.3 PTO(Probe Timeout)—— 探测超时

// PTO 是 QUIC 对 TCP RTO 的改进:不直接重传,而是发送探测包
// 目的:探测网络是否恢复,避免不必要的拥塞控制触发

// PTO 计算公式
pto_timeout = smoothed_rtt
            + max(4 * rttvar, kGranularity)    // 置信区间
            + max_ack_delay                    // 最大 ACK 延迟

// PTO 触发时的行为
on_pto_expired():
    pto_count += 1
    // 发送 1-2 个探测包(PING 帧或最老的未 ACK 数据)
    // 注意:不修改 cwnd(与 RTO 不同)
    send_probe_packets(min(2, unacked_count))

    // 指数退避
    pto_timeout = pto_timeout * 2^pto_count

// PTO vs RTO 对比
// TCP RTO:超时 → 重传 + cwnd=1(进入慢启动)
// QUIC PTO:超时 → 发探测包(不重置 cwnd,直到收到 ACK 才处理丢包)
// 优势:减少不必要的窗口缩小,在 TLP(Tail Loss Probe)场景更高效

拥塞控制

5.1 QUIC 拥塞控制特点

// QUIC 默认使用 NewReno,但可插拔替换为 CUBIC/BBR
// 与 TCP 的主要差异:
//   1. cwnd 以字节计算(TCP 历史上用 MSS 倍数,但现代实现也用字节)
//   2. 初始窗口:min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))
//      ≈ 10 * 1200 = 12000 字节(QUIC 默认 MTU=1200)
//   3. 丢包信号来自 detect_lost_packets(更精确),而非 dup ACK 计数

// 核心状态
struct quic_cc_state {
    uint64_t cwnd;                  // 拥塞窗口(字节)
    uint64_t ssthresh;              // 慢启动阈值(字节)
    uint64_t bytes_in_flight;       // 在途字节数
    uint64_t congestion_recovery_start_time;  // 进入恢复的时间
};

// 初始化
quic_cc_init():
    cwnd     = min(10 * max_datagram_size, max(2 * max_datagram_size, 14720))
    ssthresh = INFINITE
    bytes_in_flight = 0

5.2 NewReno 完整伪代码(QUIC 版)

// ACK 触发 cwnd 更新
on_packet_acked(acked_packet):
    bytes_in_flight -= acked_packet.size

    if is_in_congestion_recovery():
        return    // 恢复期间不增长窗口(NewReno 特性)

    if cwnd < ssthresh:
        // 慢启动:每 ACK 增加 acked_packet.size(等效每 RTT 翻倍)
        cwnd += acked_packet.size
    else:
        // 拥塞避免:每 RTT 增加约 max_datagram_size
        // 公式:每 ACK 增加 max_datagram_size * acked_bytes / cwnd
        cwnd += max_datagram_size * acked_packet.size / cwnd

// 丢包触发 cwnd 减少
on_congestion_event(sent_time):
    // 防止同一个 RTT 内多次触发(ECN CE 标记同理)
    if sent_time <= congestion_recovery_start_time:
        return    // 已处理过这个 RTT 的拥塞事件

    congestion_recovery_start_time = now()

    // NewReno:窗口减半(乘以 0.5)
    ssthresh = max(cwnd / 2, 2 * max_datagram_size)
    cwnd     = ssthresh

// 持续拥塞(Persistent Congestion)
// 含义:在一个 PTO 窗口内所有包都丢失,视为网络严重拥塞
on_persistent_congestion():
    // 类似 TCP 超时:重置 cwnd 到初始值
    ssthresh = max(cwnd / 2, 2 * max_datagram_size)
    cwnd     = min(ssthresh, kInitialWindow)

// 判断是否处于恢复期
is_in_congestion_recovery():
    return congestion_recovery_start_time != 0 &&
           latest_acked_send_time <= congestion_recovery_start_time

5.3 可插拔拥塞控制

// QUIC 设计为可插拔拥塞控制(通过传输参数协商)
//
// 常见实现:
//   QUIC-go(Google):支持 NewReno(默认)、CUBIC、BBR
//   Chromium/Chrome:使用 CUBIC 或 BBR(可配置)
//   nginx QUIC:支持 reno(默认)、cubic、bbr
//
// Linux 内核 QUIC(实验性):复用 TCP 拥塞控制框架
//
// 切换为 BBR(以 nginx 为例):
//   quic_congestion_control bbr;
//
// CUBIC vs BBR 在 QUIC 上的选择(参考 TCP 篇原则):
//   - 数据中心/短 RTT:CUBIC 公平性更好
//   - 跨洋/高 BDP:BBR 吞吐更高
//   - 移动网络:BBR(不把随机丢包当拥塞)

0-RTT 与 1-RTT 握手

6.1 握手流程对比

首次连接(1-RTT):
  Client                              Server
    ──── Initial[CRYPTO:ClientHello] ──→   // PN=0,含 TLS ClientHello
    ←─── Initial[CRYPTO:ServerHello] ────  // PN=0,含 ServerHello
    ←─── Handshake[CRYPTO:...] ──────────  // 含 Certificate, CertVerify
    ←─── 1-RTT[STREAM:...] ─────────────  // 服务器可以立即发数据(0.5-RTT)
    ──── Handshake[CRYPTO:Finished] ──→    // 客户端完成握手确认
    ──── 1-RTT[STREAM:HTTP请求] ────→      // 发送应用数据
         ↑ 此时为 1 RTT(比 TCP+TLS1.3 的 2 RTT 少一半)

握手 + 数据传输总共:1 RTT(服务器在 0.5 RTT 处已可发数据)
会话恢复(0-RTT):
  前提:客户端有上次连接保存的 session ticket(含服务器的 0-RTT 密钥)

  Client                              Server
    ──── Initial[CRYPTO:ClientHello] ──→   // 含 pre_shared_key 扩展
    ──── 0-RTT[STREAM:HTTP请求] ────→      // 与 ClientHello 同时发送!
         // 注意:0-RTT 数据在握手完成前发出,存在安全风险
    ←─── Initial[CRYPTO:ServerHello] ────
    ←─── 1-RTT[STREAM:HTTP响应] ─────────

数据传输无需等待握手:0 RTT(请求随 ClientHello 一起发出)

6.2 QUIC 三个 Packet Number 空间

// 不同安全级别使用独立的 PN 空间,防止跨级别重放攻击
PN 空间            加密级别              用途
─────────────────────────────────────────────────────
Initial           Initial 密钥          ClientHello/ServerHello
                  (基于 connection_id 派生,可被中间人解密)
Handshake         Handshake 密钥        Certificate, CertVerify, Finished
                  (前向安全,握手密钥)
1-RTT (Application)  应用密钥           应用数据、HTTP/3 帧
                  (最终会话密钥,前向安全)

每个空间内 PN 从 0 开始单调递增,互不干扰
ACK 帧只能确认同一 PN 空间内的包

连接迁移

7.1 TCP 与 QUIC 的连接标识对比

TCP 连接 = 四元组(src_ip, src_port, dst_ip, dst_port)
  手机 WiFi (192.168.1.5:50001) ←→ 服务器 (1.2.3.4:443)
    手机切换到 4G → IP 变为 10.0.0.1:52000
    四元组变化 → TCP 连接断开,必须重新建立连接(1-3 RTT)
    HTTP 请求失败,用户感知卡顿

QUIC 连接 = Connection ID(64位随机数,应用层维护)
  Connection ID 不依赖 IP/端口,由端点自行分配
  手机切换到 4G → IP 变为 10.0.0.1:52000
    底层 UDP 四元组变化,但 Connection ID 不变
    QUIC 连接继续有效,数据传输不中断

7.2 连接迁移流程(PATH_CHALLENGE/PATH_RESPONSE)

连接迁移时的路径验证:
  (防止攻击者伪造 IP 重定向流量)

Client (新IP: 10.0.0.1)         Server
    ──── 1-RTT[PATH_CHALLENGE: random_data=0xABCD] ──→
         // 用新路径发送路径质询,random_data 是随机64位值
    ←─── 1-RTT[PATH_RESPONSE: random_data=0xABCD] ───
         // 服务器必须回显相同 random_data,证明路径连通
    ──── 1-RTT[STREAM: 继续传输] ──────────────────→
         // 路径验证通过,使用新路径继续传输

同时,服务器也可以向新路径发 PATH_CHALLENGE,验证客户端控制该 IP
路径验证超时(无响应)→ 放弃新路径,维持旧路径

执行追踪

场景:两个 Stream 并发传输,Stream1 发生丢包

初始状态:
  Connection ID = 0xDEADBEEF
  smoothed_rtt = 20ms
  cwnd = 12000 bytes(初始窗口 ≈ 10 MSS)
  Stream1 传输 1200字节数据
  Stream2 传输 1200字节数据

步骤一:发送阶段(t=0ms)

  发送方发出 6 个 1-RTT 包:
  PN=1: STREAM{id=1, offset=0,    data=1200B}    // Stream1 第1段
  PN=2: STREAM{id=2, offset=0,    data=1200B}    // Stream2 第1段
  PN=3: STREAM{id=1, offset=1200, data=1200B}    // Stream1 第2段
  PN=4: STREAM{id=2, offset=1200, data=1200B}    // Stream2 第2段
  PN=5: STREAM{id=1, offset=2400, data=1200B}    // Stream1 第3段
  PN=6: STREAM{id=2, offset=2400, data=1200B}    // Stream2 第3段
  bytes_in_flight = 6 × 1200 = 7200 bytes

步骤二:网络传输(PN=3 丢失)

  到达接收方的顺序:PN=1,2,4,5,6(PN=3 在网络中丢失)
  接收方缓冲:
    Stream1: [offset=0: 已收] [offset=1200: 缺失] [offset=2400: 已收,等待]
    Stream2: [offset=0: 已收] [offset=1200: 已收] [offset=2400: 已收] ✓

步骤三:接收方发送 ACK(t=22ms,ack_delay=2ms)

  接收方发出:
  ACK{
    largest_acked = 6,
    ack_delay = 2000μs,         // 2ms 处理延迟
    ack_ranges = [(4-6), (1-2)] // 乱序确认:1,2 和 4,5,6 已收到,3 缺失
  }

步骤四:发送方收到 ACK(t=22ms)

  RTT 计算:
    latest_rtt = 22ms - 0ms = 22ms
    adjusted_rtt = 22ms - 2ms = 20ms
    smoothed_rtt = 7/8 * 20ms + 1/8 * 20ms = 20ms(首次保持不变)

  丢包检测(detect_lost_packets):
    检查 PN=3:largest_acked=6,pn_gap = 6-3 = 3 >= kPacketThreshold(3)
    → PN=3 判定为丢失!(Packet Threshold 触发)

    bytes_in_flight -= 1200(移除 PN=3)
    bytes_in_flight 现在 = 7200 - 5×1200 - 1200(已ACK5个+丢失1个)
                          = 0 bytes

步骤五:拥塞控制响应

  on_congestion_event(sent_time=PN=3的发送时间=0ms):
    sent_time(0ms) > congestion_recovery_start_time(0)? 是 → 触发
    ssthresh = max(12000 / 2, 2400) = 6000 bytes
    cwnd     = 6000 bytes
    congestion_recovery_start_time = 22ms

步骤六:Stream1 重传(新 PN)

  发送方重新发送 Stream1 第2段:
  PN=7: STREAM{id=1, offset=1200, data=1200B}    // 新 PN!
  bytes_in_flight = 1200 bytes

步骤七:Stream 交付状态

  Stream2 应用层:offset=0~3600 全部交付 ✓(未受影响)
  Stream1 应用层:offset=0~1200 已交付(等待 PN=7 到达后交付 1200~3600)

步骤八:PN=7 到达接收方(t=42ms)

  ACK{largest_acked=7, ack_delay=1ms, ack_ranges=[(7-7)]}
  RTT = 42ms - 22ms - 1ms = 19ms(重传包的精确 RTT)
  Stream1 接收方缓冲完整,向应用层交付 offset=1200~3600

PN 序列总览:
  PN=1: Stream1[0]     ACK'd ✓
  PN=2: Stream2[0]     ACK'd ✓
  PN=3: Stream1[1200]  LOST(重传为 PN=7)
  PN=4: Stream2[1200]  ACK'd ✓
  PN=5: Stream1[2400]  ACK'd ✓
  PN=6: Stream2[2400]  ACK'd ✓
  PN=7: Stream1[1200]  ACK'd ✓(重传内容,新 PN)

异常与边界场景

场景一:0-RTT 重放攻击

攻击原理:
  攻击者截获客户端发出的 0-RTT 数据包:
  [Initial: ClientHello] + [0-RTT: POST /transfer?amount=100]

  攻击者重放上述包:
  [Initial: ClientHello(复制)] + [0-RTT: POST /transfer?amount=100(复制)]
  服务器无法区分合法请求和重放请求 → 转账执行两次

QUIC 的防护机制:
  1. 会话 ticket 包含 replay_nonce,服务器可检测重复使用
  2. 服务器可维护「已消费 ticket」列表(有状态防重放)
  3. 客户端发送的 0-RTT 数据有时间窗口限制(通常 24小时内有效)

根本解决方案:
  0-RTT 只能用于「幂等操作」:
  ✓ 安全:GET /api/user(只读,重放无害)
  ✓ 安全:GET /favicon.ico(静态资源)
  ✗ 危险:POST /payment(非幂等,重放造成重复付款)
  ✗ 危险:DELETE /post/123(非幂等,重放造成重复删除)

HTTP/3 实践:服务器通过 TLS early_data_accepted 指示是否接受 0-RTT
应用层 MUST 对 0-RTT 请求实施幂等性检查

场景二:连接迁移中的路径验证

攻击场景(无路径验证的风险):
  正常连接:Client(1.1.1.1) ←→ Server(2.2.2.2)
  攻击者:Attacker(3.3.3.3) 发送伪造包,声称 Connection ID=0xDEADBEEF
            路径从 1.1.1.1 → 3.3.3.3
  如无验证:服务器将后续流量全部转发给攻击者!
  风险:流量劫持、带宽放大攻击(amplification)

路径验证流程(PATH_CHALLENGE):
  1. 服务器收到来自新路径(3.3.3.3)的包
  2. 服务器发送 PATH_CHALLENGE(random=0xXXXX) 到新路径
  3. 只有真正控制 3.3.3.3 的一方才能回复 PATH_RESPONSE(0xXXXX)
  4. 攻击者(只有 3.3.3.3 的监听权限,没有发送权限)无法伪造响应
  5. 验证超时(3s)→ 拒绝迁移到该路径

带宽放大攻击防护:
  在路径验证完成前,服务器向新路径的响应数据量 ≤ 3 × 收到的数据量
  防止用小包触发大量响应,造成 DDoS

场景三:UDP 被防火墙封锁的降级策略

现实情况:约 7-10% 的网络封锁 UDP 443 端口(企业防火墙、部分 ISP)

QUIC 的降级机制(Connection Coalescing):

  客户端发现策略:
  浏览器同时尝试:
    HTTP/3(QUIC/UDP:443)← 优先
    HTTP/2(TCP:443)      ← 备选
  如果 QUIC 在 300ms 内无响应 → 使用 HTTP/2
  (Happy Eyeballs V2 风格的竞速连接)

  服务器通知机制:
  HTTP/2 响应头:
    Alt-Svc: h3=":443"; ma=86400
  含义:该服务支持 HTTP/3,端口 443,有效期 24小时
  客户端下次访问直接尝试 QUIC

  QUIC Bit Greasing(欺骗中间盒):
  QUIC 随机设置 Fixed Bit(固定位),防止中间盒基于固定模式过滤
  部分防火墙通过识别 QUIC 包格式来封锁,Greasing 增加识别难度

实际降级体验:
  首次请求:300ms 超时后回退 HTTP/2,用户感知轻微延迟
  后续请求:浏览器缓存 Alt-Svc,直接使用上一次成功的协议
  HTTP/3 黑洞计数:连续失败 N 次后,将该域名标记为"不支持 QUIC"

场景四:QUIC 的 ACK 频率优化

问题:高吞吐传输时,每个包都 ACK 导致 ACK 流量过大

QUIC ACK 策略(RFC 9000):
  规则1:收到 ack_eliciting 包,最多延迟 max_ack_delay(默认 25ms)
  规则2:收到 2 个 ack_eliciting 包后,必须立即发送 ACK(减少发送方等待)
  规则3:乱序到达时立即 ACK(触发发送方的丢包检测)

ACK Frequency 扩展(RFC 9369 草案):
  发送方通过 ACK_FREQUENCY 帧协商 ACK 策略:
    ACK_FREQUENCY{
      sequence_number = 1,
      ack_eliciting_threshold = 10,  // 每10个包才必须ACK一次
      request_max_ack_delay = 50000  // 50ms 最大延迟
    }
  高吞吐场景(文件下载):减少 ACK 开销
  低延迟场景(视频会议):保持高频 ACK

参考资料

RFC(核心标准)

  1. RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport(QUIC 核心协议)
  2. RFC 9001 — Using TLS to Secure QUIC(QUIC TLS 集成)
  3. RFC 9002 — QUIC Loss Detection and Congestion Control(丢包检测与拥塞控制)
  4. RFC 9114 — HTTP/3(基于 QUIC 的 HTTP)
  5. RFC 9369 — QUIC Version 2(QUIC v2 改进)

论文

  1. Jana Iyengar, Martin Thomson. QUIC: A UDP-Based Multiplexed and Secure Transport. IETF RFC 9000, 2021.
  2. Adam Langley, Alistair Riddoch, Alyssa Wilk, et al. The QUIC Transport Protocol: Design and Internet-Scale Deployment. ACM SIGCOMM, 2017.(Google QUIC 设计论文)
  3. Jörg Ott, Stefan Loreto. Real-Time Communication with WebRTC. IETF RMCAT WG. (QUIC 与实时通信的结合)

官方文档与博客

  1. Cloudflare 技术博客:HTTP/3: the past, the present, and the future. https://blog.cloudflare.com/http3-the-past-present-and-future/
  2. Google Chrome 团队:Deploying QUIC at Google Scale. https://research.google/pubs/pub46409/
  3. QUIC WG GitHub:https://github.com/quicwg (官方实现参考)

开源实现

  1. quic-go(Go):https://github.com/quic-go/quic-go — 最活跃的 QUIC 实现
  2. ngtcp2(C):https://github.com/ngtcp2/ngtcp2 — 轻量级 C 实现
  3. MsQuic(C):https://github.com/microsoft/msquic — Microsoft 跨平台实现
  4. Chromium 内核 QUIC:net/quic/ 目录

书籍

  1. High Performance Browser Networking, Ilya Grigorik, Chapter 4 (Transport Layer Security) & Chapter 12 (HTTP/2). O'Reilly, 2013. (QUIC 背景知识)
  2. Computer Networking: A Top-Down Approach, Kurose & Ross, 8th Ed., Chapter 3(运输层基础)
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论