文章

HTTP/3与QUIC协议深度解析

HTTP/3与QUIC协议深度解析

一句话概括

HTTP/3 是 HTTP 协议的第三代版本,基于 Google 设计的 QUIC(Quick UDP Internet Connections)传输协议,彻底抛弃了 TCP 改用 UDP,实现了 0-RTT 连接建立、连接迁移和无队头阻塞等传统 HTTP 协议无法企及的能力。

背景与意义

HTTP 协议的演进困局

HTTP 从 1.0 发展到 1.1,再到 2015 年标准化的 HTTP/2,每次迭代都在提升性能。但 HTTP/2 本质上仍然跑在 TCP 之上,TCP 作为 1974 年设计的传输层协议,其核心特性——可靠传输、拥塞控制、按序交付——在移动互联网时代逐渐暴露出结构性缺陷。

2020 年,全球移动数据流量首次超过固定网络流量,移动设备在 Wi-Fi 与蜂窝网络间的频繁切换成为常态。一场视频会议中,当你从办公室 Wi-Fi 走到电梯时,TCP 连接会在信号衰减时超时断开,应用层必须重建 TCP 三次握手 + TLS 加密握手才能恢复通信,整个过程耗时 1-3 秒。在直播、云游戏、实时协作等场景下,这种断连是不可接受的。

为什么选择 UDP

UDP(User Datagram Protocol)是一个极其简单的传输层协议——发送方只管丢包,接收方只管收包,不保证可靠、不保证顺序、不管理拥塞。这正是 QUIC 选择它作为基座的原因:在 UDP 之上实现用户态的可靠传输层,让协议演进不再受限于操作系统内核的更新周期

传统 TCP 的改进(如 TCP Fast Open、MPTCP)需要同时更新客户端和服务器的操作系统内核,互联网上有数十亿台设备,操作系统版本碎片化极其严重,一个 TCP 特性的全球部署周期往往需要 5-10 年。而 QUIC 实现在应用层(Chromium、nginx、CDN 边缘节点),通过浏览器和软件更新即可快速迭代。

HTTP/3 于 2022 年 6 月正式成为 RFC 9114 标准。截至 2026 年,全球已有超过 80% 的浏览器流量通过 HTTP/3 传输,Google、YouTube、Facebook、Cloudflare、Akamai 等头部平台已全面部署。

概念与定义

QUIC 协议栈分层

1
2
3
4
5
6
7
8
9
10
11
+------------------+
|    HTTP/3        |  (应用层)
+------------------+
|    QPACK         |  (头部压缩)
+------------------+
|    QUIC 传输     |  (传输层,用户态)
+------------------+
|    UDP           |  (传输层,内核态)
+------------------+
|    IP            |  (网络层)
+------------------+

QUIC 传输层是 HTTP/3 的核心,它包含了传统 TCP、TLS、HTTP/2 帧层的部分功能:

功能TCP/TLS 方案QUIC 方案
握手延迟TCP (1 RTT) + TLS 1.3 (1 RTT) = 2 RTTQUIC 初次 1 RTT,重连 0 RTT
队头阻塞TCP 层面的 HoL blocking完全消除(流独立)
连接迁移不原生支持,需重建连接连接标识符(CID) 实现无缝迁移
加密TLS 在 TCP 之上独立分层加密是 QUIC 的内建特性
拥塞控制内核态,需系统更新用户态,可随时换算法

QUIC Connection ID

QUIC 连接标识符(Connection ID, CID)是理解连接迁移的关键。传统 TCP 通过四元组(源 IP、源端口、目的 IP、目的端口)标识一条连接,任何一个元素变化都会导致连接失效。QUIC 则使用服务端分配的 CID 标识连接,IP 和端口的变化不会切断连接。

1
2
传统 TCP 连接标识: {src_ip, src_port, dst_ip, dst_port}
QUIC 连接标识:     {Destination Connection ID (DCID)}

HTTP/3 帧结构

