QUIC 是基于 UDP 的应用层可靠传输协议,解决 TCP 的队头阻塞、连接建立延迟和中间盒僵化三大问题,是 HTTP/3 的传输层,被 Google、Cloudflare、Meta 大规模部署。
相关文章:Rendezvous Hashing · TCP 拥塞控制(CUBIC 与 BBR)
目录
| 章节 | 说明 |
|---|---|
| 问题背景 | 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(核心标准)
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport(QUIC 核心协议)
- RFC 9001 — Using TLS to Secure QUIC(QUIC TLS 集成)
- RFC 9002 — QUIC Loss Detection and Congestion Control(丢包检测与拥塞控制)
- RFC 9114 — HTTP/3(基于 QUIC 的 HTTP)
- RFC 9369 — QUIC Version 2(QUIC v2 改进)
论文
- Jana Iyengar, Martin Thomson. QUIC: A UDP-Based Multiplexed and Secure Transport. IETF RFC 9000, 2021.
- Adam Langley, Alistair Riddoch, Alyssa Wilk, et al. The QUIC Transport Protocol: Design and Internet-Scale Deployment. ACM SIGCOMM, 2017.(Google QUIC 设计论文)
- Jörg Ott, Stefan Loreto. Real-Time Communication with WebRTC. IETF RMCAT WG. (QUIC 与实时通信的结合)
官方文档与博客
- Cloudflare 技术博客:HTTP/3: the past, the present, and the future. https://blog.cloudflare.com/http3-the-past-present-and-future/
- Google Chrome 团队:Deploying QUIC at Google Scale. https://research.google/pubs/pub46409/
- QUIC WG GitHub:https://github.com/quicwg (官方实现参考)
开源实现
- quic-go(Go):https://github.com/quic-go/quic-go — 最活跃的 QUIC 实现
- ngtcp2(C):https://github.com/ngtcp2/ngtcp2 — 轻量级 C 实现
- MsQuic(C):https://github.com/microsoft/msquic — Microsoft 跨平台实现
- Chromium 内核 QUIC:
net/quic/目录
书籍
- High Performance Browser Networking, Ilya Grigorik, Chapter 4 (Transport Layer Security) & Chapter 12 (HTTP/2). O'Reilly, 2013. (QUIC 背景知识)
- Computer Networking: A Top-Down Approach, Kurose & Ross, 8th Ed., Chapter 3(运输层基础)
评论 (0)
发表评论