QUIC 不直接暴露 UDP 数据包给 HTTP/3,而是提供 QUIC Stream 抽象层。HTTP/3 在此基础上定义了自己的帧类型:

  • HEADERS 帧:传输 HTTP 头部(使用 QPACK 压缩)
  • DATA 帧:传输请求体/响应体
  • SETTINGS 帧:传输 HTTP/3 连接参数
  • GOAWAY 帧:优雅关闭连接
  • PUSH_PROMISE 帧:服务端推送

最小示例

以下是一个使用 Node.js 内置的 http2 适配库和 node-fetch 扩展演示 QUIC/HTTP/3 的基础交互(实际使用 @node-quic/core 库):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 服务端: HTTP/3 + QUIC 服务器 (需要 Node.js ≥ 22 + 编译 QUIC 支持)
// 实际运行时需要编译 quiche 或使用 Cloudflare Quiche 库

import { createQuicSocket } from 'node:quic';

// 自签名证书 (QUIC 强制要求 TLS 1.3)
const cert = `-----BEGIN CERTIFICATE-----
... (实际部署需使用 Let's Encrypt 签发的证书)
-----END CERTIFICATE-----`;

const key = `-----BEGIN PRIVATE KEY-----
... 
-----END PRIVATE KEY-----`;

async function startH3Server() {
  const socket = createQuicSocket({
    endpoint: { port: 4433 },
    server: { key, cert, alpn: 'h3' }  // ALPN 协商 h3 协议
  });

  socket.on('session', (session) => {
    console.log(`新 QUIC 会话建立: ${session.remoteAddress}`);

    session.on('stream', (stream) => {
      // QUIC 流 — 天然支持多路复用,无队头阻塞
      const reader = stream.readable.getReader();
      const writer = stream.writable.getWriter();

      (async () => {
        const { value: requestData } = await reader.read();
        console.log(`收到请求: ${requestData.toString()}`);

        // 构造 HTTP/3 响应
        const response = [
          ':status: 200',
          'content-type: text/plain',
          '',
          'Hello from HTTP/3 over QUIC!'
        ].join('\r\n');

        await writer.write(Buffer.from(response));
        await writer.close();
      })();
    });
  });

  await socket.listen();
  console.log('HTTP/3 服务器已启动在 :4433');
}

// 客户端
async function h3Client() {
  const socket = createQuicSocket({ client: { key, cert, alpn: 'h3' } });

  const session = await socket.connect({
    address: '127.0.0.1',
    port: 4433,
    servername: 'localhost'
  });

  // 建立 QUIC 流后发送 HTTP 请求
  const stream = await session.openStream();
  const writer = stream.writable.getWriter();
  
  const request = [
    ':method: GET',
    ':path: /hello',
    ':scheme: https',
    ':authority: localhost:4433',
    '',
    ''
  ].join('\r\n');

  await writer.write(Buffer.from(request));
  await writer.close();

  const reader = stream.readable.getReader();
  const { value: response } = await reader.read();
  console.log(`响应: ${response.toString()}`);

  await socket.close();
}

startH3Server().catch(console.error);

验证方式

使用 curl 的 HTTP/3 支持(需编译 quiche):

1
2
3
4
5
6
7
8
# 启动测试服务器
node server.mjs

# 另一个终端用 curl 测试
curl --http3 -v https://localhost:4433/hello

# 查看 QUIC 连接信息
curl --http3 -v --http3-only -o /dev/null https://www.google.com/

核心知识点拆解

1. 0-RTT 连接建立

HTTP/3 最令人惊叹的特性之一:在某些场景下,发送 HTTP 请求不需要等待任何网络往返。

初次连接(1-RTT):

1
2
3
4
5
6
7
8
Client                          Server
  |                               |
  |--- Initial (ClientHello) ---->|  QUIC 握手起始
  |<-- Initial (ServerHello) -----|
  |<-- Handshake (证书 + 参数) ---|
  |--- Handshake (Finished) ----->|
  |========= 连接就绪 ===========|
  |--- HTTP Request (Stream 0) -->|  1 RTT 后即可发数据

重连(0-RTT):

1
2
3
4
5
6
Client                          Server
  | (缓存了上次的服务器配置)       |
  |--- 0-RTT (HTTP 请求) +      -->| 立即发送请求数据
  |     Initial (ClientHello)     |
  |<-- Initial (ServerHello) -----|
  |<-- HTTP 响应 + Handshake -----|

核心原理:QUIC 的握手结合了 TLS 1.3 的早期数据(Early Data)机制。客户端在第一次连接后缓存服务器的配置(NH、PSK),重建连接时直接用缓存的密钥加密请求数据,与服务端的握手消息同时发出。服务器可以用缓存的会话票据快速解密。

风险考虑:0-RTT 存在重放攻击风险——攻击者截获 0-RTT 请求包,可以多次向服务器发送。因此幂等的 GET/HEAD 请求适合 0-RTT,但 POST/PUT 等修改性操作通常由服务器配置是否允许。

2. 彻底消除队头阻塞

队头阻塞(Head-of-Line Blocking, HoL)是 HTTP/1.1 和 HTTP/2 的性能死穴。

HTTP/1.1 的 HoL: 一个 TCP 连接只能串行处理请求,前一个请求的响应未完成,后续请求不能发送。浏览器用 6-8 个并发连接来缓解,但这只是”用多个管子取代一根管子”。

HTTP/2 的 HoL: HTTP/2 用多路复用在单个 TCP 连接上并行传输多个流。但是 TCP 的可靠性是按字节流的——如果流 1 的一个数据包丢失了,TCP 必须等待这个包重传成功后才把后续到达的数据交给应用层。这意味着流 2、流 3 的完整数据卡在 TCP 接收缓冲区里,即便它们没有丢包。

1
2
3
4
5
6
HTTP/2 HoL (TCP 层):
  流1: [P1] [P2] [P3] ← P2 丢失
  流2: [Q1] [Q2] [Q3] ← Q1-3 已到达但 TCP 不交付
  流3: [R1] [R2] [R3] ← R1-3 已到达但 TCP 不交付
  
  → 所有流都被 P2 的重传阻塞

HTTP/3 QUIC 的解决方案:

1
2
3
4
5
6
HTTP/3 (QUIC 多流独立):
  流1: [P1] [P2(reTx)] [P3] ← P2 丢失,单独重传 P2
  流2: [Q1] [Q2] [Q3] ← 完全不受影响,持续交付
  流3: [R1] [R2] [R3] ← 完全不受影响,持续交付
  
  → 每个流独立做可靠传输

QUIC 在 UDP 之上给每个流分配独立的序列号空间。丢包只影响所属流,其他流的数据可以立即递交给应用层。这个改变对高丢包率的移动网络尤其关键——在 5% 丢包率的网络上,HTTP/2 的吞吐量可能下降 40-60%,而 HTTP/3 几乎不受影响。

3. 连接迁移

场景:用户从 Wi-Fi 切换到 5G 蜂窝网络(或从一根 Wi-Fi 漫游到另一根),IP 地址发生变化。

TCP 方案:客户端 IP 变了 → 旧 TCP 连接上的所有数据包被新网络丢弃 → 连接超时断开 → 应用层检测到断开 → 发起新的 TCP 三次握手 + TLS 握手 → 重新发送请求。整个过程 1-3 秒不可用。

QUIC 方案:客户端 NIC 地址变化 → QUIC 连接仍在,因为连接靠 CID 标识 → 发送一个包含新 IP:端口的新数据包,携带不变的 CID → 服务器回复确认 → 连接无缝迁移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 连接迁移的 Node 模拟 (简化示意)
// 数据包携带 Connection ID,而非依赖 IP:Port
class QuicPacket {
  constructor(dcid, scid, payload) {
    this.destinationConnectionId = dcid;
    this.sourceConnectionId = scid;
    this.payload = payload;
    // 注意: 没有 src_ip / src_port,只有 CID
  }
}

class QuicSession {
  constructor(serverCid) {
    this.serverCid = serverCid;
    this.clientCid = generateRandomCid(8); // 8 字节随机 CID
    this.state = 'ACTIVE';
  }

  // 即使 IP 变了,Session 状态不变
  handlePacket(packet, fromAddress) {
    if (packet.destinationConnectionId !== this.clientCid) {
      return; // 不是发给这个 session 的
    }
    // 处理数据,不关心 fromAddress
    this.currentAddress = fromAddress; // 记录新的地址
  }
}

实际测试数据:Google 的 QUIC 部署报告显示,连接迁移机制让 YouTube 视频从 Wi-Fi 切换到 5G 时的播放中断时间从平均 1200ms 降低到 80ms。

4. QPACK 头部压缩

HTTP/2 使用 HPACK 压缩头部,但 HPACK 依赖有序的压缩上下文——所有流共享一个动态表,流的交错(因为 QUIC 的乱序交付)会导致解压错误。

QPACK 专为 HTTP/3 设计,允许编码器和解码器各自维护一个”静态表”和”动态表”,关键差异是:

  1. 双向表同步:编码器(客户端/服务端)发送指令来更新解码器的表
  2. 编码器流:引入专用流(Encoder Stream)传递表更新,与请求流分离
  3. 允许乱序:请求流可以携带一个 Required Insert Count 字段,指示解码器需要先处理到哪个表状态才能解压
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// QPACK 伪代码示意 — 编码器
class QpackEncoder {
  constructor() {
    this.dynamicTable = [];  // 动态表
    this.insertCount = 0;
  }

  encode(headers) {
    const encoded = [];
    for (const [name, value] of headers) {
      // 先在静态表中查找
      const staticIdx = this.lookupStaticTable(name, value);
      if (staticIdx !== -1) {
        encoded.push(encodeStaticIndex(staticIdx));
        continue;
      }
      // 再在动态表中查找
      const dynamicIdx = this.lookupDynamicTable(name, value);
      if (dynamicIdx !== -1) {
        encoded.push(encodeDynamicIndex(dynamicIdx));
        continue;
      }
      // 都不在表中,发送字面值并插入动态表
      encoded.push(encodeLiteral(name, value));
      this.dynamicTable.push({ name, value });
      this.insertCount++;
      // 通过编码器流通知对端
      this.sendTableUpdate({ name, value });
    }
    return Buffer.concat(encoded);
  }
}

QPACK 使得 HTTP/3 头部压缩效率相比 HPACK 提升了约 8-12%,尤其在高延迟长连接上优势明显。

实战案例

案例:实时协作白板应用

假设我们要构建一个实时协作白板应用,用户在 iPad 和桌面之间频繁切换网络(Wi-Fi ↔ 5G)。传统 WebSocket over TCP 会在网络切换时断连,HTTP/3 的连接迁移特性让这一切透明处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// 基于 WebTransport (使用 HTTP/3 的新一代传输 API)
// 浏览器 WebTransport API 基于 QUIC,天然支持连接迁移

class CollaborativeWhiteboard {
  constructor(serverUrl) {
    this.serverUrl = serverUrl;
    this.transport = null;
    this.pendingActions = [];
    this.reconnectAttempts = 0;
    this.sessionState = new Map(); // session 持久化状态
  }

  async connect() {
    // WebTransport 建立 QUIC 连接
    this.transport = new WebTransport(this.serverUrl, {
      // Certificate hashes 用于自签名证书验证
      serverCertificateHashes: [this.getCertHashHash()]
    });

    await this.transport.ready;

    // 建立双向流
    this.controlStream = await this.transport.createBidirectionalStream();
    this.controlWriter = this.controlStream.writable.getWriter();
    this.readControlStream();

    // 建立单向数据流
    this.dataSendStream = await this.transport.createUnidirectionalStream();
    this.dataWriter = this.dataSendStream.writable.getWriter();

    // 监听连接状态(QUIC 连接迁移自动恢复)
    this.transport.closed.then(() => {
      console.warn('连接关闭, 尝试重建...');
      this.reconnect();
    });

    // 网络切换提示? 在 QUIC 下你几乎不需要处理!
    console.log('已通过 QUIC/HTTP/3 连接, 支持无缝网络切换');
  }

  // 发送绘图动作
  async sendDrawAction(action) {
    const msg = JSON.stringify({
      type: 'draw',
      action,
      timestamp: Date.now(),
      sessionId: this.transport.id  // QUIC 连接 ID
    });

    if (this.transport.readyState === 'connected') {
      await this.dataWriter.write(new TextEncoder().encode(msg));
    } else {
      // 连接正在恢复(网络切换时),暂存到队列
      this.pendingActions.push(msg);
    }
  }

  // 处理接收的广播数据
  async readControlStream() {
    const reader = this.controlStream.readable.getReader();
    const decoder = new TextDecoder();

    while (true) {
      try {
        const { value, done } = await reader.read();
        if (done) break;

        const data = JSON.parse(decoder.decode(value));
        switch (data.type) {
          case 'stroke':
            this.renderRemoteStroke(data.path);
            break;
          case 'cursor':
            this.renderRemoteCursor(data.userId, data.position);
            break;
          case 'undo':
            this.undoRemoteStroke(data.strokeId);
            break;
        }
      } catch (e) {
        // 网络切换期间可能读取出错,但 QUIC 会自动恢复
        console.warn('读取异常(可忽略,连接正在迁移):', e.message);
      }
    }
  }

  // 重连(处理无法自动恢复的极端情况)
  async reconnect() {
    const backoff = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    await new Promise(r => setTimeout(r, backoff));

    try {
      await this.connect();
      this.reconnectAttempts = 0;
      // 发送积压的操作
      for (const msg of this.pendingActions) {
        await this.dataWriter.write(new TextEncoder().encode(msg));
      }
      this.pendingActions = [];
    } catch (e) {
      this.reconnectAttempts++;
      this.reconnect();
    }
  }
}

// 使用示例
const canvas = document.getElementById('whiteboard');
const board = new CollaborativeWhiteboard(
  'https://whiteboard.example.com/webtransport'
);

canvas.addEventListener('pointermove', (e) => {
  board.sendDrawAction({
    type: 'line',
    points: [{ x: e.clientX, y: e.clientY }],
    color: '#ff6b6b',
    width: 3
  });
});

// 网络切换测试:在开发者工具中模拟网络切换
// 切换到飞行模式再切回,白板应用应无缝恢复

这个案例的关键价值在于:传统技术栈(WebSocket + TCP)在网络切换时,你必须实现心跳检测、断线重连、数据同步状态恢复等复杂逻辑。而 QUIC 的连接迁移从传输层解决了这个问题,应用层几乎不需要额外代码。

底层原理

QUIC 数据包格式

QUIC 数据包分为两类:长头包(Long Header,用于初始连接和握手)和短头包(Short Header,用于数据传输)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QUIC Long Header Packet (1-RTT 版本):
  +-+-+-+-+-+-+-+-+
  |1|1| 类型(2bit)|  版本特定 |  1-RTT
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                   版本 (32 bits)                       |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |  目标连接ID长度(8  | 目标连接ID (0-20 字节)            |
  |       bits)        |  ...                             |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |  源连接ID长度 (8   | 源连接ID (0-20 字节)              |
  |       bits)        |  ...                             |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                     载荷 ...                          |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

版本协商:如果客户端发送的版本服务器不支持,服务器返回版本协商包。

1
2
3
4
5
6
7
8
9
10
11
12
QUIC Short Header Packet (1-RTT 数据):
  +-+-+-+-+-+-+-+-+
  |0|1|S|K|P|L|   |  关键位:
  +-+-+-+-+-+-+-+-+  S: 自旋位(用于 RTT 测量)
  |  目标连接ID     |  K: 密钥阶段(0/1, 指示密钥更新)
  |  (0-20 字节)   |  P: 包序号长度(2位)
  +-+-+-+-+-+-+-+-+  L: 是否包含长度的标志
  |  包序号         |
  |  (编码类型)     |
  +-+-+-+-+-+-+-+-+
  |  载荷 (加密)     |
  +-+-+-+-+-+-+-+-+

QUIC 丢包恢复机制

QUIC 不依赖 TCP 的 SACK(Selective ACK),而是使用更高效的 ACK 帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// QUIC ACK 帧的伪代码表示
class AckFrame {
  constructor() {
    this.largestAcknowledged = 0;  // 最大确认包号
    this.ackDelay = 0;             // 确认延迟 (微秒)
    this.ackRanges = [];           // 确认范围列表
  }

  // 用 [start, end] 范围高效表示大块的连续确认
  // 而不是逐个包确认
}

// QUIC 的 RACK (Recent ACKnowledgment) 丢包检测算法
class QuicLossDetection {
  constructor() {
    this.packetThreshold = 3;    // 丢包阈值 (包数量)
    this.timeThreshold = 1.25;   // 时间阈值 (RTT 的倍数)
    this.minRtt = Infinity;      // 最小 RTT 观察值
    this.smoothedRtt = 0;        // 平滑 RTT
    this.rttVar = 0;             // RTT 方差
  }

  onPacketAcked(packetNumber, sendTime, ackTime) {
    const rtt = ackTime - sendTime;
    if (rtt < this.minRtt) this.minRtt = rtt;

    // 指数加权移动平均 (EWMA) 更新 RTT
    if (this.smoothedRtt === 0) {
      this.smoothedRtt = rtt;
      this.rttVar = rtt / 2;
    } else {
      this.rttVar = (3/4) * this.rttVar + (1/4) * Math.abs(this.smoothedRtt - rtt);
      this.smoothedRtt = (7/8) * this.smoothedRtt + (1/8) * rtt;
    }
  }

  detectLoss(packet) {
    // RACK: 如果收到了比包 X 更新的包,且过了足够时间
    // 还没收到 X 的确认,则判定 X 丢失
    const timeSinceSend = Date.now() - packet.sendTime;
    const rttThreshold = this.smoothedRtt + 4 * this.rttVar;
    
    return timeSinceSend > Math.max(rttThreshold, this.timeThreshold * this.minRtt);
  }
}

拥塞控制:QUIC 的新一代 BBR

传统 TCP 的拥塞控制(Cubic、Reno)依赖丢包来感知拥塞——”拥塞导致丢包,丢包才减速”。这种策略在深队列网络中效率极差。

BBR(Bottleneck Bandwidth and Round-trip propagation time)由 Google 设计,直接测量带宽和 RTT 来建模网络路径:

1
2
3
4
5
6
7
BBR 状态机:
  STARTUP → DRAIN → PROBE_BW ↔ PROBE_RTT

- STARTUP: 指数增长探测带宽上限
- DRAIN: 排出启动阶段产生的队列
- PROBE_BW: 周期性探测可用带宽变化
- PROBE_RTT: 每 10 秒探测一次最小 RTT

Google 的数据显示,BBR 在全球范围内相比 Cubic 提升了 2-4x 的吞吐量,尤其在长肥管道(long-fat pipes)上。QUIC 的 BBR 实现可以自由选择参数——不同版本的 BBR(v1/v2/v3)通过软件更新即可切换。

Chromium 中的 HTTP/3 实现

Chromium 使用自己开发的 quiche 库(QUIC in Chrome)。关键文件:

  • net/third_party/quiche/src/quiche/ - QUIC 核心实现
  • net/third_party/quiche/src/quiche/http3/ - HTTP/3 适配层
  • net/socket/ - UDP Socket 封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 简化自 Chromium 中 QUIC 会话的包处理逻辑
// source: net/third_party/quiche/src/quiche/quic/core/quic_session.cc

class QuicSession : public QuicConnectionVisitorInterface {
 public:
  // 处理到达的数据包
  void ProcessUdpPacket(const QuicSocketAddress& self_address,
                        const QuicSocketAddress& peer_address,
                        const QuicReceivedPacket& packet) override {
    // 第一步:解密数据包
    auto decrypted = decryptor_->DecryptPacket(
        packet.encryption_level(), packet.packet_number(),
        packet.AsStringPiece());
    
    if (!decrypted) {
      // 解密失败 — 可能是密钥阶段切换或无效包
      return;
    }

    // 第二步:解析 QUIC 帧
    QuicDataReader reader(*decrypted);
    while (!reader.IsDoneReading()) {
      QuicFrame frame;
      if (!QuicFrameParser::Parse(&reader, &frame)) break;
      
      switch (frame.type) {
        case STREAM:
          // 独立的流处理 — 其他流的丢包不影响本流
          ProcessStreamFrame(frame.stream_frame);
          break;
        case ACK:
          // 丢包检测和重传决策
          loss_algorithm_->OnAckFrame(frame.ack_frame);
          break;
        case CRYPTO:
          // QUIC 握手和密钥协商
          crypto_stream_->OnCryptoFrame(frame.crypto_frame);
          break;
      }
    }
  }

 private:
  std::unique_ptr<QuicDecryptor> decryptor_;
  std::unique_ptr<LossDetectionInterface> loss_algorithm_;
  std::unique_ptr<QuicCryptoStream> crypto_stream_;
  std::unordered_map<QuicStreamId, QuicStream*> streams_;
};

高频面试题解析

面试题 1:HTTP/3 为什么使用 UDP 而不是改进 TCP?

难点:这不是一个”UDP 比 TCP 快”的问题,而是协议演进的控制权问题。

答案要点

  1. 用户态协议演进:TCP 运行在操作系统内核中,任何修改都需要更新内核。TCP Fast Open(TFO)从提案到广泛部署用了 8 年以上。QUIC 完全在用户态实现,通过浏览器更新(Chrome 6 周一个版本)就可以快速迭代新特性。Cloudflare 曾在 3 天内用 QUIC 修复了一个全局性的拥塞控制 bug。

  2. TLS 融合:TCP + TLS 是分层但独立的,握手中需要两轮计算。QUIC 将 TLS 1.3 内建到传输层,握手包同时携带加密参数和传输参数,效率更高且默认全加密。

  3. 无队头阻塞:UDP 本身不做可靠传输,QUIC 在其之上为每个流独立实现可靠性,避免了 TCP 字节流模型的尾端阻塞。

  4. 连接迁移:TCP 的四元组标识导致 IP 变化即断连。QUIC 的 Connection ID 实现了连接和底层网络路径的解耦。

  5. 更灵活的多路复用:HTTP/2 多路复用受制于 TCP 的按序交付,QUIC 彻底解除了这个约束。

加分项:提及 QUIC 的 fEC(前向纠错)特性(虽然没有成为标准功能,但在 Google 的初期实验中取得了好效果),以及 QUIC v2(RFC 9369)的简化设计。

面试题 2:解释 0-RTT 的工作机制及其安全风险

答案要点

0-RTT 的核心机制:

  1. 第一次连接后,服务器发送一个 NewSessionTicket 帧,包含 PSK(Pre-Shared Key)和记录最大早期数据量
  2. 客户端缓存 PSK、服务器的传输参数和配置
  3. 重连时,客户端用 PSK 派生加密密钥,直接加密 HTTP 请求数据
  4. 0-RTT 数据包与服务端的握手包同时发出

安全风险:

  • 重放攻击:攻击者截获一个 0-RTT 数据包,可以无条件向服务器重复发送。服务器需要用重放检测机制(如 5-tuple + timestamp 的 Replay窗口)来防范。
  • 前向安全性:如果 PSK 泄露,之前所有使用该 PSK 的 0-RTT 会话数据都可以被解密。这是 0-RTT 天然的缺陷,因为数据在完全握手完成前已经发送。
  • 降级攻击:中间人可能强迫双方不使用 0-RTT,退化到完整握手。

最佳实践:仅对幂等请求使用 0-RTT(GET、HEAD),对非幂等请求禁用或仅允许在首次连接中使用。

面试题 3:为什么说 HTTP/2 的”多路复用”在实际场景中经常不如预期?HTTP/3 如何解决?

答案要点

HTTP/2 的瓶颈分析:

TCP 的”字节流有序交付”是多路复用的最大敌人。考虑一个有 3 个并发流的例子,每个流包含 10 个数据包:

场景HTTP/2 性能HTTP/3 性能
0% 丢包优(单连接减少慢启动次数)
2% 丢包下降 25-30%(一个丢包影响所有流)下降 2-5%
5% 丢包下降 50-60%(频繁重传阻塞)下降 5-10%
10% 丢包几乎不可用(重传风暴)下降 15-20%

根本原因:TCP 的接收窗口机制——数据必须按序排列后才交付给应用层。HTTP/2 多个流的帧交织存在于同一个 TCP 流中,丢失一个帧就像一个链子断了一环。

HTTP/3 的解决方案:

每个 QUIC 流独立编号(Stream ID + Offset),使用独立的序列号。一个流的丢包仅需要重传该流的帧,其他流的帧到达后直接交付:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 模拟 QUIC 多流处理
class QuicMultiplexer {
  streams = new Map();

  onPacketReceived(packet) {
    const streamId = packet.streamId;
    const offset = packet.offset;

    if (!this.streams.has(streamId)) {
      this.streams.set(streamId, new QuicStream(streamId));
    }
    this.streams.get(streamId).addData(offset, packet.data);
  }
}

class QuicStream {
  buffer = new Map();    // offset → data
  bytesRead = 0;

  addData(offset, data) {
    this.buffer.set(offset, data);
    // 即使前一个流在等重传,本流的数据可以连续递交给应用层
    this.tryDeliver();
  }

  tryDeliver() {
    // 只检查本流的数据连续性
    while (this.buffer.has(this.bytesRead)) {
      const data = this.buffer.get(this.bytesRead);
      this.deliverToApp(data);
      this.bytesRead += data.length;
      this.buffer.delete(this.bytesRead);
    }
  }
}

加分项:提及 Twitter 的实际部署数据——从 HTTP/1.1 切换到 HTTP/2 后,部分高延迟地区的请求延迟反而变差了,因为 HTTP/2 HoL 阻塞在高丢包率网络上比 HTTP/1.1 的”多个连接”更差。

总结与扩展

HTTP/3 和 QUIC 协议代表了互联网传输层的范式转变——从”内核态的单体传输层”走向”用户态的可编程传输层”。这不仅是一次版本升级,更是对 TCP 四十年前设计假设的重新审视。

核心收获

  1. QUIC 将传输层控制权从内核交还给应用开发者——拥塞控制算法、丢包恢复策略、连接管理都可以通过软件更新优化
  2. 0-RTT 和连接迁移大幅改善了移动体验——在平均丢包率 3-5% 的蜂窝网络上,HTTP/3 比 HTTP/2 快 30-50%
  3. 消除队头阻塞释放了多路复用的真正潜力——单个流丢包不影响其他流,这在直播、云游戏等场景价值巨大

未来方向

  • WebTransport:基于 QUIC 的通用双向传输 API,不只是 HTTP 请求/响应模式,支持可靠和不可靠流、面向 WebSocket 的替代
  • Multipath QUIC:同时利用 Wi-Fi + 5G 链路传输数据(正在 IETF 标准化中)
  • QUIC + MASQUE:代理协议,用于构建高效的 VPN 和保密通信隧道
  • HTTP/3 Push:服务端推送,虽在 HTTP/2 中实现,但在 HTTP/3 + QUIC 下更高效

作为前端开发者,理解 HTTP/3 能帮助你更好地设计网络传输策略——不再需要为”网络切换”写大量重连代码,不再担心多流并发时的队头阻塞,不再等待操作系统更新来获得更好的网络性能。HTTP/3 将协议的演进权交还给了每一个开发者。

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量