原文: RFC 6455, IETF, December 2011
本文从零出发,用中文详细解读协议的每一个细节,配合 C++ 示例代码与图示。

目录

  1. 背景:为什么需要 WebSocket
  2. 协议总览
  3. 握手阶段详解
  4. 数据帧结构
  5. 掩码机制
  6. 分片机制
  7. 控制帧
  8. 关闭连接
  9. 安全考量
  10. 状态码速查表
  11. 完整 C++ 演示代码

1. 背景

1.1 HTTP 轮询的痛点

在 WebSocket 出现之前,浏览器与服务器之间想实现"双向实时通信"(比如聊天、实时行情),只能靠轮询

浏览器                          服务器
  |---GET /update?t=1--->         |    // 每隔 N 秒问一次
  |<---200 (空/无新数据)---        |
  |---GET /update?t=2--->         |
  |<---200 (有新数据!)---          |
  |---POST /send-msg--->          |    // 发消息是另一条连接
  |<---200 OK---                  |

这带来三个根本问题:

  • 多路 TCP 连接: 收消息和发消息走不同连接,资源浪费。
  • HTTP 头部开销极大: 每次请求都要带几百字节的 Header,即使消息本身只有 1 个字节。
  • 客户端逻辑复杂: 要自己维护"哪个请求对应哪个回复"的映射。

1.2 WebSocket 的解法

WebSocket 用一条 TCP 连接解决所有问题:

浏览器                          服务器
  |---HTTP Upgrade 握手--->       |    // 只有这一次 HTTP
  |<---101 Switching Protocols--- |
  |===== 双向 WebSocket 帧流 ====>|    // 之后随时互发,无额外 HTTP 开销
  |<==== 双向 WebSocket 帧流 ====|

WebSocket 本质上是在 TCP 之上加了一层极简的帧格式,让浏览器能在同一个端口上同时使用 HTTP 和 WebSocket。

2. 协议总览

2.1 两个阶段

协议分为两大阶段:

阶段一: 握手 (Handshake)
  └── 用 HTTP/1.1 协议协商升级,只发生一次
阶段二: 数据传输 (Data Transfer)
  └── 双向发送 WebSocket 帧,直到关闭

2.2 URI 格式

WebSocket 定义了两种 URI Scheme:

Scheme 默认端口 是否加密 示例
ws 80 ws://example.com/chat
wss 443 是 (TLS) wss://example.com/chat

URI 语法(ABNF 形式):

ws-URI  = "ws:"  "//" host [":" port] path ["?" query]
wss-URI = "wss:" "//" host [":" port] path ["?" query]

重要: URI 中不允许出现 Fragment(# 后面的部分),如需表示 # 字符必须写成 %23

3. 握手阶段详解

3.1 客户端发起握手

客户端发送的握手本质上是一个合法的 HTTP/1.1 GET 请求,但带有特殊的头部字段:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

逐字段解析:

头部字段 是否必须 含义
GET /path HTTP/1.1 必须 方法必须是 GET,HTTP 版本至少 1.1
Host 必须 服务器主机名,防止虚拟主机混淆
Upgrade: websocket 必须 告知服务器想升级到 WebSocket
Connection: Upgrade 必须 配合 Upgrade 头使用
Sec-WebSocket-Key 必须 16 字节随机数的 Base64 编码,用于握手验证
Sec-WebSocket-Version: 13 必须 协议版本号,RFC 6455 固定为 13
Origin 浏览器必须 脚本来源,服务器可据此拒绝跨域连接
Sec-WebSocket-Protocol 可选 客户端支持的子协议列表,按优先级排列
Sec-WebSocket-Extensions 可选 客户端支持的扩展列表

3.2 Sec-WebSocket-Key 的生成规则

1. 客户端生成 16 字节的随机数(必须使用密码学强随机源)
2. 对这 16 字节做 Base64 编码
3. 得到类似 "dGhlIHNhbXBsZSBub25jZQ==" 的字符串
4. 每次连接必须重新生成,不能复用

3.3 服务端响应握手

服务端接受连接时返回:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

头部字段 含义
101 Switching Protocols 状态码必须是 101,其他任何值表示握手失败
Upgrade: websocket 确认升级到 WebSocket
Connection: Upgrade 配合 Upgrade
Sec-WebSocket-Accept 由客户端的 Key 计算而来,证明服务器真的收到了握手
Sec-WebSocket-Protocol 服务器从客户端列表中选择的一个子协议

3.4 Sec-WebSocket-Accept 的计算方法

这是握手安全验证的核心。计算步骤如下:
步骤一: 取客户端发来的 Sec-WebSocket-Key 的值(字符串形式,不做 Base64 解码),拼接上一个固定的 GUID:

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
拼接结果 = Sec-WebSocket-Key + GUID
         = "dGhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
         = "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

步骤二: 对拼接结果做 SHA-1 哈希(得到 20 字节 / 160 位):
hash = SHA-1 ( Key ∥ GUID ) \text{hash} = \text{SHA-1}(\text{Key} \| \text{GUID}) hash=SHA-1(KeyGUID)
具体数值:

SHA-1("dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
= 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6
  0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea

步骤三: 对这 20 字节做 Base64 编码:
Sec-WebSocket-Accept = Base64 ( SHA-1 ( Key ∥ GUID ) ) \text{Sec-WebSocket-Accept} = \text{Base64}(\text{SHA-1}(\text{Key} \| \text{GUID})) Sec-WebSocket-Accept=Base64(SHA-1(KeyGUID))

Base64(上面的 20 字节) = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

为什么要这样设计?
这个机制的目的不是加密,而是证明服务器确实理解 WebSocket 协议。一个普通的 HTTP 服务器或者缓存代理不会知道这个 GUID,所以无法伪造正确的 Accept 值,从而防止非 WebSocket 服务器被意外"升级"连接。

3.5 握手流程图

服务器 客户端 服务器 客户端 如果是 wss,还要做 TLS 握手 握手完成,进入 OPEN 状态 TCP 三次握手建立连接 HTTP GET + Upgrade 头 Sec-WebSocket-Key: xxxx 验证请求合法性 计算 SHA-1(Key + GUID) Base64 编码得到 Accept HTTP 101 Switching Protocols Sec-WebSocket-Accept: yyyy 验证 Accept 值正确 WebSocket 数据帧(任意时刻) WebSocket 数据帧(任意时刻)

3.6 版本协商

如果客户端请求的版本服务器不支持(比如客户端要 v25,服务器只支持 v13):

客户端 -> 服务器:
  Sec-WebSocket-Version: 25
服务器 -> 客户端:
  HTTP/1.1 400 Bad Request
  Sec-WebSocket-Version: 13, 8, 7   (服务器支持的版本列表)
客户端 -> 服务器:
  Sec-WebSocket-Version: 13         (改用服务器支持的版本重试)

4. 数据帧结构

4.1 帧格式图示

握手成功后,数据以**帧(Frame)**为单位传输。每个帧的二进制格式如下(每行 32 位):

 位偏移:
  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R|opcode |M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4b) |A|     (7b)   |          (16b 或 64b)         |
 |N|V|V|V|       |S|             |  (当 payload len == 126/127)  |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |   Extended payload length (续,当 payload len == 127 时)      |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |  Masking-key (当 MASK=1 时)   |
 +-------------------------------+-------------------------------+
 |       Masking-key (续)        |        Payload Data           |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                    Payload Data (续) ...                      :
 +---------------------------------------------------------------+

4.2 各字段详解

FIN 位(1 bit)

FIN = 1 → 这是消息的最后一帧(或唯一一帧)
FIN = 0 → 后面还有更多分片属于同一消息

RSV1, RSV2, RSV3(各 1 bit)

正常情况下必须全为 0。
只有协商了扩展(Extension)之后,扩展协议才可以使用这些位。
如果收到非零值但没有对应扩展 → 必须断开连接。

Opcode(4 bits)

操作码 含义 类型
0x0 延续帧(Continuation Frame) 数据帧
0x1 文本帧(UTF-8 编码) 数据帧
0x2 二进制帧 数据帧
0x3~0x7 保留(未来非控制帧)
0x8 关闭连接(Close) 控制帧
0x9 Ping 控制帧
0xA Pong 控制帧
0xB~0xF 保留(未来控制帧)

区分规则:操作码最高位为 1 → 控制帧;为 0 → 数据帧
MASK 位(1 bit)

客户端 → 服务器: 必须为 1(所有帧都要掩码)
服务器 → 客户端: 必须为 0(服务器不能对发出的帧掩码)

Payload Length(7 bit + 可选扩展)
这是一个变长编码,设计很巧妙:

如果 7 位值为 0~125  →  直接就是载荷长度,单位字节
如果 7 位值为 126    →  后续 2 字节(16位无符号整数)才是真正的长度
如果 7 位值为 127    →  后续 8 字节(64位无符号整数,最高位必须为0)才是真正的长度

用数学表达:
L = { x 若  x ∈ [ 0 , 125 ] uint16 从后 2 字节读取 若  x = 126 uint64 从后 8 字节读取 若  x = 127 L = \begin{cases} x & \text{若 } x \in [0, 125] \\ \text{uint16 从后 2 字节读取} & \text{若 } x = 126 \\ \text{uint64 从后 8 字节读取} & \text{若 } x = 127 \end{cases} L= xuint16 从后 2 字节读取uint64 从后 8 字节读取 x[0,125] x=126 x=127
另外规定:必须使用最短编码,比如长度 = 100,只能用单字节 100 表示,不能用 126 + 两字节 100。
Masking-Key(0 或 4 字节)
只在 MASK = 1 时存在,是一个 32 位随机值,用于对载荷做异或掩码。
Payload Data

Payload Data = Extension Data(扩展数据,长度由扩展协议决定)
             + Application Data(应用数据,填满剩余空间)

5. 客户端到服务端的掩码机制

5.1 为什么要掩码?

这是为了防止缓存投毒攻击(Cache Poisoning Attack)
攻击场景(不使用掩码时):

攻击者控制一个恶意网站
  │
  ├── 1. 诱导用户访问恶意网站
  ├── 2. 恶意脚本建立 WebSocket 连接到攻击者服务器
  ├── 3. 发送的 WebSocket 数据经过精心构造,
  │      在网络中间的透明代理看来像是一个合法的 HTTP GET 请求
  └── 4. 代理把这个"请求"的"响应"缓存下来
         其他用户访问该资源时,返回的是攻击者注入的内容

掩码后,攻击者无法控制数据在网络上的实际字节序列(因为掩码密钥是客户端随机生成的),所以无法构造"看起来像 HTTP 请求"的字节流。

5.2 掩码算法

掩码和解掩码使用完全相同的算法(XOR 的对合性:做两次等于没做)。
设:

  • 原始载荷的第 i i i 个字节为 o i o_i oi
  • 掩码密钥为 4 字节 [ k 0 , k 1 , k 2 , k 3 ] [k_0, k_1, k_2, k_3] [k0,k1,k2,k3]
  • 变换后的第 i i i 个字节为 t i t_i ti
    则:
    j = i   m o d   4 j = i \bmod 4 j=imod4
    t i = o i ⊕ k j t_i = o_i \oplus k_j ti=oikj
    其中 ⊕ \oplus 表示按位异或(XOR)。
    图示:
载荷字节: [ B0  B1  B2  B3  B4  B5  B6  B7  ... ]
掩码循环: [ K0  K1  K2  K3  K0  K1  K2  K3  ... ]
          ↓XOR ↓XOR ↓XOR ↓XOR ↓XOR ↓XOR ↓XOR ↓XOR
结果:     [ T0  T1  T2  T3  T4  T5  T6  T7  ... ]

6. 分片机制

6.1 为什么需要分片?

分片(Fragmentation)解决两个问题:
问题一:如果消息很大,必须先知道总长度才能填 Payload Length 字段。有了分片,可以边生成边发送,无需预先缓冲整个消息。
问题二:在多路复用场景中,一个超大消息不应该独占信道,可以分片后与其他消息的分片交替发送。

6.2 分片规则

未分片的消息(整个消息就一帧):

FIN=1, opcode=0x1(文本) 或 0x2(二进制)

分片消息的格式

第一帧:  FIN=0, opcode=0x1 或 0x2  (opcode 标明消息类型)
中间帧:  FIN=0, opcode=0x0          (延续帧,opcode=0)
最后帧:  FIN=1, opcode=0x0          (延续帧,FIN=1 表示结束)

以发送 “Hello” 分为 “Hel” + “lo” 两片为例:

帧1: FIN=0, opcode=0x1(Text), payload="Hel"   → 字节: 0x01 0x03 0x48 0x65 0x6c
帧2: FIN=1, opcode=0x0(Cont), payload="lo"    → 字节: 0x80 0x02 0x6c 0x6f

6.3 控制帧可以插队

分片消息传输过程中,控制帧(Close/Ping/Pong)可以插入到分片之间,但控制帧本身不能被分片

帧1 (数据片1, FIN=0) → Ping 控制帧 → 帧2 (数据片2, FIN=1)

这确保了 Ping/Pong 心跳的延迟不会被大消息阻塞。

7. 控制帧

控制帧有三种,都有一个共同约束:载荷长度不超过 125 字节,且不能被分片

7.1 Close 帧(opcode = 0x8)

用于发起关闭握手。载荷是可选的:

载荷格式(可选):
  [0~1 字节]: 状态码,2字节无符号整数,大端序
  [2~N 字节]: UTF-8 编码的关闭原因文本(可选)

关闭握手流程:

端A                           端B
 |---Close Frame (code=1000)-->|
 |<---Close Frame (code=1000)--|   (回应 Close,一般反射同一状态码)
 |                             |
 |<====TCP FIN/ACK 四次挥手====>|

重要规则:

  • 收到 Close 帧后,如果自己还没发过 Close,必须立即回一个 Close。
  • 发出 Close 帧后,不能再发任何数据帧。
  • 双方 Close 帧都发出并收到后,服务器先关闭 TCP,客户端等待服务器关闭。

7.2 Ping 帧(opcode = 0x9)

用于检测对端是否还在线(心跳)。

发送方 ---Ping (可携带任意数据)--> 接收方
       <---Pong (必须原样返回 Ping 的数据)---

规则:

  • 收到 Ping 必须尽快回 Pong(除非已经在关闭握手中)。
  • 如果积压了多个未回复的 Ping,只需回复最新一个。

7.3 Pong 帧(opcode = 0xA)

Pong 也可以主动发送(不作为 Ping 的回应),作为单向心跳。对方不需要对主动 Pong 做任何响应。

8. 关闭连接

8.1 状态机

WebSocket 连接在生命周期中经历以下状态:

开始建立连接

握手成功 (101)

握手失败

任一方发送 Close 帧

TCP 连接关闭

CONNECTING

OPEN

CLOSED

CLOSING

8.2 关闭状态码完整列表


状态码 名称 说明
1000 Normal Closure 正常关闭,任务已完成
1001 Going Away 服务器关机或浏览器跳转离开
1002 Protocol Error 协议错误
1003 Unsupported Data 收到了无法接受的数据类型(如只处理文本却收到二进制)
1004 保留 未来使用
1005 No Status Rcvd 保留值,不能由端点设置;表示没有状态码
1006 Abnormal Closure 保留值,不能由端点设置;表示连接异常断开(没有 Close 帧)
1007 Invalid Payload 消息类型不一致(如文本帧包含非 UTF-8 数据)
1008 Policy Violation 违反策略,通用代码
1009 Message Too Big 消息太大无法处理
1010 Mandatory Ext. 客户端要求的扩展服务器未返回
1011 Internal Error 服务器遇到意外错误
1015 TLS Handshake 保留值,TLS 握手失败时由库层使用
3000~3999 库/框架使用 需要向 IANA 注册
4000~4999 私有使用 应用自定义,无需注册

8.3 异常关闭与重连退避

当连接异常断开时,客户端不应立即重连,否则大量客户端同时重连会对服务器造成 DDoS。
推荐策略(指数退避):
delay ( n ) = min ⁡ ( cap , base × 2 n + rand ( 0 , jitter ) ) \text{delay}(n) = \min\left(\text{cap}, \text{base} \times 2^n + \text{rand}(0, \text{jitter})\right) delay(n)=min(cap,base×2n+rand(0,jitter))

  • 第一次重连:随机等待 0~5 秒
  • 后续失败:使用截断指数退避(Truncated Binary Exponential Backoff)

9. 安全考量

9.1 Origin 头部与跨域防护

浏览器在发起 WebSocket 连接时会自动附带 Origin 头。服务器可以据此拒绝来自非预期来源的连接:

Origin: http://evil.com   → 服务器返回 HTTP 403 Forbidden
Origin: http://myapp.com  → 服务器允许连接

非浏览器客户端(如 curl、自定义程序)可以伪造 Origin,因此 Origin 只是一层浏览器安全机制,不能作为唯一的鉴权手段。

9.2 TLS 保护

使用 wss:// 时,WebSocket 运行在 TLS 之上,提供:

  • 机密性:数据加密传输
  • 完整性:防止数据被篡改
  • 端点认证:通过证书验证服务器身份

9.3 数据验证

  • 文本帧的载荷必须是合法的 UTF-8 编码,否则必须断开连接。
  • 实现必须对帧大小和消息总大小做限制,防止内存耗尽攻击。

10. 状态码速查表

参见第 8.2 节。

11. 完整 C++ 演示代码

下面的代码演示了 WebSocket 握手验证中 Sec-WebSocket-Accept 的计算过程,以及帧的基本编解码。代码使用标准 C++17,依赖 OpenSSL 提供 SHA-1。

// websocket_demo.cpp
// 演示 WebSocket RFC 6455 核心机制:
//   1. 计算 Sec-WebSocket-Accept
//   2. 构造和解析 WebSocket 帧(不含网络 I/O)
//
// 编译(需要 OpenSSL):
//   g++ -std=c++17 -o websocket_demo websocket_demo.cpp -lssl -lcrypto
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <random>
#include <iomanip>
#include <sstream>
// OpenSSL 提供 SHA-1 和 Base64
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
// ============================================================
// 工具函数:Base64 编码 / 解码
// ============================================================
// 将字节数组进行 Base64 编码,返回 std::string
std::string base64Encode(const unsigned char* data, size_t len)
{
    // 使用 OpenSSL 的 BIO 链来完成 Base64 编码
    BIO* b64 = BIO_new(BIO_f_base64());
    BIO* mem = BIO_new(BIO_s_mem());
    // 不插入换行符(RFC 6455 要求单行 Base64)
    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
    BIO_push(b64, mem);
    BIO_write(b64, data, static_cast<int>(len));
    BIO_flush(b64);
    BUF_MEM* bufMem = nullptr;
    BIO_get_mem_ptr(mem, &bufMem);
    std::string result(bufMem->data, bufMem->length);
    BIO_free_all(b64);
    return result;
}
// 将 Base64 字符串解码为字节数组
std::vector<unsigned char> base64Decode(const std::string& encoded)
{
    BIO* b64 = BIO_new(BIO_f_base64());
    BIO* mem = BIO_new_mem_buf(encoded.c_str(), static_cast<int>(encoded.size()));
    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
    BIO_push(b64, mem);
    std::vector<unsigned char> buf(encoded.size()); // 足够大
    int readLen = BIO_read(b64, buf.data(), static_cast<int>(buf.size()));
    BIO_free_all(b64);
    if (readLen < 0) readLen = 0;
    buf.resize(static_cast<size_t>(readLen));
    return buf;
}
// ============================================================
// Section 3.4 —— 计算 Sec-WebSocket-Accept
// ============================================================
// RFC 6455 规定的固定 GUID
static const std::string WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
// 输入: 客户端发来的 Sec-WebSocket-Key(已经是 Base64 字符串)
// 输出: 服务器应该返回的 Sec-WebSocket-Accept 值
std::string computeWebSocketAccept(const std::string& secWebSocketKey)
{
    // Step 1: 拼接 Key + GUID(字符串拼接,不对 Key 做 Base64 解码)
    std::string concatenated = secWebSocketKey + WEBSOCKET_GUID;
    // Step 2: SHA-1 哈希
    // SHA_DIGEST_LENGTH = 20(SHA-1 输出 160 位 = 20 字节)
    unsigned char sha1Hash[SHA_DIGEST_LENGTH];
    SHA1(reinterpret_cast<const unsigned char*>(concatenated.c_str()),
         concatenated.size(),
         sha1Hash);
    // Step 3: Base64 编码
    return base64Encode(sha1Hash, SHA_DIGEST_LENGTH);
}
// 生成客户端用的随机 Sec-WebSocket-Key(16 字节随机数的 Base64)
std::string generateWebSocketKey()
{
    // 使用密码学安全的随机数生成器
    std::random_device rd;
    std::mt19937_64 gen(rd());
    std::uniform_int_distribution<uint8_t> dist(0, 255);
    unsigned char randomBytes[16];
    for (auto& b : randomBytes) {
        b = dist(gen);
    }
    return base64Encode(randomBytes, 16);
}
// ============================================================
// Section 5.2 —— WebSocket 帧编解码
// ============================================================
// 操作码枚举(对应 RFC 6455 Section 5.2 的 opcode 字段)
enum class Opcode : uint8_t {
    Continuation = 0x0,
    Text         = 0x1,
    Binary       = 0x2,
    Close        = 0x8,
    Ping         = 0x9,
    Pong         = 0xA
};
// WebSocket 帧结构体
struct WebSocketFrame {
    bool        fin     = true;    // FIN 位:是否是最后一帧
    bool        masked  = false;   // MASK 位:客户端发的帧必须为 true
    Opcode      opcode  = Opcode::Text;
    uint8_t     maskKey[4] = {0,0,0,0}; // 掩码密钥(仅 masked=true 时有效)
    std::vector<uint8_t> payload;        // 原始载荷(解掩码后)
};
// 将 WebSocket 帧编码为字节流(用于发送)
// masked=true 时会自动生成随机掩码密钥并应用掩码
std::vector<uint8_t> encodeFrame(const WebSocketFrame& frame)
{
    std::vector<uint8_t> output;
    // ---- 字节 0: FIN(1) + RSV(3) + Opcode(4) ----
    uint8_t byte0 = 0;
    if (frame.fin) {
        byte0 |= 0x80; // 最高位置 1 = FIN
    }
    // RSV1/2/3 全为 0(不使用扩展)
    byte0 |= static_cast<uint8_t>(frame.opcode) & 0x0F; // 低 4 位 = opcode
    output.push_back(byte0);
    // ---- 字节 1+: MASK(1) + Payload Length(7+) ----
    size_t payloadLen = frame.payload.size();
    uint8_t byte1 = frame.masked ? 0x80 : 0x00; // 最高位 = MASK 标志
    if (payloadLen <= 125) {
        // 7 位直接表示长度
        byte1 |= static_cast<uint8_t>(payloadLen);
        output.push_back(byte1);
    } else if (payloadLen <= 65535) {
        // 7 位为 126,后跟 2 字节大端序长度
        byte1 |= 126;
        output.push_back(byte1);
        output.push_back(static_cast<uint8_t>((payloadLen >> 8) & 0xFF));
        output.push_back(static_cast<uint8_t>( payloadLen       & 0xFF));
    } else {
        // 7 位为 127,后跟 8 字节大端序长度
        byte1 |= 127;
        output.push_back(byte1);
        for (int i = 7; i >= 0; --i) {
            output.push_back(static_cast<uint8_t>((payloadLen >> (i * 8)) & 0xFF));
        }
    }
    // ---- 掩码密钥(4 字节,仅 masked=true 时存在) ----
    uint8_t actualMaskKey[4];
    if (frame.masked) {
        // 生成随机掩码密钥
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<uint8_t> dist(0, 255);
        for (auto& k : actualMaskKey) k = dist(gen);
        output.push_back(actualMaskKey[0]);
        output.push_back(actualMaskKey[1]);
        output.push_back(actualMaskKey[2]);
        output.push_back(actualMaskKey[3]);
    }
    // ---- 载荷数据(应用掩码后写入) ----
    for (size_t i = 0; i < payloadLen; ++i) {
        if (frame.masked) {
            // 掩码算法: t_i = o_i XOR k_{i mod 4}
            output.push_back(frame.payload[i] ^ actualMaskKey[i % 4]);
        } else {
            output.push_back(frame.payload[i]);
        }
    }
    return output;
}
// 从字节流中解析一个 WebSocket 帧
// 返回解析成功的帧,并通过 bytesConsumed 告知消耗了多少字节
// 如果字节流不足以解析完整帧,抛出 std::runtime_error
WebSocketFrame decodeFrame(const std::vector<uint8_t>& data, size_t& bytesConsumed)
{
    if (data.size() < 2) {
        throw std::runtime_error("数据不足:至少需要 2 字节");
    }
    WebSocketFrame frame;
    size_t pos = 0;
    // ---- 字节 0: FIN + RSV + Opcode ----
    uint8_t byte0 = data[pos++];
    frame.fin    = (byte0 & 0x80) != 0;
    // RSV1/2/3 对应 bit 6/5/4,忽略(假设无扩展)
    frame.opcode = static_cast<Opcode>(byte0 & 0x0F);
    // ---- 字节 1: MASK + Payload Length ----
    uint8_t byte1 = data[pos++];
    frame.masked = (byte1 & 0x80) != 0;
    uint64_t payloadLen = byte1 & 0x7F;
    // 处理扩展长度
    if (payloadLen == 126) {
        if (data.size() < pos + 2) throw std::runtime_error("数据不足:16 位扩展长度");
        payloadLen  = static_cast<uint64_t>(data[pos++]) << 8;
        payloadLen |= static_cast<uint64_t>(data[pos++]);
    } else if (payloadLen == 127) {
        if (data.size() < pos + 8) throw std::runtime_error("数据不足:64 位扩展长度");
        payloadLen = 0;
        for (int i = 0; i < 8; ++i) {
            payloadLen = (payloadLen << 8) | static_cast<uint64_t>(data[pos++]);
        }
    }
    // ---- 掩码密钥(4 字节) ----
    if (frame.masked) {
        if (data.size() < pos + 4) throw std::runtime_error("数据不足:掩码密钥");
        for (int i = 0; i < 4; ++i) {
            frame.maskKey[i] = data[pos++];
        }
    }
    // ---- 载荷数据 ----
    if (data.size() < pos + payloadLen) throw std::runtime_error("数据不足:载荷");
    frame.payload.resize(payloadLen);
    for (uint64_t i = 0; i < payloadLen; ++i) {
        if (frame.masked) {
            // 解掩码:与掩码算法相同,XOR 是自逆的
            frame.payload[i] = data[pos++] ^ frame.maskKey[i % 4];
        } else {
            frame.payload[i] = data[pos++];
        }
    }
    bytesConsumed = pos;
    return frame;
}
// ============================================================
// 辅助:将字节数组打印为十六进制
// ============================================================
void printHex(const std::string& label, const std::vector<uint8_t>& bytes)
{
    std::cout << label << " [" << bytes.size() << " 字节]: ";
    for (auto b : bytes) {
        std::cout << std::hex << std::setw(2) << std::setfill('0')
                  << static_cast<int>(b) << " ";
    }
    std::cout << std::dec << "\n";
}
// ============================================================
// 主函数:演示握手验证 + 帧编解码
// ============================================================
int main()
{
    std::cout << "=== WebSocket RFC 6455 核心机制演示 ===\n\n";
    // ---- 演示 1:计算 Sec-WebSocket-Accept ----
    std::cout << "--- 演示 1: 握手 Accept 值计算 ---\n";
    // RFC 6455 文档中的标准测试向量
    std::string testKey = "dGhlIHNhbXBsZSBub25jZQ==";
    std::string expected = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
    std::string computed = computeWebSocketAccept(testKey);
    std::cout << "Sec-WebSocket-Key    : " << testKey   << "\n";
    std::cout << "期望 Accept 值        : " << expected  << "\n";
    std::cout << "计算得到的 Accept 值  : " << computed  << "\n";
    std::cout << "验证" << (computed == expected ? "通过" : "失败") << "\n\n";
    // 生成一个新的随机 Key 演示
    std::string newKey = generateWebSocketKey();
    std::string newAccept = computeWebSocketAccept(newKey);
    std::cout << "新随机 Key           : " << newKey    << "\n";
    std::cout << "对应的 Accept 值     : " << newAccept << "\n\n";
    // ---- 演示 2:构造并编码一个文本帧(不带掩码,模拟服务器发送) ----
    std::cout << "--- 演示 2: 构造文本帧(服务器→客户端,无掩码) ---\n";
    WebSocketFrame textFrame;
    textFrame.fin     = true;
    textFrame.masked  = false;    // 服务器发出的帧不掩码
    textFrame.opcode  = Opcode::Text;
    std::string msg   = "Hello";
    textFrame.payload.assign(msg.begin(), msg.end());
    std::vector<uint8_t> encodedText = encodeFrame(textFrame);
    printHex("编码后的帧字节", encodedText);
    // RFC 6455 Section 5.7 给出的标准示例:
    // 单帧未掩码文本 "Hello" → 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f
    std::cout << "RFC 标准答案: 81 05 48 65 6c 6c 6f\n\n";
    // ---- 演示 3:解码刚才编码的帧 ----
    std::cout << "--- 演示 3: 解码文本帧 ---\n";
    size_t consumed = 0;
    WebSocketFrame decoded = decodeFrame(encodedText, consumed);
    std::cout << "FIN    : " << (decoded.fin    ? "true" : "false") << "\n";
    std::cout << "Masked : " << (decoded.masked ? "true" : "false") << "\n";
    std::cout << "Opcode : 0x" << std::hex << static_cast<int>(decoded.opcode) << std::dec << "\n";
    std::cout << "载荷   : " << std::string(decoded.payload.begin(), decoded.payload.end()) << "\n";
    std::cout << "消耗字节: " << consumed << "\n\n";
    // ---- 演示 4:构造带掩码的帧(模拟客户端发送) ----
    std::cout << "--- 演示 4: 构造带掩码的帧(客户端→服务器) ---\n";
    WebSocketFrame maskedFrame;
    maskedFrame.fin    = true;
    maskedFrame.masked = true;    // 客户端发出的帧必须掩码
    maskedFrame.opcode = Opcode::Text;
    std::string clientMsg = "Hi Server";
    maskedFrame.payload.assign(clientMsg.begin(), clientMsg.end());
    std::vector<uint8_t> encodedMasked = encodeFrame(maskedFrame);
    printHex("带掩码的帧字节", encodedMasked);
    // 解码(会自动去掩码)
    size_t consumed2 = 0;
    WebSocketFrame decoded2 = decodeFrame(encodedMasked, consumed2);
    std::cout << "解掩码后的载荷: "
              << std::string(decoded2.payload.begin(), decoded2.payload.end()) << "\n\n";
    // ---- 演示 5:构造 Ping 控制帧 ----
    std::cout << "--- 演示 5: 构造 Ping 控制帧 ---\n";
    WebSocketFrame pingFrame;
    pingFrame.fin    = true;
    pingFrame.masked = false;
    pingFrame.opcode = Opcode::Ping;
    std::string pingData = "Hello";
    pingFrame.payload.assign(pingData.begin(), pingData.end());
    std::vector<uint8_t> encodedPing = encodeFrame(pingFrame);
    printHex("Ping 帧字节", encodedPing);
    // RFC 标准: 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f
    std::cout << "RFC 标准答案: 89 05 48 65 6c 6c 6f\n\n";
    // ---- 演示 6:构造 Close 帧(状态码 1000 = 正常关闭) ----
    std::cout << "--- 演示 6: 构造 Close 帧(状态码 1000) ---\n";
    WebSocketFrame closeFrame;
    closeFrame.fin    = true;
    closeFrame.masked = false;
    closeFrame.opcode = Opcode::Close;
    // Close 帧载荷前 2 字节是大端序状态码
    uint16_t closeCode = 1000;
    closeFrame.payload.push_back(static_cast<uint8_t>((closeCode >> 8) & 0xFF));
    closeFrame.payload.push_back(static_cast<uint8_t>( closeCode       & 0xFF));
    // 可选:附加关闭原因文本
    std::string closeReason = "Normal closure";
    closeFrame.payload.insert(closeFrame.payload.end(), closeReason.begin(), closeReason.end());
    std::vector<uint8_t> encodedClose = encodeFrame(closeFrame);
    printHex("Close 帧字节", encodedClose);
    // 解析状态码
    size_t consumed3 = 0;
    WebSocketFrame decodedClose = decodeFrame(encodedClose, consumed3);
    if (decodedClose.payload.size() >= 2) {
        uint16_t statusCode = (static_cast<uint16_t>(decodedClose.payload[0]) << 8)
                            |  static_cast<uint16_t>(decodedClose.payload[1]);
        std::string reason(decodedClose.payload.begin() + 2, decodedClose.payload.end());
        std::cout << "解析状态码: " << statusCode << "\n";
        std::cout << "关闭原因  : " << reason << "\n";
    }
    std::cout << "\n=== 演示完毕 ===\n";
    return 0;
}

11.1 编译与运行

# Ubuntu/Debian 安装 OpenSSL 开发库
sudo apt-get install libssl-dev
# 编译
g++ -std=c++17 -o websocket_demo websocket_demo.cpp -lssl -lcrypto
# 运行
./websocket_demo

11.2 预期输出

=== WebSocket RFC 6455 核心机制演示 ===
--- 演示 1: 握手 Accept 值计算 ---
Sec-WebSocket-Key    : dGhlIHNhbXBsZSBub25jZQ==
期望 Accept 值        : s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
计算得到的 Accept 值  : s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
验证通过
新随机 Key           : (每次随机,如 rE2eDsX0LhSHfPEA8hvHjg==)
对应的 Accept 值     : (对应计算结果)
--- 演示 2: 构造文本帧(服务器→客户端,无掩码) ---
编码后的帧字节 [7 字节]: 81 05 48 65 6c 6c 6f
RFC 标准答案: 81 05 48 65 6c 6c 6f
--- 演示 3: 解码文本帧 ---
FIN    : true
Masked : false
Opcode : 0x1
载荷   : Hello
消耗字节: 7
--- 演示 4: 构造带掩码的帧(客户端→服务器) ---
编码后的帧字节 [15 字节]: 81 89 xx xx xx xx (掩码后数据随机变化)
解掩码后的载荷: Hi Server
--- 演示 5: 构造 Ping 控制帧 ---
Ping 帧字节 [7 字节]: 89 05 48 65 6c 6c 6f
RFC 标准答案: 89 05 48 65 6c 6c 6f
--- 演示 6: 构造 Close 帧(状态码 1000) ---
Close 帧字节 [18 字节]: 88 10 03 e8 4e 6f 72 ...
解析状态码: 1000
关闭原因  : Normal closure
=== 演示完毕 ===

附录:帧字节解读示例

以 RFC 6455 Section 5.7 的例子为例,逐字节解读 "Hello" 文本帧:

字节流: 0x81  0x05  0x48  0x65  0x6c  0x6c  0x6f
0x81 = 1000 0001
       ↑         → FIN = 1(最后一帧)
        ↑↑↑      → RSV1=RSV2=RSV3 = 0
            ↑↑↑↑ → Opcode = 0001 = 0x1(文本帧)
0x05 = 0000 0101
       ↑         → MASK = 0(无掩码,服务器发出)
        ↑↑↑↑↑↑↑ → Payload Length = 5
0x48 = 'H'
0x65 = 'e'
0x6c = 'l'
0x6c = 'l'
0x6f = 'o'

TLS 1.3 协议 RFC 8446 — 从零理解完整指南

原文: RFC 8446, E. Rescorla, Mozilla, August 2018
本文从零出发,用中文详细解读 TLS 1.3 协议的每一个关键机制,配合图示与 C++ 示例代码。

目录

  1. TLS 是什么,解决什么问题
  2. TLS 1.3 相比 1.2 的重大改动
  3. 协议总览:三种握手模式
  4. 完整握手流程详解
  5. PSK 会话恢复
  6. 0-RTT 早期数据
  7. 记录层协议
  8. 密钥调度系统
  9. Alert 警告协议
  10. 安全性分析
  11. 完整 C++ 演示代码

1. TLS 是什么

TLS(Transport Layer Security,传输层安全)的目标是在两个通信端之间建立一条安全信道,提供三个核心保证:
认证(Authentication):服务器端必须被认证(客户端可选)。可以用非对称密钥(RSA、ECDSA、EdDSA)或对称预共享密钥(PSK)来完成。
保密性(Confidentiality):握手完成后信道上传输的数据,只有通信双方能看到。TLS 不隐藏数据长度,但提供填充机制来混淆长度信息。
完整性(Integrity):数据在传输过程中一旦被篡改,接收方必须能检测出来。
即使攻击者对网络拥有完全控制权(能监听、截获、篡改所有数据包),上述三个属性也必须成立。
TLS 由两个子协议组成:

TLS 协议
├── 握手协议 (Handshake Protocol)
│     认证双方身份,协商密码算法,建立共享密钥材料
│
└── 记录协议 (Record Protocol)
      用握手协议建立的参数,对应用数据进行加密保护

2. TLS 1.3 相比 1.2 的重大改动


改动项 TLS 1.2 TLS 1.3
加密算法 包含 RC4、CBC 等遗留算法 全部为 AEAD 算法
握手往返 2-RTT 1-RTT(可降至 0-RTT)
密钥交换 支持静态 RSA 全部提供前向保密(仅 (EC)DHE)
握手加密 ServerHello 之后明文传输 ServerHello 之后全部加密
密钥推导 PRF(伪随机函数) HKDF(基于 HMAC 的密钥推导)
版本协商 用 version 字段 用 supported_versions 扩展
会话恢复 session ID / session ticket 统一为 PSK 机制
椭圆曲线 需要额外扩展 内置支持(含 EdDSA)
ChangeCipherSpec 真实语义 仅为兼容中间件,可忽略

3. 协议总览

TLS 1.3 支持三种密钥交换模式:

(EC)DHE          — 椭圆曲线或有限域 Diffie-Hellman
PSK-only         — 纯预共享密钥(无前向保密)
PSK + (EC)DHE    — 预共享密钥 + DHE(有前向保密)

握手分三个阶段:

阶段一: 密钥交换 (Key Exchange)
  └── 建立共享密钥材料,选定密码参数
      此阶段结束后所有消息全部加密
阶段二: 服务器参数 (Server Parameters)
  └── 确定其他握手参数(客户端认证方式、ALPN等)
阶段三: 认证 (Authentication)
  └── 认证服务器(可选认证客户端),提供密钥确认和握手完整性

4. 完整握手流程详解

4.1 标准 1-RTT 握手

客户端                                        服务器
  |                                              |
  |----ClientHello----------------------->       |  密钥交换阶段开始
  |    + key_share (ECDHE 公钥)                  |
  |    + supported_versions ([TLS1.3])           |
  |    + signature_algorithms                    |
  |    + cipher_suites                           |
  |                                              |
  |       <----ServerHello-------------------    |  服务器选定参数
  |            + key_share (服务器 ECDHE 公钥)   |
  |            + supported_versions (TLS1.3)     |
  |                                              |
  |  [此后所有消息均已加密]                       |
  |                                              |
  |       <----EncryptedExtensions-----------    |  服务器参数阶段
  |       <----Certificate-------------------    |  认证阶段:发证书
  |       <----CertificateVerify-------------    |  认证阶段:签名证明
  |       <----Finished----------------------    |  认证阶段:MAC确认
  |                                              |
  |----Certificate (如果服务器要求)-------->      |
  |----CertificateVerify (如果有证书)------>      |
  |----Finished---------------------------->      |
  |                                              |
  |<===== 应用数据(双向加密传输)============>   |

4.2 ClientHello 消息详解

ClientHello 是客户端发的第一条 TLS 消息,结构如下:

struct {
    ProtocolVersion legacy_version = 0x0303;  // 固定填 TLS 1.2(0x0303)
                                               // 向后兼容,真实版本在扩展里
    Random random;                             // 32 字节密码学安全随机数
    opaque legacy_session_id<0..32>;           // 兼容旧版,TLS 1.3 一般填随机值
    CipherSuite cipher_suites<2..2^16-2>;      // 客户端支持的密码套件列表
    opaque legacy_compression_methods<1..2^8-1>; // 必须只有 0x00(不压缩)
    Extension extensions<8..2^16-1>;           // 扩展列表(TLS 1.3 核心信息在这里)
} ClientHello;

为什么 legacy_version 填 0x0303(TLS 1.2)?
历史原因:很多老旧的网络中间件(防火墙、代理)一看到高版本号就拒绝连接。所以 TLS 1.3 把真实版本号藏在 supported_versions 扩展里,外面伪装成 TLS 1.2。
重要扩展字段一览:

扩展名 用途
supported_versions 告知服务器客户端真正支持的 TLS 版本(必须包含 0x0304)
key_share 携带客户端的 ECDHE/DHE 公钥(用于密钥交换)
signature_algorithms 客户端能验证的签名算法列表
supported_groups 客户端支持的椭圆曲线/DH 参数组
pre_shared_key 携带 PSK 标识(会话恢复时使用)
psk_key_exchange_modes 指定 PSK 结合 DHE 还是纯 PSK
early_data 声明要发送 0-RTT 数据

4.3 ServerHello 消息详解

服务器从客户端提供的选项中选择一套参数,回复 ServerHello:

struct {
    ProtocolVersion legacy_version = 0x0303;  // 同样伪装成 TLS 1.2
    Random random;                             // 32 字节随机数(含降级保护信息)
    opaque legacy_session_id_echo<0..32>;      // 原样回显客户端的 session_id
    CipherSuite cipher_suite;                  // 服务器选择的密码套件(单个)
    uint8 legacy_compression_method = 0;       // 固定为 0
    Extension extensions<6..2^16-1>;           // 包含 supported_versions + key_share
} ServerHello;

降级攻击保护机制
如果服务器支持 TLS 1.3 但被迫(被中间人攻击)降级到 TLS 1.2,它必须在 ServerHello.random 的最后 8 字节写入特殊标志:

降级到 TLS 1.2: 最后 8 字节 = 44 4F 57 4E 47 52 44 01  (DOWNGRD\x01)
降级到 TLS 1.1: 最后 8 字节 = 44 4F 57 4E 47 52 44 00  (DOWNGRD\x00)

客户端收到后,如果发现这个标志,立即报 illegal_parameter 错误并断开连接。

4.4 HelloRetryRequest(参数不匹配时)

如果客户端提供的 key_share 组与服务器偏好不符,服务器发送 HelloRetryRequest 让客户端重试:

服务器 客户端 服务器 客户端 服务器只支持 P-256 X25519 不在支持列表里 用 P-256 重新生成密钥对 后续握手正常进行 ClientHello + key_share(X25519) HelloRetryRequest + selected_group(P-256) ClientHello(2) + key_share(P-256) ServerHello + key_share(P-256)

HelloRetryRequest 在协议线上的格式与 ServerHello 完全相同,区别是 Random 字段被设置为一个固定的特殊值(SHA-256("HelloRetryRequest") 的结果):

CF 21 AD 74 E5 9A 61 11 BE 1D 8C 02 1E 65 B8 91
C2 A2 11 16 7A BB 8C 5E 07 9E 09 E2 C8 A8 33 9C

4.5 Certificate 与 CertificateVerify

Certificate 消息:携带服务器的证书链(X.509 格式),供客户端验证服务器身份。
CertificateVerify 消息:证明服务器持有证书对应的私钥。签名内容是:
CertVerify_input = 0x20 × 64 ⏟ 64 个空格字节 ∥ "TLS   1.3,   server   CertificateVerify" ⏟ 上下文字符串 ∥ 0x00 ⏟ 分隔符 ∥ Transcript-Hash ( … ) ⏟ 握手记录哈希 \text{CertVerify\_input} = \underbrace{\texttt{0x20} \times 64}_{\text{64 个空格字节}} \| \underbrace{\texttt{"TLS 1.3, server CertificateVerify"}}_{\text{上下文字符串}} \| \underbrace{\texttt{0x00}}_{\text{分隔符}} \| \underbrace{\text{Transcript-Hash}(\ldots)}_{\text{握手记录哈希}} CertVerify_input=64 个空格字节 0x20×64上下文字符串 "TLS 1.3, server CertificateVerify"分隔符 0x00握手记录哈希 Transcript-Hash()
前 64 个 0x20 字节的作用:防止跨协议攻击(不让攻击者把这个签名误用到其他协议中)。
Finished 消息:握手完整性的最终校验,计算方式:
finished_key = HKDF-Expand-Label ( BaseKey , "finished" , "" , Hash.length ) \text{finished\_key} = \text{HKDF-Expand-Label}(\text{BaseKey}, \texttt{"finished"}, \texttt{""}, \text{Hash.length}) finished_key=HKDF-Expand-Label(BaseKey,"finished","",Hash.length)
verify_data = HMAC ( finished_key ,  Transcript-Hash ( 所有握手消息 ) ) \text{verify\_data} = \text{HMAC}(\text{finished\_key},\ \text{Transcript-Hash}(\text{所有握手消息})) verify_data=HMAC(finished_key, Transcript-Hash(所有握手消息))
双方都必须验证对方的 Finished 值,任何不一致都意味着握手被篡改。

4.6 握手状态机(客户端视角)

                    START
                      |
              发送 ClientHello
                      |
                  WAIT_SH <--------+
                      |            | 收到 HelloRetryRequest
              收到 ServerHello     |
              K_recv = handshake   |
                      |            
                  WAIT_EE
                      |
              收到 EncryptedExtensions
                 /           \
         用 PSK              用证书
            |                   |
            |             WAIT_CERT_CR
            |          收到 CertificateRequest
            |                   |
            |             WAIT_CERT
            |           收到 Certificate
            |                   |
            |               WAIT_CV
            |         收到 CertificateVerify
            |                   |
            +---> WAIT_FINISHED <+
                      |
              收到 Finished
              [发送 EndOfEarlyData]
              发送 Certificate(如需)
              发送 CertificateVerify(如需)
              发送 Finished
              K_send = K_recv = application
                      |
                  CONNECTED

5. PSK 会话恢复

5.1 为什么需要会话恢复?

完整握手需要做公钥运算(慢),PSK 恢复用上一次握手建立的对称密钥代替,速度快得多。

5.2 NewSessionTicket 票据

完整握手结束后,服务器发一个票据给客户端:

struct {
    uint32 ticket_lifetime;      // 票据有效期(秒),最长 7 天(604800 秒)
    uint32 ticket_age_add;       // 用于混淆票据年龄的随机 32 位值
    opaque ticket_nonce<0..255>; // 本连接内唯一的 nonce
    opaque ticket<1..2^16-1>;    // 票据本身(不透明,服务器自定义格式)
    Extension extensions<0..2^16-2>;
} NewSessionTicket;

票据对应的 PSK 计算方式:
PSK = HKDF-Expand-Label ( resumption_master_secret ,   "resumption" ,  ticket_nonce ,  Hash.length ) \text{PSK} = \text{HKDF-Expand-Label}(\text{resumption\_master\_secret},\ \texttt{"resumption"},\ \text{ticket\_nonce},\ \text{Hash.length}) PSK=HKDF-Expand-Label(resumption_master_secret, "resumption", ticket_nonce, Hash.length)

5.3 PSK 恢复握手流程

服务器 客户端 服务器 客户端 === 第一次连接(完整握手)=== === 第二次连接(PSK 恢复,更快)=== 应用数据传输 ClientHello + key_share ServerHello + key_share EncryptedExtensions Certificate + CertificateVerify Finished Finished NewSessionTicket(票据) ClientHello + pre_shared_key(票据标识) + key_share(可选,用于前向保密) ServerHello + pre_shared_key(确认使用) + key_share(可选) EncryptedExtensions Finished(无需 Certificate!) Finished

恢复时不需要 Certificate 和 CertificateVerify,节省了昂贵的证书验证计算。

6. 0-RTT 早期数据

6.1 什么是 0-RTT?

普通握手需要至少 1 个往返(1-RTT)才能发应用数据。0-RTT 允许客户端在第一个数据包里就携带应用数据,不等服务器回复。

普通 1-RTT:
  客户端 ---ClientHello-----> 服务器       (第1次发送)
  客户端 <---ServerHello+Finished--- 服务器 (第1次等待)
  客户端 ---Finished + 应用数据--> 服务器   (第2次发送)
0-RTT:
  客户端 ---ClientHello + [应用数据]--> 服务器  (第1次发送,同时带数据)
  客户端 <---ServerHello+Finished--- 服务器     (等待一次)
  客户端 ---EndOfEarlyData + Finished--> 服务器

6.2 0-RTT 的安全限制

0-RTT 数据有两个根本性的安全弱点:
弱点一:无前向保密
0-RTT 数据用 PSK 派生的 client_early_traffic_secret 加密。如果 PSK 泄露,历史 0-RTT 数据就会被解密。
弱点二:重放攻击风险
攻击者可以截获 ClientHello + 0-RTT 数据,然后重新发送给服务器,让服务器执行同一个请求两次。

正常流程:
  客户端 ---[POST /pay 100元]--> 服务器   转账成功
重放攻击:
  攻击者截获上面的数据包
  攻击者 ---[POST /pay 100元]--> 服务器   再次转账!

因此,0-RTT 只应用于幂等操作(如 HTTP GET),绝不能用于有副作用的操作(如转账、下单)。

6.3 服务器如何防重放

服务器有三种处理方式,可以选择:
单次票据(Single-Use Tickets):维护一个数据库,每个票据用过一次就删除。简单但需要分布式存储。
ClientHello 记录(ClientHello Recording):在一个时间窗口内记录所有 ClientHello 的哈希值,发现重复则拒绝。
新鲜度检查(Freshness Checks):
expected_arrival_time = adjusted_creation_time + clients_ticket_age \text{expected\_arrival\_time} = \text{adjusted\_creation\_time} + \text{clients\_ticket\_age} expected_arrival_time=adjusted_creation_time+clients_ticket_age
票据年龄用混淆值传输,防止被动观测者关联连接:
obfuscated_ticket_age = ( actual_age_ms + ticket_age_add )   m o d   2 32 \text{obfuscated\_ticket\_age} = (\text{actual\_age\_ms} + \text{ticket\_age\_add}) \bmod 2^{32} obfuscated_ticket_age=(actual_age_ms+ticket_age_add)mod232

7. 记录层协议

7.1 TLS 记录的两种格式

明文记录(握手初期):

struct {
    ContentType type;                    // 内容类型(握手/警告/应用数据)
    ProtocolVersion legacy_record_version; // 固定 0x0303,已废弃但保留
    uint16 length;                       // 后续数据长度,最大 2^14 字节
    opaque fragment[length];             // 实际数据
} TLSPlaintext;

密文记录(握手完成后):

struct {
    ContentType opaque_type = application_data; // 永远填 23,隐藏真实类型
    ProtocolVersion legacy_record_version = 0x0303;
    uint16 length;                       // 加密后数据长度(最大 2^14 + 256 字节)
    opaque encrypted_record[length];     // AEAD 加密结果
} TLSCiphertext;

内层明文结构(加密前):

struct {
    opaque content[TLSPlaintext.length]; // 实际内容
    ContentType type;                    // 真实内容类型(放在最后!)
    uint8 zeros[length_of_padding];      // 可选填充(全零字节)
} TLSInnerPlaintext;

为什么真实 ContentType 放在最后?
这样可以加任意长度的填充(在内容后面加零字节),接收方解密后从后往前扫描找到第一个非零字节就是 ContentType,不需要提前知道填充长度。同时,对外部观察者来说所有记录都显示为 application_data(23),无法区分是握手数据还是应用数据。

7.2 AEAD 加密

TLS 1.3 要求所有加密算法都是 AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密),同时提供加密和认证,不需要单独的 MAC 计算。
加密过程:
AEADEncrypted = AEAD-Encrypt ( write_key ,  nonce ,  additional_data ,  TLSInnerPlaintext ) \text{AEADEncrypted} = \text{AEAD-Encrypt}(\text{write\_key},\ \text{nonce},\ \text{additional\_data},\ \text{TLSInnerPlaintext}) AEADEncrypted=AEAD-Encrypt(write_key, nonce, additional_data, TLSInnerPlaintext)
关联数据(additional_data)= TLSCiphertext 的头部(3 字节 = opaque_type + legacy_record_version + length)。
解密过程:
TLSInnerPlaintext = AEAD-Decrypt ( peer_write_key ,  nonce ,  additional_data ,  encrypted_record ) \text{TLSInnerPlaintext} = \text{AEAD-Decrypt}(\text{peer\_write\_key},\ \text{nonce},\ \text{additional\_data},\ \text{encrypted\_record}) TLSInnerPlaintext=AEAD-Decrypt(peer_write_key, nonce, additional_data, encrypted_record)
解密失败(认证标签不匹配)→ 立即发送 bad_record_mac 警告并断开连接。

7.3 Per-Record Nonce(每条记录的随机数)

为了防止重放和重排序攻击,每条记录使用不同的 nonce:
nonce = pad ( seq_num , iv_length ) ⊕ write_iv \text{nonce} = \text{pad}(\text{seq\_num}, \text{iv\_length}) \oplus \text{write\_iv} nonce=pad(seq_num,iv_length)write_iv
其中 pad \text{pad} pad 表示把 64 位序号左填零至 iv_length 字节, ⊕ \oplus 是按位异或, write_iv \text{write\_iv} write_iv 是从密钥材料派生的静态 IV。
序号从 0 开始,每发/收一条记录递增 1。序号不同 → nonce 不同 → 即使内容相同,加密结果也不同。

7.4 必须实现的密码套件


密码套件 要求
TLS_AES_128_GCM_SHA256 {0x13, 0x01} 必须实现
TLS_AES_256_GCM_SHA384 {0x13, 0x02} 推荐实现
TLS_CHACHA20_POLY1305_SHA256 {0x13, 0x03} 推荐实现
TLS_AES_128_CCM_SHA256 {0x13, 0x04} 可选
TLS_AES_128_CCM_8_SHA256 {0x13, 0x05} 可选

命名规则:TLS_<AEAD算法>_<HKDF哈希算法>

8. 密钥调度系统

8.1 HKDF 基础

TLS 1.3 的全部密钥都通过 HKDF(HMAC-based Extract-and-Expand Key Derivation Function)推导。HKDF 有两个操作:
HKDF-Extract:把高熵但格式不规整的输入(如 DHE 共享秘密)提炼成固定长度的伪随机密钥。
PRK = HKDF-Extract ( Salt ,  IKM ) \text{PRK} = \text{HKDF-Extract}(\text{Salt},\ \text{IKM}) PRK=HKDF-Extract(Salt, IKM)
HKDF-Expand:从 PRK 中展开出任意长度的密钥材料。
OKM = HKDF-Expand ( PRK ,  info ,  Length ) \text{OKM} = \text{HKDF-Expand}(\text{PRK},\ \text{info},\ \text{Length}) OKM=HKDF-Expand(PRK, info, Length)
TLS 1.3 在此基础上定义了两个便利函数:
HKDF-Expand-Label ( Secret ,  Label ,  Context ,  Length ) = HKDF-Expand ( Secret ,  HkdfLabel ,  Length ) \text{HKDF-Expand-Label}(\text{Secret},\ \text{Label},\ \text{Context},\ \text{Length}) = \text{HKDF-Expand}(\text{Secret},\ \text{HkdfLabel},\ \text{Length}) HKDF-Expand-Label(Secret, Label, Context, Length)=HKDF-Expand(Secret, HkdfLabel, Length)
其中 HkdfLabel = Length || "tls13 " + Label || Context(都是长度前缀编码)。
Derive-Secret ( Secret ,  Label ,  Messages ) = HKDF-Expand-Label ( Secret ,  Label ,  Transcript-Hash ( Messages ) ,  Hash.length ) \text{Derive-Secret}(\text{Secret},\ \text{Label},\ \text{Messages}) = \text{HKDF-Expand-Label}(\text{Secret},\ \text{Label},\ \text{Transcript-Hash}(\text{Messages}),\ \text{Hash.length}) Derive-Secret(Secret, Label, Messages)=HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)

8.2 完整密钥调度图

           输入: 0(零值,Hash.length 字节)
                  |
                  v
PSK --------> HKDF-Extract = Early Secret(早期秘密)
                  |
                  +-------> Derive-Secret(., "ext binder"/"res binder", "")
                  |                           = binder_key(PSK 绑定器密钥)
                  |
                  +-------> Derive-Secret(., "c e traffic", ClientHello)
                  |                           = client_early_traffic_secret(0-RTT 密钥源)
                  |
                  +-------> Derive-Secret(., "e exp master", ClientHello)
                  |                           = early_exporter_master_secret
                  |
                  v
            Derive-Secret(., "derived", "")
                  |
                  v
(EC)DHE --> HKDF-Extract = Handshake Secret(握手秘密)
                  |
                  +-------> Derive-Secret(., "c hs traffic", CH...SH)
                  |                           = client_handshake_traffic_secret
                  |
                  +-------> Derive-Secret(., "s hs traffic", CH...SH)
                  |                           = server_handshake_traffic_secret
                  |
                  v
            Derive-Secret(., "derived", "")
                  |
                  v
   0 -------> HKDF-Extract = Master Secret(主秘密)
                  |
                  +-------> Derive-Secret(., "c ap traffic", CH...SF)
                  |                           = client_application_traffic_secret_0
                  |
                  +-------> Derive-Secret(., "s ap traffic", CH...SF)
                  |                           = server_application_traffic_secret_0
                  |
                  +-------> Derive-Secret(., "exp master", CH...SF)
                  |                           = exporter_master_secret
                  |
                  +-------> Derive-Secret(., "res master", CH...CF)
                                              = resumption_master_secret

缩写说明:CH = ClientHello, SH = ServerHello, SF = server Finished, CF = client Finished

8.3 从流量密钥推导实际加密密钥

每种流量密钥(client_handshake、server_application 等)的实际加密密钥按以下方式计算:
write_key = HKDF-Expand-Label ( Secret ,   "key" ,   "" ,  key_length ) \text{write\_key} = \text{HKDF-Expand-Label}(\text{Secret},\ \texttt{"key"},\ \texttt{""},\ \text{key\_length}) write_key=HKDF-Expand-Label(Secret, "key", "", key_length)
write_iv = HKDF-Expand-Label ( Secret ,   "iv" ,   "" ,  iv_length ) \text{write\_iv} = \text{HKDF-Expand-Label}(\text{Secret},\ \texttt{"iv"},\ \texttt{""},\ \text{iv\_length}) write_iv=HKDF-Expand-Label(Secret, "iv", "", iv_length)
不同阶段用不同的 Secret:

记录类型 Secret 来源
0-RTT 应用数据 client_early_traffic_secret
握手数据 [sender]_handshake_traffic_secret
应用数据 [sender]_application_traffic_secret_N

8.4 密钥更新(KeyUpdate)

握手完成后,任一方可以发送 KeyUpdate 消息,触发密钥轮转:
application_traffic_secret N + 1 = HKDF-Expand-Label ( application_traffic_secret N ,   "traffic   upd" ,   "" ,  Hash.length ) \text{application\_traffic\_secret}_{N+1} = \text{HKDF-Expand-Label}(\text{application\_traffic\_secret}_N,\ \texttt{"traffic upd"},\ \texttt{""},\ \text{Hash.length}) application_traffic_secretN+1=HKDF-Expand-Label(application_traffic_secretN, "traffic upd", "", Hash.length)
旧密钥一旦派生出新密钥就应立即删除,提供前向保密(即使当前密钥泄露,过去的通信也是安全的)。

9. Alert 警告协议

9.1 格式

struct {
    AlertLevel level;          // warning(1) 或 fatal(2),TLS 1.3 中基本忽略
    AlertDescription description;  // 具体错误码
} Alert;

TLS 1.3 中所有错误告警都视为 fatal,收到后必须立即断开连接。

9.2 常见告警代码速查


告警 含义 典型触发场景
close_notify (0) 正常关闭 一方主动关闭连接
unexpected_message (10) 收到不期望的消息 消息顺序错误
bad_record_mac (20) AEAD 解密/认证失败 数据被篡改
record_overflow (22) 记录超过大小限制 记录超过 2^14+256 字节
handshake_failure (40) 无法协商出可接受的参数 算法不兼容
illegal_parameter (47) 握手字段非法 格式正确但语义错误
decrypt_error (51) 握手层密码操作失败 证书签名验证失败、Finished 验证失败
protocol_version (70) 协议版本不支持 版本协商失败
missing_extension (109) 缺少必需扩展 没有提供 signature_algorithms
certificate_required (116) 需要客户端证书但未提供 mTLS 场景

10. 安全性分析

10.1 前向保密(Forward Secrecy)

TLS 1.3 默认提供前向保密:即使服务器的长期私钥在将来被泄露,攻击者也无法解密过去的会话数据。
原因:每次握手使用的是临时 (EC)DHE 密钥对,会话结束后立即销毁。服务器私钥只用于签名认证,不参与密钥交换。
唯一例外:PSK-only 模式(不使用 DHE)没有前向保密。

10.2 降级攻击防护

攻击者可能试图让双方协商到较弱的旧版本协议。TLS 1.3 有两层保护:
第一层:Finished 消息的 HMAC 覆盖整个握手记录,任何篡改都会导致验证失败。
第二层:ServerHello.random 中的降级标志(见第 4.2 节),客户端主动检测是否被强迫降级。

10.3 密钥确认(Key Confirmation)

Finished 消息证明双方持有相同的密钥材料。如果有中间人替换了握手参数,Finished 的验证必然失败(因为哈希包含了所有握手消息)。

11. 完整 C++ 演示代码

下面的代码演示 TLS 1.3 密钥调度的核心计算:HKDF-Extract、HKDF-Expand-Label、Derive-Secret,以及 Finished 消息的计算。依赖 OpenSSL。

// tls13_key_schedule.cpp
// 演示 TLS 1.3 RFC 8446 的核心密钥推导机制:
//   1. HKDF-Extract / HKDF-Expand
//   2. HKDF-Expand-Label(TLS 1.3 特有包装)
//   3. Derive-Secret(带握手记录哈希)
//   4. Early Secret / Handshake Secret / Master Secret 的计算流程
//   5. Finished 消息的 verify_data 计算
//
// 编译:
//   g++ -std=c++17 -o tls13_demo tls13_key_schedule.cpp -lssl -lcrypto
//
// 注意: 本代码仅演示密钥推导逻辑,不涉及实际网络通信
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <stdexcept>
// OpenSSL 头文件(提供 HMAC、SHA-256、HKDF 等)
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <openssl/kdf.h>
// ============================================================
// 基础类型别名
// ============================================================
using Bytes = std::vector<uint8_t>;
// ============================================================
// 工具函数:打印字节数组为十六进制
// ============================================================
void printHex(const std::string& label, const Bytes& data)
{
    std::cout << label << " [" << data.size() << " 字节]: ";
    for (uint8_t b : data) {
        std::cout << std::hex << std::setw(2) << std::setfill('0')
                  << static_cast<int>(b);
    }
    std::cout << std::dec << "\n";
}
// ============================================================
// 工具函数:字符串转 Bytes
// ============================================================
Bytes strToBytes(const std::string& s)
{
    return Bytes(s.begin(), s.end());
}
// ============================================================
// HKDF-Extract
// 输入: salt(盐值)和 ikm(输入密钥材料)
// 输出: PRK(伪随机密钥),长度 = Hash 输出长度(SHA-256 为 32 字节)
//
// 数学: PRK = HMAC-Hash(salt, ikm)
// ============================================================
Bytes hkdfExtract(const Bytes& salt, const Bytes& ikm)
{
    // HMAC 的 key = salt,data = ikm
    unsigned int len = 0;
    Bytes prk(EVP_MAX_MD_SIZE);
    // 若 salt 为空,用全零 Hash.length 字节作为 salt
    Bytes actualSalt = salt.empty() ? Bytes(32, 0x00) : salt;
    HMAC(EVP_sha256(),
         actualSalt.data(), static_cast<int>(actualSalt.size()),
         ikm.data(),        static_cast<int>(ikm.size()),
         prk.data(),        &len);
    prk.resize(len);
    return prk;
}
// ============================================================
// HKDF-Expand
// 输入: prk(来自 Extract 的 PRK)、info(上下文信息)、length(期望输出长度)
// 输出: OKM(输出密钥材料),长度 = length
//
// 数学(迭代计算):
//   T(0) = ""(空)
//   T(i) = HMAC-Hash(PRK, T(i-1) || info || i)
//   OKM  = T(1) || T(2) || ... 截取前 length 字节
// ============================================================
Bytes hkdfExpand(const Bytes& prk, const Bytes& info, size_t length)
{
    Bytes okm;
    okm.reserve(length);
    Bytes T;         // T(i-1),初始为空
    uint8_t counter = 1;
    while (okm.size() < length) {
        // 构造 HMAC 输入 = T(i-1) || info || counter
        Bytes input;
        input.insert(input.end(), T.begin(), T.end());
        input.insert(input.end(), info.begin(), info.end());
        input.push_back(counter++);
        // 计算 T(i) = HMAC-SHA256(PRK, input)
        unsigned int len = 0;
        T.resize(EVP_MAX_MD_SIZE);
        HMAC(EVP_sha256(),
             prk.data(),   static_cast<int>(prk.size()),
             input.data(), static_cast<int>(input.size()),
             T.data(),     &len);
        T.resize(len);
        // 追加到 OKM
        okm.insert(okm.end(), T.begin(), T.end());
    }
    okm.resize(length);
    return okm;
}
// ============================================================
// HKDF-Expand-Label(TLS 1.3 特有的包装,RFC 8446 Section 7.1)
//
// 构造 HkdfLabel 结构:
//   uint16 length          (2 字节,大端序)
//   uint8  label_len       (1 字节)
//   "tls13 " + Label       (label_len 字节)
//   uint8  context_len     (1 字节)
//   Context                (context_len 字节)
//
// 然后调用 HKDF-Expand(secret, HkdfLabel, length)
// ============================================================
Bytes hkdfExpandLabel(const Bytes& secret,
                      const std::string& label,
                      const Bytes& context,
                      size_t length)
{
    // 构造带有 "tls13 " 前缀的完整标签
    std::string fullLabel = "tls13 " + label;
    // 构造 HkdfLabel 字节序列
    Bytes hkdfLabel;
    // 1. length(2 字节大端序)
    hkdfLabel.push_back(static_cast<uint8_t>((length >> 8) & 0xFF));
    hkdfLabel.push_back(static_cast<uint8_t>(length & 0xFF));
    // 2. label_len(1 字节)+ 标签内容
    hkdfLabel.push_back(static_cast<uint8_t>(fullLabel.size()));
    hkdfLabel.insert(hkdfLabel.end(), fullLabel.begin(), fullLabel.end());
    // 3. context_len(1 字节)+ 上下文内容
    hkdfLabel.push_back(static_cast<uint8_t>(context.size()));
    hkdfLabel.insert(hkdfLabel.end(), context.begin(), context.end());
    return hkdfExpand(secret, hkdfLabel, length);
}
// ============================================================
// SHA-256 哈希计算
// ============================================================
Bytes sha256(const Bytes& data)
{
    Bytes hash(SHA256_DIGEST_LENGTH);
    SHA256(data.data(), data.size(), hash.data());
    return hash;
}
// ============================================================
// Transcript-Hash(握手记录哈希)
// 把所有握手消息拼接后做 SHA-256
// ============================================================
Bytes transcriptHash(const std::vector<Bytes>& messages)
{
    Bytes concat;
    for (const auto& msg : messages) {
        concat.insert(concat.end(), msg.begin(), msg.end());
    }
    return sha256(concat);
}
// ============================================================
// Derive-Secret(RFC 8446 Section 7.1)
//
// Derive-Secret(Secret, Label, Messages) =
//     HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
//
// Hash.length 对于 SHA-256 来说是 32 字节
// ============================================================
Bytes deriveSecret(const Bytes& secret,
                   const std::string& label,
                   const std::vector<Bytes>& messages)
{
    Bytes context = transcriptHash(messages);  // 握手记录的哈希值
    return hkdfExpandLabel(secret, label, context, 32); // SHA-256 输出 32 字节
}
// ============================================================
// HMAC-SHA256 计算(用于 Finished 消息)
// ============================================================
Bytes hmacSha256(const Bytes& key, const Bytes& data)
{
    unsigned int len = 0;
    Bytes result(EVP_MAX_MD_SIZE);
    HMAC(EVP_sha256(),
         key.data(),  static_cast<int>(key.size()),
         data.data(), static_cast<int>(data.size()),
         result.data(), &len);
    result.resize(len);
    return result;
}
// ============================================================
// 计算 Finished 消息的 verify_data(RFC 8446 Section 4.4.4)
//
// finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)
// verify_data  = HMAC(finished_key, Transcript-Hash(所有握手消息))
// ============================================================
Bytes computeFinishedVerifyData(const Bytes& baseKey,
                                 const std::vector<Bytes>& handshakeMessages)
{
    // 步骤 1: 推导 finished_key
    Bytes finishedKey = hkdfExpandLabel(baseKey, "finished", Bytes{}, 32);
    // 步骤 2: 计算握手记录哈希(到 Finished 之前的所有消息)
    Bytes txHash = transcriptHash(handshakeMessages);
    // 步骤 3: 计算 HMAC
    return hmacSha256(finishedKey, txHash);
}
// ============================================================
// 主函数:演示 TLS 1.3 密钥调度流程
// ============================================================
int main()
{
    std::cout << "=== TLS 1.3 RFC 8446 密钥调度演示 ===\n\n";
    // ---- 模拟输入 ----
    // 在真实场景中:
    //   PSK 来自上一次连接的 resumption_master_secret 派生,或外部配置
    //   DHE 共享秘密来自 ECDH 计算
    //   握手消息是实际的 ClientHello/ServerHello 等字节内容
    // PSK(预共享密钥)— 无 PSK 时用全零 32 字节
    Bytes psk(32, 0x00);
    // (EC)DHE 共享秘密(实际场景中是 ECDH 的 x 坐标)
    // 这里用模拟值
    Bytes dhSharedSecret(32, 0x42);
    // 模拟握手消息内容(实际应是真实的 TLS 握手消息字节)
    Bytes clientHello  = strToBytes("ClientHello_mock_data_for_demo");
    Bytes serverHello  = strToBytes("ServerHello_mock_data_for_demo");
    Bytes encExt       = strToBytes("EncryptedExtensions_mock");
    Bytes certificate  = strToBytes("Certificate_mock_data");
    Bytes certVerify   = strToBytes("CertificateVerify_mock");
    Bytes serverFinish = strToBytes("Finished_server_mock");
    std::cout << "--- 阶段 1: 计算 Early Secret ---\n";
    // Early Secret = HKDF-Extract(0, PSK)
    // Salt 为全零 Hash.length 字节,IKM 为 PSK
    Bytes zeroSalt(32, 0x00);
    Bytes earlySecret = hkdfExtract(zeroSalt, psk);
    printHex("Early Secret", earlySecret);
    // 从 Early Secret 派生 binder_key(用于 PSK 绑定器计算)
    Bytes binderKey = deriveSecret(earlySecret, "res binder", {});
    printHex("binder_key (res binder)", binderKey);
    // 从 Early Secret 派生 client_early_traffic_secret(0-RTT 密钥源)
    Bytes clientEarlyTrafficSecret = deriveSecret(earlySecret, "c e traffic", {clientHello});
    printHex("client_early_traffic_secret", clientEarlyTrafficSecret);
    std::cout << "\n--- 阶段 2: 计算 Handshake Secret ---\n";
    // 先从 Early Secret 派生 "derived" 作为下一个 Extract 的 Salt
    Bytes derivedFromEarly = deriveSecret(earlySecret, "derived", {});
    printHex("derived(Early -> Handshake 中间值)", derivedFromEarly);
    // Handshake Secret = HKDF-Extract(derived, DHE共享秘密)
    Bytes handshakeSecret = hkdfExtract(derivedFromEarly, dhSharedSecret);
    printHex("Handshake Secret", handshakeSecret);
    // 派生握手流量密钥(用于加密 EncryptedExtensions、Certificate 等)
    Bytes clientHsTraffic = deriveSecret(handshakeSecret, "c hs traffic",
                                          {clientHello, serverHello});
    Bytes serverHsTraffic = deriveSecret(handshakeSecret, "s hs traffic",
                                          {clientHello, serverHello});
    printHex("client_handshake_traffic_secret", clientHsTraffic);
    printHex("server_handshake_traffic_secret", serverHsTraffic);
    std::cout << "\n--- 阶段 3: 从握手流量密钥派生实际加密密钥 ---\n";
    // write_key = HKDF-Expand-Label(secret, "key", "", key_length)
    // write_iv  = HKDF-Expand-Label(secret, "iv",  "", iv_length)
    // AES-128-GCM: key_length=16, iv_length=12
    Bytes serverWriteKey = hkdfExpandLabel(serverHsTraffic, "key", Bytes{}, 16);
    Bytes serverWriteIv  = hkdfExpandLabel(serverHsTraffic, "iv",  Bytes{}, 12);
    printHex("server_write_key (16字节,用于AES-128-GCM)", serverWriteKey);
    printHex("server_write_iv  (12字节,用于AEAD nonce基础)", serverWriteIv);
    std::cout << "\n--- 阶段 4: 计算 Master Secret ---\n";
    // 从 Handshake Secret 派生 "derived"
    Bytes derivedFromHs = deriveSecret(handshakeSecret, "derived", {});
    // Master Secret = HKDF-Extract(derived, 0)
    Bytes zeroIkm(32, 0x00);
    Bytes masterSecret = hkdfExtract(derivedFromHs, zeroIkm);
    printHex("Master Secret", masterSecret);
    // 派生应用数据流量密钥
    std::vector<Bytes> hsMessages = {clientHello, serverHello, encExt,
                                      certificate, certVerify, serverFinish};
    Bytes clientAppTraffic = deriveSecret(masterSecret, "c ap traffic", hsMessages);
    Bytes serverAppTraffic = deriveSecret(masterSecret, "s ap traffic", hsMessages);
    printHex("client_application_traffic_secret_0", clientAppTraffic);
    printHex("server_application_traffic_secret_0", serverAppTraffic);
    // 派生 resumption_master_secret(用于下一次 PSK 恢复)
    std::vector<Bytes> allMessages = {clientHello, serverHello, encExt,
                                       certificate, certVerify, serverFinish,
                                       strToBytes("client_Finished_mock")};
    Bytes resumptionSecret = deriveSecret(masterSecret, "res master", allMessages);
    printHex("resumption_master_secret", resumptionSecret);
    std::cout << "\n--- 阶段 5: 计算 server Finished 的 verify_data ---\n";
    // BaseKey for server Finished = server_handshake_traffic_secret
    // Handshake Context = ClientHello ... CertificateVerify(Finished 之前所有消息)
    std::vector<Bytes> beforeServerFinished = {clientHello, serverHello, encExt,
                                                certificate, certVerify};
    Bytes serverVerifyData = computeFinishedVerifyData(serverHsTraffic,
                                                        beforeServerFinished);
    printHex("server Finished verify_data", serverVerifyData);
    std::cout << "\n--- 阶段 6: 演示 Per-Record Nonce 计算 ---\n";
    // nonce = pad(seq_num, iv_length) XOR write_iv
    // 假设当前序号为 0(第一条记录)
    uint64_t seqNum = 0;
    // 将序号左填零至 12 字节
    Bytes paddedSeq(12, 0x00);
    for (int i = 0; i < 8; ++i) {
        paddedSeq[11 - i] = static_cast<uint8_t>((seqNum >> (i * 8)) & 0xFF);
    }
    // nonce = paddedSeq XOR write_iv
    Bytes nonce(12);
    for (int i = 0; i < 12; ++i) {
        nonce[i] = paddedSeq[i] ^ serverWriteIv[i];
    }
    printHex("序号 0 对应的 per-record nonce", nonce);
    // 序号 1 的 nonce
    seqNum = 1;
    std::fill(paddedSeq.begin(), paddedSeq.end(), 0x00);
    for (int i = 0; i < 8; ++i) {
        paddedSeq[11 - i] = static_cast<uint8_t>((seqNum >> (i * 8)) & 0xFF);
    }
    for (int i = 0; i < 12; ++i) {
        nonce[i] = paddedSeq[i] ^ serverWriteIv[i];
    }
    printHex("序号 1 对应的 per-record nonce", nonce);
    std::cout << "\n--- 阶段 7: 演示 KeyUpdate(密钥轮转)---\n";
    // application_traffic_secret_{N+1} =
    //   HKDF-Expand-Label(application_traffic_secret_N, "traffic upd", "", Hash.length)
    Bytes nextClientAppTraffic = hkdfExpandLabel(clientAppTraffic, "traffic upd", Bytes{}, 32);
    printHex("client_application_traffic_secret_1(轮转后)", nextClientAppTraffic);
    std::cout << "\n=== 演示完毕 ===\n";
    return 0;
}

11.1 编译与运行

# 安装 OpenSSL 开发库
sudo apt-get install libssl-dev     # Ubuntu/Debian
brew install openssl                # macOS
# 编译
g++ -std=c++17 -o tls13_demo tls13_key_schedule.cpp -lssl -lcrypto
# 运行
./tls13_demo

11.2 预期输出结构

=== TLS 1.3 RFC 8446 密钥调度演示 ===
--- 阶段 1: 计算 Early Secret ---
Early Secret [32 字节]: 33ad0a1c607ec03b09e6cd9893680ce2...
binder_key (res binder) [32 字节]: ...
client_early_traffic_secret [32 字节]: ...
--- 阶段 2: 计算 Handshake Secret ---
derived(Early -> Handshake 中间值)[32 字节]: ...
Handshake Secret [32 字节]: ...
client_handshake_traffic_secret [32 字节]: ...
server_handshake_traffic_secret [32 字节]: ...
--- 阶段 3: 从握手流量密钥派生实际加密密钥 ---
server_write_key (16字节,用于AES-128-GCM) [16 字节]: ...
server_write_iv  (12字节,用于AEAD nonce基础) [12 字节]: ...
--- 阶段 4: 计算 Master Secret ---
...(后续各阶段输出)...
=== 演示完毕 ===

附录:TLS 1.3 密钥层次结构一览

原始输入
   PSK ──────────────────────────────────────┐
   (EC)DHE 共享秘密 ─────────────────────────┤
                                             │
                                        HKDF 推导链
                                             │
                       ┌─────────────────────┼──────────────────────────┐
                       │                     │                          │
                  Early Secret        Handshake Secret           Master Secret
                       │                     │                          │
          ┌────────────┤         ┌────────────┤             ┌────────────┤
          │            │         │            │             │            │
      binder_key  client_early  c_hs_traffic s_hs_traffic  c_ap_traffic s_ap_traffic
                  _traffic_    (握手加密)  (握手加密)  (应用加密)  (应用加密)
                  secret
                  (0-RTT)
                                                                         resumption
                                                                         _master_secret
                                                                         (下次 PSK)

RFC 9000 — QUIC 协议完全中文详解

原文: QUIC: A UDP-Based Multiplexed and Secure Transport
作者: J. Iyengar (Fastly), M. Thomson (Mozilla)
发布: 2021 年 5 月
本文目标: 从零开始,用最易懂的语言,结合图示与代码,完整理解 QUIC 协议核心。

目录

  1. 概述 — QUIC 是什么
  2. 流 (Streams) — 数据的通道
  3. 流的状态机
  4. 流量控制 (Flow Control)
  5. 连接 (Connections)
  6. 版本协商
  7. 握手过程
  8. 地址验证
  9. 连接迁移
  10. 连接终止
  11. 错误处理
  12. 包与帧的结构
  13. 数据包化与可靠性
  14. 数据报大小与 PMTU
  15. 可变长整数编码
  16. 包格式详解
  17. 帧类型与格式详解
  18. 错误码大全
  19. 安全考量
  20. 完整 C++ 演示代码

1. 概述

1.1 QUIC 解决了什么问题

在理解 QUIC 之前,先看看旧世界的问题。
TCP 的痛点:

浏览器 ──TCP连接──> 服务器
       ─HTTP请求1─>
       ─HTTP请求2─>
       ─HTTP请求3─>
       <─响应1────
                   请求2数据包丢了!
       等待...     请求3虽然到了,但必须等请求2
       等待...     这就是"队头阻塞"(Head-of-Line Blocking)

TCP 是"一根管子",管子里的数据必须按顺序交付。一个包丢了,后面所有数据都得等它重传。
QUIC 的解法:

浏览器 ──UDP连接──> 服务器
       ──流1───────>
       ──流2───────>
       ──流3───────>
       <──流1响应──
                   流2数据包丢了!
       <──流3响应──  流3不用等流2! 各流独立!
       <──流2重传──

QUIC 在 UDP 之上实现了多路复用,每条"流"独立传输,互不影响。

1.2 QUIC 的核心特性一览

+─────────────────────────────────────────────────────────+
|                      应用层 (HTTP/3 等)                  |
+─────────────────────────────────────────────────────────+
|                      QUIC 协议                           |
|  ┌──────────┐  ┌──────────┐  ┌──────────┐              |
|  │  流 1    │  │  流 2    │  │  流 3    │  ...          |
|  │(独立传输)|  │(独立传输)|  │(独立传输)|              |
|  └──────────┘  └──────────┘  └──────────┘              |
|  ┌────────────────────────────────────────┐             |
|  │         TLS 1.3 加密 (内置)           │             |
|  └────────────────────────────────────────┘             |
|  ┌────────────────────────────────────────┐             |
|  │         流量控制 & 拥塞控制           │             |
|  └────────────────────────────────────────┘             |
+─────────────────────────────────────────────────────────+
|                      UDP                                  |
+─────────────────────────────────────────────────────────+
|                      IP                                   |
+─────────────────────────────────────────────────────────+

QUIC 的核心能力:

  • 多路复用 — 一个连接里跑多条流,互不阻塞
  • 低延迟建立 — 0-RTT 或 1-RTT 完成握手(TCP+TLS 需要 2-3 RTT)
  • 内置加密 — TLS 1.3 是 QUIC 的一部分,不可拆卸
  • 连接迁移 — 手机从 WiFi 切换到 4G,连接不断
  • 消除队头阻塞 — 流级别的丢包重传,不影响其他流

1.3 基本术语


术语 中文 含义
Endpoint 端点 参与 QUIC 连接的一方(客户端或服务器)
Client 客户端 发起连接的一方
Server 服务器 接受连接的一方
QUIC Packet QUIC 包 可被完整处理的一个 UDP 载荷单元
Frame 包内部的结构化数据单元
Stream 连接内部的有序字节序列通道
Connection ID 连接ID 标识一条 QUIC 连接的标识符
RTT 往返时延 一个包从发送到收到确认的时间

2. 流 (Streams)

2.1 流是什么

把 QUIC 的连接想象成一条高速公路,就是高速公路上的各个车道。
每条车道(流)可以:

  • 独立传输数据
  • 独立控制速度(流量控制)
  • 一条车道堵了,其他车道照跑

2.2 流 ID 的编码规则

流 ID 是一个 62 位整数,它的最低 2 位携带了类型信息:

流 ID 的结构(62 位):
┌──────────────────────────────────────────────────┬──┬──┐
│              高 60 位 (流的序号)                 │位1│位0│
└──────────────────────────────────────────────────┴──┴──┘
                                                    │   │
                                              方向位  发起方位
                                    0=双向  0=客户端发起
                                    1=单向  1=服务器发起

最低2位 流类型 例子(最小ID)
0b00 (0x00) 客户端发起的双向流 0, 4, 8, 12…
0b01 (0x01) 服务器发起的双向流 1, 5, 9, 13…
0b10 (0x02) 客户端发起的单向流 2, 6, 10, 14…
0b11 (0x03) 服务器发起的单向流 3, 7, 11, 15…

记忆口诀:

  • 位0 = 谁发起(0=客户端,1=服务器)
  • 位1 = 什么方向(0=双向,1=单向)

2.3 双向流 vs 单向流

双向流 (Bidirectional):
客户端 ──数据──> 服务器
客户端 <──数据── 服务器
(双方都能发)
单向流 (Unidirectional):
客户端 ──数据──> 服务器
(只有发起者能发,对方只能收)

2.4 流的优先级

QUIC 本身不定义优先级的线上格式,而是把这个决策权交给应用层。比如 HTTP/3 可以告诉 QUIC 底层:“这个请求比那个请求更重要,优先传它”。

3. 流的状态机

3.1 发送方状态机

一条流的发送端会经历以下几个状态:

应用创建流 /
对端创建双向流

发送第一个 STREAM 帧
或 STREAM_DATA_BLOCKED 帧

发送 RESET_STREAM

发送带 FIN 的 STREAM 帧
(所有数据已发完)

发送 RESET_STREAM

收到所有数据的 ACK
(终止状态)

发送 RESET_STREAM

收到 RESET_STREAM 的 ACK
(终止状态)

Ready

Send

Reset_Sent

Data_Sent

Data_Recvd

Reset_Recvd

每个状态的含义:

  • Ready(就绪) — 流刚被创建,可以接收应用数据,但还没发出去。
  • Send(发送中) — 正在通过 STREAM 帧传输数据,遵守对端的流量控制。
  • Data Sent(数据已发) — 所有数据(含 FIN 标志)已发出,等待对端确认。
  • Data Recvd(数据已确认) — 终止态,所有数据已被对端 ACK,大功告成。
  • Reset Sent(重置已发) — 发送了 RESET_STREAM,告诉对端"我不发了"。
  • Reset Recvd(重置已确认) — 终止态,重置操作已被对端确认。

3.2 接收方状态机

收到 STREAM / STREAM_DATA_BLOCKED /
RESET_STREAM 帧

收到带 FIN 的 STREAM 帧
(知道了流的总大小)

收到 RESET_STREAM

所有数据都收到了

收到 RESET_STREAM

应用读完了所有数据
(终止状态)

收到 RESET_STREAM (可选)

所有数据都到了 (可选)

应用读到了重置信号
(终止状态)

Recv

Size_Known

Reset_Recvd

Data_Recvd

Data_Read

Reset_Read

每个状态的含义:

  • Recv(接收中) — 正在接收数据,可能乱序到来,需要缓冲和重排。
  • Size Known(大小已知) — 收到了带 FIN 的帧,知道总共有多少字节,但数据可能还没全到。
  • Data Recvd(数据已收齐) — 所有字节都到了,等待应用层读取。
  • Data Read(应用已读) — 终止态,应用已读走所有数据。
  • Reset Recvd(收到重置) — 对端发来了 RESET_STREAM。
  • Reset Read(重置已通知) — 终止态,应用已收到重置通知。

3.3 双向流的组合状态

双向流 = 发送状态机 + 接收状态机的组合。

发送端状态 接收端状态 合并状态
Ready / 无 无 / Recv idle (空闲)
Ready/Send/Data Sent Recv/Size Known open (打开)
Ready/Send/Data Sent Data Recvd/Read half-closed (remote) 对端已关
Data Recvd Recv/Size Known half-closed (local) 本端已关
Reset Sent/Recvd Data Recvd/Read closed (已关闭)
Data Recvd Data Recvd/Read closed (已关闭)

4. 流量控制

4.1 为什么需要流量控制

想象接收方只有一个小水桶,发送方却用消防水管喷水:

发送方:水管喷水 ──────────────────────> 接收方:小水桶
                                         水桶溢出!内存耗尽!

流量控制就是让接收方告诉发送方"你最多能发多少"。

4.2 两层流量控制

QUIC 实现了两级流量控制:

连接级别流量控制(总水位线):
┌────────────────────────────────────────────────┐
│ 所有流的总字节数 不能超过 MAX_DATA 指定的值   │
│                                                │
│  流1: ████████░░░░░░  已发500字节             │
│  流2: ████░░░░░░░░░░  已发300字节             │
│  流3: ██░░░░░░░░░░░░  已发200字节             │
│  总计: 1000字节 < MAX_DATA=2000字节 ✓         │
└────────────────────────────────────────────────┘
流级别流量控制(每条流的水位线):
┌────────────────────────────────────────────────┐
│ 每条流各自有独立的 MAX_STREAM_DATA 限制        │
│                                                │
│  流1: 已发500字节 < MAX_STREAM_DATA=800 ✓     │
│  流2: 已发300字节 < MAX_STREAM_DATA=600 ✓     │
└────────────────────────────────────────────────┘

4.3 流量控制的工作流程

发送方                               接收方
   |                                    |
   |───STREAM(数据,500字节)──────────>|
   |                                    | 应用读走了300字节
   |                                    | 缓冲区空出来了
   |<──MAX_STREAM_DATA(新限制=1300)────|
   |                                    |
   |  现在可以发到 offset 1300 了       |
   |───STREAM(数据,800字节)──────────>|

接收方通过发送 MAX_DATA(连接级)和 MAX_STREAM_DATA(流级)来动态扩大限制。
关键数学关系:
发送方在任何时候发送的字节偏移量 o f f s e t offset offset 必须满足:
o f f s e t ≤ MAX_STREAM_DATA offset \leq \text{MAX\_STREAM\_DATA} offsetMAX_STREAM_DATA
∑ 所有流 最大发送偏移 ≤ MAX_DATA \sum_{\text{所有流}} \text{最大发送偏移} \leq \text{MAX\_DATA} 所有流最大发送偏移MAX_DATA

4.4 流的最终大小 (Final Size)

当一条流关闭时,必须让对方知道这条流究竟发了多少字节,用来精确计算连接级流量控制消耗。
Final Size = 最大已发字节的 offset + 1 \text{Final Size} = \text{最大已发字节的 offset} + 1 Final Size=最大已发字节的 offset+1
或者说,如果最后一个字节在偏移 N − 1 N-1 N1 处,则 Final Size = N N N

4.5 并发流控制

除了字节级流量控制,QUIC 还限制同时开多少条流:

max_streams 限制:
客户端想开第 N 条流
    ↓
流ID < (max_streams × 4 + first_stream_id_of_type) ?
    ↓是                         ↓否
  可以开                    发 STREAMS_BLOCKED 帧
                            等服务器发 MAX_STREAMS 帧

5. 连接

5.1 连接 ID 的作用

QUIC 连接不靠"IP地址+端口"来标识自己(这是 TCP 的做法),而是靠连接 ID (Connection ID)
这有什么好处?

手机在 WiFi 下建立 QUIC 连接:
  本机 IP: 192.168.1.5:12345
  连接 ID: ABCDEF01
手机切换到 4G:
  本机 IP 变成: 10.0.0.3:54321  ← IP 和端口都变了!
  连接 ID 还是: ABCDEF01        ← 但连接 ID 没变!
服务器看到:
  "哦,连接 ID 是 ABCDEF01,这是老朋友,连接继续!"

TCP 靠四元组(源IP、源端口、目标IP、目标端口)识别连接,一旦 IP 变了连接就断了。QUIC 靠 Connection ID,可以在 IP 变化时保持连接。

5.2 连接 ID 的分配与管理

每端各自选择对方应该用什么连接 ID:
客户端 ──────────────────> 服务器
  "你发给我时,用连接ID: C1"
  Source Connection ID = C1
服务器 <────────────────── 服务器
  "你发给我时,用连接ID: S3"
  Source Connection ID = S3
后续通信:
  客户端发包: Destination Connection ID = S3
  服务器发包: Destination Connection ID = C1

多个连接 ID 的用途
一个连接可以持有多个连接 ID,通过 NEW_CONNECTION_ID 帧分发给对端。这样:

  • 切换网络路径时使用新的连接 ID,避免观察者关联不同路径上的包
  • 隐私保护:不同路径用不同 ID,观察者无法将多条路径上的流量归为同一连接

5.3 连接 ID 的序列号

每个连接 ID 都有一个单调递增的序列号:

序列号 0: 握手时分配的初始连接 ID
序列号 1: preferred_address 传输参数中附带的连接 ID (如果有)
序列号 2: 第一个 NEW_CONNECTION_ID 帧中的连接 ID
序列号 3: 第二个 NEW_CONNECTION_ID 帧中的连接 ID
...

退役连接 ID 时,发送 RETIRE_CONNECTION_ID 帧,携带要退役的序列号。

6. 版本协商

6.1 为什么需要版本协商

QUIC 将来会有新版本,客户端可能支持 v1 和 v2,服务器可能只支持 v1。版本协商让双方找到共同支持的版本。

6.2 版本号的编码

版本号是 32 位无符号整数:

含义
0x00000000 保留,用于版本协商包本身
0x00000001 QUIC 版本 1(本文档)
0x?a?a?a?a 保留用于强制触发版本协商测试

"保留版本"的规律:每个字节的低 4 位都是 1010(十六进制 a)。

6.3 版本协商流程

客户端              服务器
   |                   |
   |──Initial(v99)───>|  客户端用版本99,服务器不支持
   |                   |
   |<─Version Neg─────|  服务器回: "我支持 v1, v2"
   |                   |
   |──Initial(v1)────>|  客户端改用 v1
   |                   |
   |  握手继续...      |

重要规则:

  • 版本协商包本身不包含包号,不会被 ACK
  • 客户端收到版本协商包后,必须用其中列出的版本重新尝试
  • 客户端一旦收到过任何其他包(包括更早的版本协商包),后续收到的版本协商包必须丢弃

7. 握手过程

7.1 握手的分层结构

QUIC 握手 = 密码学握手 (TLS 1.3) + 传输层参数协商
             ↑                        ↑
         确定加密密钥              双方声明能力

密码学握手要求:

  1. 认证的密钥交换 — 服务器必须被认证,客户端可选认证
  2. 独立的连接密钥 — 每条连接产生不同的密钥
  3. 传输参数的认证交换 — 防止中间人篡改参数
  4. 应用协议的认证协商 — 通过 ALPN(应用层协议协商)

7.2 完整 1-RTT 握手时序

客户端                                          服务器
  |                                               |
  |── Initial[0]: CRYPTO[ClientHello] ──────────>|
  |                                               |
  |<── Initial[0]: CRYPTO[ServerHello] ACK[0] ───|
  |<── Handshake[0]: CRYPTO[EncExt,Cert,CV,Fin]──|
  |<── 1-RTT[0]: STREAM[1, "0.5-RTT数据"] ───────|
  |                                               |
  |── Initial[1]: ACK[0] ──────────────────────>|
  |── Handshake[0]: CRYPTO[Finished] ACK[0] ───>|
  |── 1-RTT[0]: STREAM[0, "请求数据"] ACK[0] ──>|
  |                                               |
  |<── Handshake[1]: ACK[0] ──────────────────── |
  |<── 1-RTT[1]: HANDSHAKE_DONE, STREAM[3] ────── |
  |                                               |
  |   1-RTT 数据双向传输 <====================> |

RTT 计算: 1-RTT 握手只需 1 个来回就能开始传应用数据(服务器在第一个响应里就带了 0.5-RTT 数据)。

7.3 0-RTT 握手 — 极速重连

如果客户端之前连接过这个服务器,可以利用缓存的会话票据直接发数据:

客户端                                          服务器
  |                                               |
  |── Initial[0]: CRYPTO[ClientHello] ──────────>|
  |── 0-RTT[0]: STREAM[0, "立刻就发的请求"] ───>|  ← 第0轮就发了!
  |                                               |
  |<── Initial[0]: CRYPTO[SH] ACK[0] ────────── |
  |<── Handshake[0] CRYPTO[EE, Fin] ────────────|
  |<── 1-RTT[0]: STREAM[1, "响应"] ACK[0] ────── |
  |                                               |
  |── Initial[1]: ACK[0] ──────────────────────>|
  |── Handshake[0]: CRYPTO[Fin], ACK[0] ───────>|
  |── 1-RTT[1]: STREAM[0, ...] ACK[0] ─────────>|

0-RTT 的代价:不防重放攻击。服务器可能收到同一个 0-RTT 请求两次(攻击者录下来重放)。因此 0-RTT 请求必须是幂等的(比如 GET 请求,不能是转账)。

7.4 传输参数

握手时双方各自声明自己的传输参数,这些参数被纳入 TLS 握手的加密保护范围内。

参数 ID 默认值 说明
max_idle_timeout 0x01 0(禁用) 最大空闲超时(毫秒)
max_udp_payload_size 0x03 65527 最大 UDP 载荷大小
initial_max_data 0x04 0 初始连接级最大数据
initial_max_stream_data_bidi_local 0x05 0 本端发起双向流的初始流控限制
initial_max_stream_data_bidi_remote 0x06 0 对端发起双向流的初始流控限制
initial_max_stream_data_uni 0x07 0 单向流的初始流控限制
initial_max_streams_bidi 0x08 0 对端可开双向流的最大数量
initial_max_streams_uni 0x09 0 对端可开单向流的最大数量
ack_delay_exponent 0x0a 3 ACK 延迟的指数(解码用)
max_ack_delay 0x0b 25ms 最大 ACK 延迟
disable_active_migration 0x0c 不存在 禁止主动连接迁移
active_connection_id_limit 0x0e 2 愿意存储的连接ID最大数量

7.5 0-RTT 传输参数的限制

0-RTT 使用的是上次连接记住的服务器传输参数。新握手完成后,切换到新参数。
不能被"记住"用于 0-RTT 的参数(每次必须用新值):

  • ack_delay_exponent
  • max_ack_delay
  • initial_source_connection_id
  • original_destination_connection_id
  • preferred_address
  • retry_source_connection_id
  • stateless_reset_token

8. 地址验证

8.1 放大攻击 (Amplification Attack)

如果不进行地址验证,攻击者可以:

攻击者                   QUIC服务器           受害者
  |                          |                   |
  |──伪造源IP(受害者IP)──>|                   |
  |  "我是受害者,请回我"   |                   |
  |                          |──────大量数据───>|
  |                          |                   | 受害者懵了!

攻击者发一个小包,服务器向"来源地址"(受害者)发大量数据——这就是放大攻击

8.2 反放大限制

在地址验证完成之前,服务器发出的数据量不能超过收到数据量的 3 倍:
可发字节数 ≤ 3 × 已收字节数 \text{可发字节数} \leq 3 \times \text{已收字节数} 可发字节数3×已收字节数
初始包 (Initial Packet) 的客户端必须填充到至少 1200 字节,确保服务器有足够的"预算"来完成握手。

8.3 Retry 包 — 强制地址验证

服务器可以在握手前用 Retry 包来验证客户端的地址:

客户端                                服务器
  |                                      |
  |── Initial[0]: CRYPTO[CH] ──────────>|
  |                                      | 服务器不确定客户端地址是真的
  |<── Retry + Token ─────────────────── |
  |                                      |
  |── Initial+Token[1]: CRYPTO[CH] ────>| Token 证明了客户端能收到包
  |                                      | 地址验证通过!
  |<── Initial[0]: CRYPTO[SH] ACK[1] ── |
  |── Handshake[0]: CRYPTO[Fin] ───────>|

Token 的安全要求:

  • Token 必须难以猜测(包含至少 128 位熵)
  • Token 必须有完整性保护(防篡改)
  • Retry 的 Token 只能用一次

8.4 路径验证 (Path Validation)

连接迁移时,必须验证新路径的可达性:

PATH_CHALLENGE 帧:
  ┌──────────────────────────┐
  │ 类型: 0x1a               │
  │ Data: 8字节随机数         │  发送方自己记住这个随机数
  └──────────────────────────┘
PATH_RESPONSE 帧:
  ┌──────────────────────────┐
  │ 类型: 0x1b               │
  │ Data: 与Challenge相同     │  原样回显
  └──────────────────────────┘
验证流程:
发送方 ──PATH_CHALLENGE(随机数R)──> 接收方
发送方 <──PATH_RESPONSE(R)─────── 接收方
  "R 匹配上了,路径可达!"

9. 连接迁移

9.1 什么触发连接迁移

场景1: 主动迁移(手机切换网络)
  WiFi(192.168.1.5) ──→ 4G(10.0.0.3)
  客户端主动从新地址发包
场景2: NAT 重绑定(被动变化)
  NAT 设备悄悄更换了出口端口
  服务器突然收到来自新地址的包

9.2 迁移的安全性挑战

挑战1: 对端地址欺骗
攻击者伪造源地址,让服务器向受害者发大量数据。
对策:未验证的新地址,也受 3 倍放大限制。
挑战2: 路径上的地址欺骗
攻击者在路径上,将自己的地址放入数据包,让流量绕道经过攻击者。
对策:路径验证失败时,回退到上次验证过的地址。
挑战3: 路径外包转发
攻击者观察包后,以更快的路径转发,让自己变成"路径中间人"。
对策:在检测到可能的迁移时,主动验证旧路径(发 PATH_CHALLENGE),收到新路径的更高包号数据时切回。

9.3 迁移时的拥塞控制重置

切换到新路径时,旧路径的拥塞状态不能直接用于新路径:

旧路径: 拥塞窗口 = 100KB,RTT = 50ms
新路径: 可能完全不同!
         拥塞窗口重置为初始值
         RTT 估计重置为初始值

例外:如果只有端口号变化(常见的 NAT 重绑定),可以保留拥塞控制状态。

9.4 隐私与连接 ID 使用规范

错误做法(可被追踪):
  路径1(WiFi): 连接ID=ABCD,包号1,2,3...
  路径2(4G):  连接ID=ABCD,包号续...  ← 观察者能关联!
正确做法(保护隐私):
  路径1(WiFi): 连接ID=ABCD,包号1,2,3...
  路径2(4G):  连接ID=WXYZ,包号续...  ← 用新连接ID,观察者无法关联

规则:从不同本地地址发包时,必须用不同的连接 ID

10. 连接终止

10.1 三种终止方式

本端发 CONNECTION_CLOSE

收到 CONNECTION_CLOSE

空闲超时 / 无状态重置

收到 CONNECTION_CLOSE

3×PTO 超时

3×PTO 超时

连接打开

关闭中
(Closing)

排空中
(Draining)

已关闭

10.2 方式一:空闲超时

如果双方都设置了 max_idle_timeout,则取两者的最小值作为实际超时时间:
有效超时 = min ⁡ ( 客户端max_idle_timeout , 服务器max_idle_timeout ) \text{有效超时} = \min(\text{客户端max\_idle\_timeout}, \text{服务器max\_idle\_timeout}) 有效超时=min(客户端max_idle_timeout,服务器max_idle_timeout)
超时时间至少要是当前 PTO(探测超时)的 3 倍:
空闲超时 ≥ 3 × PTO \text{空闲超时} \geq 3 \times \text{PTO} 空闲超时3×PTO

10.3 方式二:立即关闭

发送 CONNECTION_CLOSE 帧,所有流立即变为隐式重置状态。

发送方发完 CONNECTION_CLOSE 后:
  → 进入 Closing 状态
  → 对收到的任何包都回复 CONNECTION_CLOSE
  → 3×PTO 后丢弃所有状态
接收方收到 CONNECTION_CLOSE 后:
  → 进入 Draining 状态
  → 可以发一个 CONNECTION_CLOSE 作为回应
  → 之后禁止发任何包
  → 3×PTO 后丢弃所有状态

为什么要等 3×PTO 才完全关闭?防止网络中还有延迟到达的包,让对端能正确忽略它们。

10.4 方式三:无状态重置 (Stateless Reset)

当服务器崩溃重启后,它没有了连接状态。这时客户端还在发包,服务器需要告诉对方"我不认识这条连接了"。

无状态重置包的格式:
┌──────────────────────────────────────────────────────┐
│ 前2位固定为 1 (0b10...) 看起来像短头包              │
│ 中间 38+ 位: 随机数(伪装成包头)                    │
│ 最后 128 位: Stateless Reset Token (16字节)          │
└──────────────────────────────────────────────────────┘

Token 的生成: 用一个全局静态密钥和连接 ID 派生:
Token = HMAC ( 静态密钥 , 连接ID ) [ 0 : 16 ] \text{Token} = \text{HMAC}(\text{静态密钥}, \text{连接ID})_{[0:16]} Token=HMAC(静态密钥,连接ID)[0:16]
这样即使服务器重启丢失了所有状态,只要静态密钥还在,就能重新计算出 Token,发给客户端让它结束连接。

11. 错误处理

11.1 连接级错误

整条连接不可用时,发送 CONNECTION_CLOSE 帧:

CONNECTION_CLOSE 帧的两种类型:
  0x1c → 传输层错误 (QUIC 自己的问题)
  0x1d → 应用层错误 (应用协议的问题)

注意:在握手完成前,如果要在 Initial 或 Handshake 包里发 CONNECTION_CLOSE,只能用 0x1c 类型,不能用 0x1d,防止泄露应用状态。

11.2 流级错误

只有一条流出问题时,不必关闭整个连接:

  • 发送 RESET_STREAM — 终止一条流的发送
  • 发送 STOP_SENDING — 请求对端停止发送这条流

12. 包与帧的结构

12.1 包的类型层级

QUIC 包大家族:
  ┌──长头包 (Long Header, 握手前用)
  │    ├── Initial 包    (初始握手, 类型 0x00)
  │    ├── 0-RTT 包      (早期数据, 类型 0x01)
  │    ├── Handshake 包  (握手数据, 类型 0x02)
  │    ├── Retry 包      (地址验证, 类型 0x03)
  │    └── Version Negotiation 包 (版本协商)
  │
  └──短头包 (Short Header, 握手后用)
       └── 1-RTT 包      (正常数据传输)

12.2 包的加密保护级别


包类型 加密保护 说明
Version Negotiation 无加密 版本独立,任何人可读
Initial 弱保护 密钥公开派生,防篡改但无保密性
Retry AEAD 完整性 防意外修改,但无保密性
Handshake 强加密 TLS 密钥派生,只有双端能读
0-RTT 强加密 早期流量密钥,不防重放
1-RTT 最强加密 完整的 TLS 1.3 保护

12.3 包号空间

QUIC 有三个独立的包号空间,每个空间的包号从 0 开始独立递增:

Initial 空间:    [0, 1, 2, ...]  ← 只有 Initial 包
Handshake 空间:  [0, 1, 2, ...]  ← 只有 Handshake 包
应用数据空间:    [0, 1, 2, ...]  ← 0-RTT 和 1-RTT 包共用

为什么要分空间?保证不同加密级别的包互不干扰,ACK 只在同一空间内有效。

12.4 合并包 (Coalescing)

多个 QUIC 包可以装进同一个 UDP 数据报:

UDP 数据报 (单个):
┌──────────────────────────────────────────────────────┐
│ Initial 包 [Length=...][包头][包号][载荷]             │
│ Handshake 包 [Length=...][包头][包号][载荷]           │
│ 1-RTT 包 [包头][包号][载荷]  ← 短头,必须放最后      │
└──────────────────────────────────────────────────────┘

好处:减少 UDP 数据报数量,提高效率,特别是握手期间。

12.5 帧类型速查表


类型值 帧名称 出现在哪些包中 特殊属性
0x00 PADDING I H 0 1 N P
0x01 PING I H 0 1
0x02-0x03 ACK I H 1 N C
0x04 RESET_STREAM 0 1
0x05 STOP_SENDING 0 1
0x06 CRYPTO I H 1
0x07 NEW_TOKEN 1
0x08-0x0f STREAM 0 1 F
0x10 MAX_DATA 0 1
0x11 MAX_STREAM_DATA 0 1
0x12-0x13 MAX_STREAMS 0 1
0x14 DATA_BLOCKED 0 1
0x15 STREAM_DATA_BLOCKED 0 1
0x16-0x17 STREAMS_BLOCKED 0 1
0x18 NEW_CONNECTION_ID 0 1 P
0x19 RETIRE_CONNECTION_ID 0 1
0x1a PATH_CHALLENGE 0 1 P
0x1b PATH_RESPONSE 1 P
0x1c-0x1d CONNECTION_CLOSE i h 0 1 N
0x1e HANDSHAKE_DONE 1

包类型说明: I=Initial, H=Handshake, 0=0-RTT, 1=1-RTT, i/h=只有0x1c类型
特殊属性: N=不触发ACK, C=不计入拥塞窗口, P=探测帧, F=受流量控制

13. 数据包化与可靠性

13.1 ACK 帧的结构

QUIC 的 ACK 可以确认不连续的包号范围(类似 TCP SACK):

ACK 帧示意(确认了包号 10, 11, 12, 15, 16):
┌───────────────────────────────────────────────┐
│ Largest Acknowledged = 16                     │
│ First ACK Range = 1  (确认 15, 16)            │
│ Gap = 1              (包 13, 14 没到)         │
│ ACK Range = 2        (确认 10, 11, 12)        │
└───────────────────────────────────────────────┘
计算过程:
  最大确认 = 16
  第一段: 16 - 1 = 15, 所以确认 [15, 16]
  Gap=1 → 跳过 2 个包 (15-1-2=12 → 下一段最大是 12)
  第二段: 12 - 2 = 10, 所以确认 [10, 12]

ACK Range 的数学公式:
每个 Gap 字段 g g g 表示跳过的包数为 g + 1 g + 1 g+1,下一段最大包号为:
下一段最大 = 前一段最小 − g − 2 \text{下一段最大} = \text{前一段最小} - g - 2 下一段最大=前一段最小g2
每个 ACK Range 字段 r r r,表示确认范围为:
[ 最大 − r , 最大 ] [\text{最大} - r, \quad \text{最大}] [最大r,最大]

13.2 ACK 发送时机

接收端什么时候发 ACK?

规则1: 至少每收到 2 个触发ACK的包,就发一次ACK
规则2: 收到乱序包时,立即发ACK(帮助发送方快速发现丢包)
规则3: 收到ECN-CE标记的包时,立即发ACK(帮助发送方快速响应拥塞)
规则4: 任何情况下,不超过 max_ack_delay 时间必须发ACK

13.3 数据重传机制

QUIC 不重传包,而是重传信息!丢失的包里的信息,用新包携带新帧来重新发送:

包1 [STREAM offset=0, 100字节]  → 丢失了!
包2 [STREAM offset=100, 50字节] → 到达
包3 [STREAM offset=0, 100字节]  → 重新发这些数据(新包号)

不同类型的信息有不同的重传规则:

  • STREAM 数据 → 在新 STREAM 帧里重发,直到被 ACK 或流被重置
  • RESET_STREAM → 重发,内容不变
  • MAX_DATA / MAX_STREAM_DATA → 重发最新值(可能已经更大了)
  • PATH_CHALLENGE → 每次发送用新的随机数
  • PATH_RESPONSE → 只发一次,不重传
  • HANDSHAKE_DONE → 必须重传直到被 ACK

13.4 显式拥塞通知 (ECN)

ECN 是一种让网络路由器"吼一嗓子"通知拥塞的机制,比丢包通知更早、更温柔:

正常传输:
  发送方 → 路由器 → 接收方
  包的 IP 头中 ECN 字段 = ECT(0)  (表示"我支持 ECN")
路由器开始拥塞:
  不是直接丢包,而是把 ECN 改成 CE  (Congestion Experienced)
  接收方看到 CE → 在 ACK 里报告 ECN-CE Count 增加
发送方看到 CE Count 增加:
  → 降低发送速率(比丢包更平滑)

ECN 验证流程(确保路径支持 ECN):

开始新路径

测试期结束

ACK中ECN计数验证通过

验证失败

后续验证失败

主动重新验证(可选)

测试中
testing

未知
unknown

支持ECN
capable

不支持ECN
failed

14. 数据报大小与 PMTU

14.1 最小保证大小

QUIC 要求网络路径能支持至少 1200 字节 的 UDP 载荷:

IPv6 最小 MTU = 1280 字节
  - IPv6 头: 40 字节
  - UDP 头:   8 字节
  = 最大 UDP 载荷: 1232 字节  > 1200 ✓
IPv4 最小 MTU (实际上大多数网络) = 1500 字节
  - IPv4 头: 20 字节
  - UDP 头:   8 字节
  = 最大 UDP 载荷: 1472 字节  > 1200 ✓

14.2 PMTU 探测

实际网络路径的最大传输单元 (PMTU) 可以通过两种方法发现:
方法1: 传统 PMTUD
依赖 ICMP “Packet Too Big” 消息(路由器告诉你"这个包太大了")。
方法2: DPLPMTUD(更推荐)
QUIC 自己发送不同大小的探测包(PADDING 填充),看哪个大小能成功被确认:

发送探测包:
  1200 字节 → ACK 到了 ✓
  1400 字节 → ACK 到了 ✓
  1500 字节 → 超时,没有 ACK ✗
  1450 字节 → ACK 到了 ✓  ← 二分查找
  ...
  最终确定 PMTU 在某个范围内

15. 可变长整数编码

15.1 编码规则

QUIC 大量使用可变长整数,用最少的字节表示最常见的小数值:
最高2位决定整数用几个字节:

最高2位  长度   可表示范围
  00     1字节  0 ~ 63            (2^6 - 1)
  01     2字节  0 ~ 16383         (2^14 - 1)
  10     4字节  0 ~ 1073741823    (2^30 - 1)
  11     8字节  0 ~ 4611686018427387903 (2^62 - 1)

15.2 编码示例

数值 37 (十六进制 0x25):
  二进制: 0b00100101
  最高2位 = 00 → 用1字节
  编码: 0x25
数值 15293 (十六进制 0x3BBD):
  需要 14 位表示
  最高2位 = 01 → 用2字节
  编码: 0x7BBD  (0b01 << 14 | 15293 = 0x4000 | 0x3BBD = 0x7BBD)
数值 494878333 (十六进制 0x1D7F3E7D):
  需要 30 位表示
  最高2位 = 10 → 用4字节
  编码: 0x9D7F3E7D  (0b10 << 30 | 494878333)

解码伪代码:

ReadVarint(data):
  v = 读取第一个字节
  prefix = v >> 6      // 取最高2位
  length = 1 << prefix // 1, 2, 4, 或 8 字节
  v = v & 0x3f         // 清除最高2位
  重复 (length-1) 次:
    v = (v << 8) | 读取下一个字节
  return v

16. 包格式详解

16.1 长头包格式

长头包通用格式 (握手阶段使用):
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
|1|1|    类型    |  类型特定位   |         版本 (32位)              |
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
| 目标连接ID长度  |          目标连接ID (0~160位)                   |
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
| 源连接ID长度    |           源连接ID (0~160位)                    |
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
|                    类型特定载荷 ...                               |
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
第0字节各位的含义:
  位7 (0x80): Header Form = 1 (长头)
  位6 (0x40): Fixed Bit = 1 (必须为1)
  位5-4 (0x30): Long Packet Type (包类型)
  位3-0 (0x0F): Type-Specific Bits (类型特定)

16.2 1-RTT 短头包格式

1-RTT 短头包 (正常数据传输):
 0                   1                   2
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
|0|1|S|R|R|K|  PN长度  |  目标连接ID  |  包号  |...
+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+─+
位7 (0x80): Header Form = 0 (短头)
位6 (0x40): Fixed Bit = 1
位5 (0x20): Spin Bit (S, 延迟自旋位)
位4-3 (0x18): Reserved (R, 保留,必须为0)
位2 (0x04): Key Phase (K, 密钥阶段)
位1-0 (0x03): Packet Number Length (PN长度 - 1)

16.3 延迟自旋位 (Latency Spin Bit)

自旋位是 QUIC 提供给网络观察者的一个"窗口",用于测量 RTT,但不暴露任何加密内容:

工作原理:
  客户端发包时: Spin Bit = S_client
  服务器收到包后: 将自己发出的包的 Spin Bit 设为与收到的相同
  客户端收到包后: 将自己发出的包的 Spin Bit 设为收到值的反转
观察者看到:
  → → → → → ← ← ← ← ← → → → → →
  0 0 0 0 0 1 1 1 1 1 0 0 0 0 0
                ↑                ↑
              翻转点           翻转点
  两次翻转之间的时间 ≈ 1 个 RTT

自旋位为可选功能,至少 1/16 的连接必须随机禁用它(防止被利用做流量分析)。

16.4 包号编码与解码

为了节省带宽,包头里的包号不是完整的 62 位,而是截断后的低位
编码规则(发送方):
设当前要发的完整包号为 P P P,对端已确认的最大包号为 A A A,则需要表示的范围为 P − A P - A PA,编码需要的位数 B B B 满足:
B = ⌈ log ⁡ 2 ( P − A ) ⌉ + 1 B = \lceil \log_2(P - A) \rceil + 1 B=log2(PA)⌉+1
即用能表示 2 ( P − A ) 2(P-A) 2(PA) 范围的最小位数,向上取整到 1/2/3/4 字节。
解码规则(接收方):
已知最高接收包号 L L L,收到的截断值 T T T,编码位数为 n n n
窗口大小 = 2 n , 期望包号 = L + 1 \text{窗口大小} = 2^n, \quad \text{期望包号} = L + 1 窗口大小=2n,期望包号=L+1
候选包号 = ( 期望包号 & ¬ ( 2 n − 1 ) ) ∣ T \text{候选包号} = (\text{期望包号} \& \neg(2^n - 1)) \mid T 候选包号=(期望包号(2n1))T
若候选值 ≤ \leq 期望值 − 2 n − 1 - 2^{n-1} 2n1,则加 2 n 2^n 2n;若候选值 > > > 期望值 + 2 n − 1 + 2^{n-1} +2n1,则减 2 n 2^n 2n

17. 帧类型与格式详解

17.1 STREAM 帧 — 最重要的帧

STREAM 帧携带应用数据,类型字段本身携带标志位:

STREAM 帧类型字节: 0b0000 1 XXX
                              ↑↑↑
                              |||── 位0 (FIN): 1=这是流的最后一帧
                              ||─── 位1 (LEN): 1=存在 Length 字段
                              |──── 位2 (OFF): 1=存在 Offset 字段
STREAM 帧格式:
┌─────────────────────────────────────────────────────┐
│ 类型 (变长整数)   0x08 ~ 0x0f                       │
│ Stream ID (变长整数)                                 │
│ [Offset (变长整数)]  -- 仅当 OFF=1 时存在           │
│ [Length (变长整数)]  -- 仅当 LEN=1 时存在           │
│ 数据 (实际字节)                                      │
└─────────────────────────────────────────────────────┘

Offset 的重要性: 发送方可能乱序发送,接收方靠 Offset 重排。

17.2 ACK 帧

ACK 帧格式:
┌─────────────────────────────────────────────────────┐
│ 类型: 0x02 或 0x03 (0x03 含 ECN 信息)              │
│ Largest Acknowledged (变长整数)                     │
│ ACK Delay (变长整数, 微秒)                          │
│ ACK Range Count (变长整数)                          │
│ First ACK Range (变长整数)                          │
│ [Gap (变长整数), ACK Range (变长整数)] × Range Count│
│ [ECN Counts] -- 仅当类型为 0x03 时                 │
└─────────────────────────────────────────────────────┘
ACK Delay 的解码:
  实际延迟(微秒) = ACK_Delay_字段值 × 2^ack_delay_exponent

17.3 CONNECTION_CLOSE 帧

CONNECTION_CLOSE 帧格式:
┌─────────────────────────────────────────────────────┐
│ 类型: 0x1c (传输层错误) 或 0x1d (应用层错误)       │
│ Error Code (变长整数)                               │
│ [Frame Type] -- 仅当类型为 0x1c 时存在             │
│ Reason Phrase Length (变长整数)                     │
│ Reason Phrase (UTF-8 字节)                          │
└─────────────────────────────────────────────────────┘

17.4 NEW_CONNECTION_ID 帧

NEW_CONNECTION_ID 帧格式:
┌─────────────────────────────────────────────────────┐
│ 类型: 0x18                                          │
│ Sequence Number (变长整数)                          │
│ Retire Prior To (变长整数) -- 要求对端退役哪些ID   │
│ Length (8位整数) -- 连接ID长度 (1~20)              │
│ Connection ID (Length 字节)                         │
│ Stateless Reset Token (128位 = 16字节)              │
└─────────────────────────────────────────────────────┘

18. 错误码大全


错误码 名称 含义
0x00 NO_ERROR 无错误,正常关闭
0x01 INTERNAL_ERROR 内部错误
0x02 CONNECTION_REFUSED 服务器拒绝连接
0x03 FLOW_CONTROL_ERROR 超出流量控制限制
0x04 STREAM_LIMIT_ERROR 超出流数量限制
0x05 STREAM_STATE_ERROR 在错误状态收到帧
0x06 FINAL_SIZE_ERROR 流的最终大小矛盾
0x07 FRAME_ENCODING_ERROR 帧格式错误
0x08 TRANSPORT_PARAMETER_ERROR 传输参数错误
0x09 CONNECTION_ID_LIMIT_ERROR 连接ID数量超限
0x0a PROTOCOL_VIOLATION 协议违规(通用)
0x0b INVALID_TOKEN Token 无效
0x0c APPLICATION_ERROR 应用层错误
0x0d CRYPTO_BUFFER_EXCEEDED CRYPTO 缓冲区溢出
0x0e KEY_UPDATE_ERROR 密钥更新错误
0x0f AEAD_LIMIT_REACHED AEAD 使用次数上限
0x10 NO_VIABLE_PATH 无可用网络路径
0x0100~0x01ff CRYPTO_ERROR TLS 告警码(256个)

19. 安全考量

19.1 放大攻击防御

QUIC 的三倍限制:
  服务器在地址验证前:
    可发字节数 ≤ 3 × 已收字节数
  客户端发 Initial 包时:
    必须填充到至少 1200 字节
    (给服务器足够的"三倍预算"完成握手)

19.2 请求伪造攻击 (Request Forgery)

QUIC 的几个机制可能被滥用来让服务器向第三方发包:

攻击场景:
  攻击者 → 建立QUIC连接
  攻击者 → 声称"我的新地址是受害者IP"
  服务器 → 向受害者发路径验证包 (PATH_CHALLENGE)
         → 发大量应用数据 (如果被骗以为地址已验证)
防御:
  服务器在验证地址前,只发探测帧
  使用 ingress filtering 防止 IP 欺骗

19.3 慢速攻击 (Slowloris)

攻击者打开大量连接,每个连接发送尽可能少的数据维持存活,耗尽服务器资源。
防御措施:

  • 限制单 IP 的连接数
  • 最小传输速率限制
  • 限制连接最长存活时间

19.4 连接迁移安全性

攻击1: 路径上的欺骗
  攻击者拦截包 → 修改源地址为自己 → 服务器被骗迁移到攻击者
  对策: 路径验证 (PATH_CHALLENGE/PATH_RESPONSE)
攻击2: 路径外的转发
  攻击者转发复制包,让自己成为中间人
  对策: 收到原始路径的更高包号数据时,切回原始路径
攻击3: 虚假迁移后的验证
  对策: 迁移后立即验证新旧两条路径

20. 完整 C++ 演示代码

以下代码演示了 QUIC 协议的几个关键概念的实现:可变长整数编解码、包号编解码、流状态机和流量控制器。

/**
 * QUIC 协议核心概念演示代码
 * 演示: 可变长整数编解码、包号编解码、流状态机、流量控制器
 *
 * 编译: g++ -std=c++17 -o quic_demo quic_demo.cpp
 */
#include <iostream>
#include <vector>
#include <string>
#include <cstdint>
#include <cassert>
#include <optional>
#include <stdexcept>
#include <algorithm>
#include <map>
#include <sstream>
#include <iomanip>
// ============================================================
// 第一部分: 可变长整数 (Variable-Length Integer)
// 对应 RFC 9000 Section 16
// ============================================================
/**
 * 将可变长整数编码到字节缓冲区
 *
 * 编码规则:
 *   最高2位表示总字节数:
 *   00 → 1字节,可表示 0~63
 *   01 → 2字节,可表示 0~16383
 *   10 → 4字节,可表示 0~1073741823
 *   11 → 8字节,可表示 0~4611686018427387903
 *
 * @param value  要编码的值 (必须 < 2^62)
 * @param out    输出字节缓冲区
 */
void encodeVarint(uint64_t value, std::vector<uint8_t>& out) {
    if (value > (UINT64_C(1) << 62) - 1) {
        throw std::overflow_error("QUIC varint: value exceeds 62-bit maximum");
    }
    if (value <= 63) {
        // 1字节: 最高2位 = 00
        out.push_back(static_cast<uint8_t>(value));
    } else if (value <= 16383) {
        // 2字节: 最高2位 = 01
        // 先设置最高2位为 01,然后存低14位
        out.push_back(static_cast<uint8_t>(0x40 | (value >> 8)));
        out.push_back(static_cast<uint8_t>(value & 0xFF));
    } else if (value <= 1073741823) {
        // 4字节: 最高2位 = 10
        out.push_back(static_cast<uint8_t>(0x80 | (value >> 24)));
        out.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
        out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
        out.push_back(static_cast<uint8_t>(value & 0xFF));
    } else {
        // 8字节: 最高2位 = 11
        out.push_back(static_cast<uint8_t>(0xC0 | (value >> 56)));
        out.push_back(static_cast<uint8_t>((value >> 48) & 0xFF));
        out.push_back(static_cast<uint8_t>((value >> 40) & 0xFF));
        out.push_back(static_cast<uint8_t>((value >> 32) & 0xFF));
        out.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
        out.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
        out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
        out.push_back(static_cast<uint8_t>(value & 0xFF));
    }
}
/**
 * 从字节缓冲区解码可变长整数
 *
 * @param data    输入字节缓冲区
 * @param offset  当前读取位置 (解码后会前进)
 * @return        解码出的值
 */
uint64_t decodeVarint(const std::vector<uint8_t>& data, size_t& offset) {
    if (offset >= data.size()) {
        throw std::out_of_range("QUIC varint: buffer underflow");
    }
    uint8_t first = data[offset++];
    // 取最高2位决定总长度
    uint8_t prefix = first >> 6;        // 0, 1, 2, 或 3
    size_t length = static_cast<size_t>(1) << prefix; // 1, 2, 4, 或 8
    // 清除最高2位,得到值的高位部分
    uint64_t value = first & 0x3F;
    // 读取剩余字节 (大端序)
    for (size_t i = 1; i < length; ++i) {
        if (offset >= data.size()) {
            throw std::out_of_range("QUIC varint: buffer underflow during read");
        }
        value = (value << 8) | data[offset++];
    }
    return value;
}
// ============================================================
// 第二部分: 包号编解码
// 对应 RFC 9000 Section 17.1, Appendix A.2, A.3
// ============================================================
/**
 * 编码包号 — 选择合适的字节数
 * 对应 RFC 9000 Appendix A.2
 *
 * @param full_pn       完整的包号 (62位)
 * @param largest_acked 已确认的最大包号 (可能不存在)
 * @return              编码后的包号和字节数
 */
struct EncodedPacketNumber {
    uint32_t value;    // 截断后的值(最多32位)
    int      num_bytes; // 使用几个字节 (1, 2, 3, 或 4)
};
EncodedPacketNumber encodePacketNumber(
    uint64_t full_pn,
    std::optional<uint64_t> largest_acked)
{
    // 计算未确认的包数量
    uint64_t num_unacked;
    if (!largest_acked.has_value()) {
        // 还没收到任何ACK,需要表示从0到当前的所有包
        num_unacked = full_pn + 1;
    } else {
        num_unacked = full_pn - largest_acked.value();
    }
    // 需要的最小位数: log2(num_unacked) + 1
    // 用于保证接收方能在 [full_pn - 2^(n-1), full_pn + 2^(n-1)] 窗口内解码
    int min_bits = 1;
    uint64_t range = num_unacked;
    while (range > 1) {
        range >>= 1;
        min_bits++;
    }
    // 加1位是为了表示两倍范围
    min_bits++;
    // 向上取整到字节
    int num_bytes = (min_bits + 7) / 8;
    if (num_bytes > 4) num_bytes = 4; // QUIC 包号最多4字节
    // 截取低 num_bytes*8 位
    uint32_t mask = (num_bytes == 4)
        ? 0xFFFFFFFF
        : (1u << (num_bytes * 8)) - 1u;
    uint32_t truncated = static_cast<uint32_t>(full_pn) & mask;
    return {truncated, num_bytes};
}
/**
 * 解码包号 — 重建完整的62位包号
 * 对应 RFC 9000 Appendix A.3
 *
 * @param largest_pn    本空间内已成功处理的最大完整包号
 * @param truncated_pn  包头中读到的截断包号
 * @param pn_nbits      包头中包号占的位数 (8, 16, 24, 或 32)
 * @return              恢复出的完整包号
 */
uint64_t decodePacketNumber(
    uint64_t largest_pn,
    uint32_t truncated_pn,
    int      pn_nbits)
{
    uint64_t expected_pn = largest_pn + 1;   // 期望收到的包号
    uint64_t pn_win  = UINT64_C(1) << pn_nbits; // 编码窗口大小
    uint64_t pn_hwin = pn_win >> 1;              // 半个窗口
    uint64_t pn_mask = pn_win - 1;               // 低位掩码
    // 候选值: 把 expected_pn 的高位 + 截断值
    uint64_t candidate = (expected_pn & ~pn_mask) | static_cast<uint64_t>(truncated_pn);
    // 调整候选值到正确的窗口内
    if (candidate <= expected_pn - pn_hwin &&
        candidate < (UINT64_C(1) << 62) - pn_win) {
        // 候选值太小,往上移一个窗口
        return candidate + pn_win;
    }
    if (candidate > expected_pn + pn_hwin &&
        candidate >= pn_win) {
        // 候选值太大,往下移一个窗口
        return candidate - pn_win;
    }
    return candidate;
}
// ============================================================
// 第三部分: 流状态机
// 对应 RFC 9000 Section 3
// ============================================================
/**
 * 流发送端的状态
 */
enum class SendStreamState {
    READY,       // 就绪,等待应用数据
    SEND,        // 发送中
    DATA_SENT,   // 所有数据(含FIN)已发出,等待ACK
    DATA_RECVD,  // 终止: 所有数据已被确认
    RESET_SENT,  // 已发 RESET_STREAM,等待ACK
    RESET_RECVD  // 终止: RESET 已被确认
};
/**
 * 流接收端的状态
 */
enum class RecvStreamState {
    RECV,        // 接收中
    SIZE_KNOWN,  // 已知流的总大小 (收到了 FIN)
    DATA_RECVD,  // 所有数据已到,等待应用读取
    DATA_READ,   // 终止: 应用已读完所有数据
    RESET_RECVD, // 收到 RESET_STREAM
    RESET_READ   // 终止: 应用已收到重置通知
};
// 状态名称转字符串(方便打印)
const char* sendStateToStr(SendStreamState s) {
    switch (s) {
        case SendStreamState::READY:       return "Ready";
        case SendStreamState::SEND:        return "Send";
        case SendStreamState::DATA_SENT:   return "Data Sent";
        case SendStreamState::DATA_RECVD:  return "Data Recvd (terminal)";
        case SendStreamState::RESET_SENT:  return "Reset Sent";
        case SendStreamState::RESET_RECVD: return "Reset Recvd (terminal)";
    }
    return "Unknown";
}
const char* recvStateToStr(RecvStreamState s) {
    switch (s) {
        case RecvStreamState::RECV:        return "Recv";
        case RecvStreamState::SIZE_KNOWN:  return "Size Known";
        case RecvStreamState::DATA_RECVD:  return "Data Recvd";
        case RecvStreamState::DATA_READ:   return "Data Read (terminal)";
        case RecvStreamState::RESET_RECVD: return "Reset Recvd";
        case RecvStreamState::RESET_READ:  return "Reset Read (terminal)";
    }
    return "Unknown";
}
/**
 * QUIC 流的状态机实现
 * 同时维护发送端和接收端状态(双向流需要)
 */
class QuicStream {
public:
    uint64_t stream_id;
    SendStreamState send_state = SendStreamState::READY;
    RecvStreamState recv_state = RecvStreamState::RECV;
    explicit QuicStream(uint64_t id) : stream_id(id) {}
    // ---- 发送端事件 ----
    /**
     * 应用开始发送数据
     * Ready → Send
     */
    bool onSendData() {
        if (send_state == SendStreamState::READY) {
            send_state = SendStreamState::SEND;
            std::cout << "  [流" << stream_id << "] 发送端: Ready → Send\n";
            return true;
        }
        return false;
    }
    /**
     * 应用发送了最后一个字节(含 FIN)
     * Send → Data Sent
     */
    bool onSendFin() {
        if (send_state == SendStreamState::SEND) {
            send_state = SendStreamState::DATA_SENT;
            std::cout << "  [流" << stream_id << "] 发送端: Send → Data Sent (FIN发出)\n";
            return true;
        }
        return false;
    }
    /**
     * 所有数据的 ACK 都收到了
     * Data Sent → Data Recvd
     */
    bool onAllDataAcked() {
        if (send_state == SendStreamState::DATA_SENT) {
            send_state = SendStreamState::DATA_RECVD;
            std::cout << "  [流" << stream_id << "] 发送端: Data Sent → Data Recvd (终止)\n";
            return true;
        }
        return false;
    }
    /**
     * 发送了 RESET_STREAM
     * Ready/Send/Data Sent → Reset Sent
     */
    bool onSendReset() {
        if (send_state == SendStreamState::READY ||
            send_state == SendStreamState::SEND ||
            send_state == SendStreamState::DATA_SENT) {
            auto old_state = send_state;
            send_state = SendStreamState::RESET_SENT;
            std::cout << "  [流" << stream_id << "] 发送端: "
                      << sendStateToStr(old_state) << " → Reset Sent\n";
            return true;
        }
        return false;
    }
    /**
     * RESET_STREAM 的 ACK 收到了
     * Reset Sent → Reset Recvd
     */
    bool onResetAcked() {
        if (send_state == SendStreamState::RESET_SENT) {
            send_state = SendStreamState::RESET_RECVD;
            std::cout << "  [流" << stream_id << "] 发送端: Reset Sent → Reset Recvd (终止)\n";
            return true;
        }
        return false;
    }
    // ---- 接收端事件 ----
    /**
     * 收到 STREAM 帧 (不含 FIN)
     */
    bool onRecvStreamData() {
        if (recv_state == RecvStreamState::RECV) {
            std::cout << "  [流" << stream_id << "] 接收端: Recv (正在接收数据)\n";
            return true;
        }
        return false;
    }
    /**
     * 收到含 FIN 的 STREAM 帧
     * Recv → Size Known
     */
    bool onRecvFin() {
        if (recv_state == RecvStreamState::RECV) {
            recv_state = RecvStreamState::SIZE_KNOWN;
            std::cout << "  [流" << stream_id << "] 接收端: Recv → Size Known (收到FIN)\n";
            return true;
        }
        return false;
    }
    /**
     * 所有字节都已到达
     * Size Known → Data Recvd
     */
    bool onAllDataReceived() {
        if (recv_state == RecvStreamState::SIZE_KNOWN) {
            recv_state = RecvStreamState::DATA_RECVD;
            std::cout << "  [流" << stream_id << "] 接收端: Size Known → Data Recvd\n";
            return true;
        }
        return false;
    }
    /**
     * 应用读完了所有数据
     * Data Recvd → Data Read
     */
    bool onAppReadAll() {
        if (recv_state == RecvStreamState::DATA_RECVD) {
            recv_state = RecvStreamState::DATA_READ;
            std::cout << "  [流" << stream_id << "] 接收端: Data Recvd → Data Read (终止)\n";
            return true;
        }
        return false;
    }
    /**
     * 收到 RESET_STREAM 帧
     * Recv / Size Known → Reset Recvd
     */
    bool onRecvReset() {
        if (recv_state == RecvStreamState::RECV ||
            recv_state == RecvStreamState::SIZE_KNOWN) {
            recv_state = RecvStreamState::RESET_RECVD;
            std::cout << "  [流" << stream_id << "] 接收端: → Reset Recvd\n";
            return true;
        }
        return false;
    }
    /**
     * 应用得知了重置事件
     * Reset Recvd → Reset Read
     */
    bool onAppReadReset() {
        if (recv_state == RecvStreamState::RESET_RECVD) {
            recv_state = RecvStreamState::RESET_READ;
            std::cout << "  [流" << stream_id << "] 接收端: Reset Recvd → Reset Read (终止)\n";
            return true;
        }
        return false;
    }
    void printState() const {
        std::cout << "  流[" << stream_id << "]: 发送=" << sendStateToStr(send_state)
                  << ", 接收=" << recvStateToStr(recv_state) << "\n";
    }
};
// ============================================================
// 第四部分: 流量控制器
// 对应 RFC 9000 Section 4
// ============================================================
/**
 * 连接级流量控制器
 * 追踪所有流的累计字节数,确保不超过 MAX_DATA
 */
class ConnectionFlowController {
public:
    explicit ConnectionFlowController(uint64_t initial_limit)
        : limit_(initial_limit), bytes_sent_(0)
    {}
    /**
     * 检查是否可以再发送 n 个字节
     * @return true=可以发, false=被流控阻塞
     */
    bool canSend(uint64_t n) const {
        return bytes_sent_ + n <= limit_;
    }
    /**
     * 记录发送了 n 个字节
     */
    bool consumeCredit(uint64_t n) {
        if (!canSend(n)) return false;
        bytes_sent_ += n;
        return true;
    }
    /**
     * 收到 MAX_DATA 帧,更新限制
     * 注意: 限制只能增大,不能减小
     */
    void updateLimit(uint64_t new_limit) {
        if (new_limit > limit_) {
            std::cout << "  连接流控: MAX_DATA 更新 " << limit_
                      << " → " << new_limit << "\n";
            limit_ = new_limit;
        }
    }
    uint64_t available() const { return limit_ - bytes_sent_; }
    uint64_t bytesSent()  const { return bytes_sent_; }
    uint64_t limit()      const { return limit_; }
private:
    uint64_t limit_;       // 当前的 MAX_DATA 值
    uint64_t bytes_sent_;  // 已发送的总字节数
};
/**
 * 流级流量控制器
 * 每条流独立维护,追踪该流的最大发送偏移
 */
class StreamFlowController {
public:
    explicit StreamFlowController(uint64_t initial_limit)
        : limit_(initial_limit), max_offset_sent_(0)
    {}
    /**
     * 检查能否在 current_offset 处发送 n 字节
     * 条件: current_offset + n <= limit
     */
    bool canSend(uint64_t current_offset, uint64_t n) const {
        return current_offset + n <= limit_;
    }
    /**
     * 记录在 offset 处发送了 n 字节
     */
    bool consumeCredit(uint64_t offset, uint64_t n) {
        if (!canSend(offset, n)) return false;
        uint64_t new_max = offset + n;
        if (new_max > max_offset_sent_) {
            max_offset_sent_ = new_max;
        }
        return true;
    }
    /**
     * 收到 MAX_STREAM_DATA 帧
     */
    void updateLimit(uint64_t new_limit) {
        if (new_limit > limit_) {
            std::cout << "  流控: MAX_STREAM_DATA 更新 " << limit_
                      << " → " << new_limit << "\n";
            limit_ = new_limit;
        }
    }
    uint64_t limit()         const { return limit_; }
    uint64_t maxOffsetSent() const { return max_offset_sent_; }
private:
    uint64_t limit_;            // MAX_STREAM_DATA
    uint64_t max_offset_sent_;  // 当前已发到的最大偏移
};
// ============================================================
// 第五部分: 流 ID 工具函数
// 对应 RFC 9000 Section 2.1
// ============================================================
/**
 * 流类型判断工具
 * 流ID的最低两位编码了类型信息
 */
struct StreamIdInfo {
    bool is_client_initiated;   // true=客户端发起, false=服务器发起
    bool is_bidirectional;      // true=双向流, false=单向流
    uint64_t stream_number;     // 流的序号 (去掉类型位后的值)
};
StreamIdInfo parseStreamId(uint64_t stream_id) {
    return {
        (stream_id & 0x01) == 0,        // 位0=0 → 客户端发起
        (stream_id & 0x02) == 0,        // 位1=0 → 双向流
        stream_id >> 2                   // 高位是流序号
    };
}
/**
 * 构造流 ID
 * @param stream_number       流的序号 (0, 1, 2, ...)
 * @param is_client_initiated 客户端=true, 服务器=false
 * @param is_bidirectional    双向=true, 单向=false
 */
uint64_t makeStreamId(uint64_t stream_number,
                       bool is_client_initiated,
                       bool is_bidirectional) {
    uint64_t id = stream_number << 2;
    if (!is_client_initiated) id |= 0x01;  // 位0: 发起方
    if (!is_bidirectional)    id |= 0x02;  // 位1: 方向
    return id;
}
// ============================================================
// 测试与演示主函数
// ============================================================
void separatorLine(const std::string& title) {
    std::cout << "\n" << std::string(60, '=') << "\n";
    std::cout << "  " << title << "\n";
    std::cout << std::string(60, '=') << "\n";
}
void demoVarintCoding() {
    separatorLine("演示 1: 可变长整数编解码");
    // 测试不同大小的值
    std::vector<uint64_t> test_values = {
        0, 37, 63,           // 1字节范围
        64, 15293, 16383,    // 2字节范围
        16384, 494878333,    // 4字节范围
        494878334, 151288809941952652ULL // 8字节范围
    };
    for (uint64_t val : test_values) {
        std::vector<uint8_t> buf;
        encodeVarint(val, buf);
        // 打印编码后的字节
        std::cout << "  值=" << std::setw(20) << val
                  << " → " << buf.size() << "字节: [";
        for (size_t i = 0; i < buf.size(); ++i) {
            if (i > 0) std::cout << " ";
            std::cout << "0x" << std::hex << std::setw(2)
                      << std::setfill('0') << (int)buf[i];
        }
        std::cout << std::dec << std::setfill(' ') << "]\n";
        // 验证解码
        size_t offset = 0;
        uint64_t decoded = decodeVarint(buf, offset);
        assert(decoded == val && "编解码不一致!");
        assert(offset == buf.size() && "偏移量不正确!");
    }
    std::cout << "  所有编解码测试通过!\n";
}
void demoPacketNumberCoding() {
    separatorLine("演示 2: 包号编解码");
    // 场景: 已确认包号 0xabe8b3, 当前包号 0xac5c02
    uint64_t largest_acked = 0xabe8b3;
    uint64_t current_pn    = 0xac5c02;
    std::cout << "  场景: 最大已确认包号=0x" << std::hex << largest_acked
              << ", 当前包号=0x" << current_pn << std::dec << "\n";
    auto encoded = encodePacketNumber(current_pn, largest_acked);
    std::cout << "  编码结果: 值=0x" << std::hex << encoded.value
              << ", 使用" << std::dec << encoded.num_bytes << "字节\n";
    // 解码
    uint64_t decoded = decodePacketNumber(
        largest_acked,
        encoded.value,
        encoded.num_bytes * 8
    );
    std::cout << "  解码结果: 0x" << std::hex << decoded << std::dec << "\n";
    assert(decoded == current_pn && "包号编解码不一致!");
    std::cout << "  包号编解码测试通过!\n";
    // 额外验证 RFC 中的例子: 收到截断值 0x9b32,最大已处理包 0xa82f30ea
    std::cout << "\n  RFC 示例验证:\n";
    uint64_t largest_pn = 0xa82f30ea;
    uint32_t truncated  = 0x9b32;
    uint64_t result = decodePacketNumber(largest_pn, truncated, 16);
    std::cout << "  最大已处理=0x" << std::hex << largest_pn
              << ", 截断值=0x" << truncated
              << " → 完整包号=0x" << result << std::dec << "\n";
    assert(result == 0xa82f9b32ULL && "RFC示例验证失败!");
    std::cout << "  RFC示例验证通过!\n";
}
void demoStreamStateMachine() {
    separatorLine("演示 3: 流状态机");
    std::cout << "\n--- 场景A: 正常双向流的完整生命周期 ---\n";
    {
        // 客户端发起的双向流,流ID=0
        QuicStream stream(0);
        stream.printState();
        stream.onSendData();       // Ready → Send
        stream.onRecvStreamData(); // 接收端: Recv(保持)
        stream.onRecvFin();        // Recv → Size Known
        stream.onAllDataReceived();// Size Known → Data Recvd
        stream.onSendFin();        // Send → Data Sent
        stream.onAllDataAcked();   // Data Sent → Data Recvd(终止)
        stream.onAppReadAll();     // Data Recvd → Data Read(终止)
        stream.printState();
    }
    std::cout << "\n--- 场景B: 发送方重置流 ---\n";
    {
        QuicStream stream(4); // 客户端发起的第二条双向流
        stream.printState();
        stream.onSendData();   // Ready → Send
        stream.onSendReset();  // Send → Reset Sent
        stream.onResetAcked(); // Reset Sent → Reset Recvd (终止)
        stream.printState();
    }
    std::cout << "\n--- 场景C: 接收方收到重置 ---\n";
    {
        QuicStream stream(2); // 客户端发起的单向流
        stream.printState();
        stream.onRecvStreamData(); // 接收中
        stream.onRecvReset();      // Recv → Reset Recvd
        stream.onAppReadReset();   // Reset Recvd → Reset Read (终止)
        stream.printState();
    }
}
void demoFlowControl() {
    separatorLine("演示 4: 流量控制");
    // 初始连接级限制: 10000 字节
    ConnectionFlowController conn_fc(10000);
    // 流1 的初始限制: 4000 字节
    StreamFlowController stream1_fc(4000);
    std::cout << "  初始状态: 连接限制=" << conn_fc.limit()
              << ", 流1限制=" << stream1_fc.limit() << "\n\n";
    // 模拟发送 2000 字节
    uint64_t send_size = 2000;
    uint64_t offset = 0;
    std::cout << "  尝试在 offset=" << offset << " 发送 " << send_size << " 字节:\n";
    if (stream1_fc.canSend(offset, send_size) && conn_fc.canSend(send_size)) {
        stream1_fc.consumeCredit(offset, send_size);
        conn_fc.consumeCredit(send_size);
        std::cout << "    发送成功!\n";
        std::cout << "    连接已用: " << conn_fc.bytesSent()
                  << ", 剩余: " << conn_fc.available() << "\n";
    }
    // 再发 2500 字节
    offset = 2000;
    send_size = 2500;
    std::cout << "\n  尝试在 offset=" << offset << " 发送 " << send_size << " 字节:\n";
    if (!stream1_fc.canSend(offset, send_size)) {
        std::cout << "    流级流控阻塞! 流限制=" << stream1_fc.limit()
                  << ", 需要到达 offset=" << (offset + send_size) << "\n";
        // 收到 MAX_STREAM_DATA 帧,更新限制
        stream1_fc.updateLimit(5000);
        std::cout << "    收到 MAX_STREAM_DATA, 重试...\n";
        if (stream1_fc.canSend(offset, send_size) && conn_fc.canSend(send_size)) {
            stream1_fc.consumeCredit(offset, send_size);
            conn_fc.consumeCredit(send_size);
            std::cout << "    发送成功! 连接已用: " << conn_fc.bytesSent() << "\n";
        }
    }
    // 尝试发超出连接限制的数据
    offset = 4500;
    send_size = 6000; // 4500 + 6000 = 10500 > 10000
    std::cout << "\n  尝试在 offset=" << offset << " 发送 " << send_size
              << " 字节 (总计会超出连接限制 10000):\n";
    if (!conn_fc.canSend(send_size)) {
        std::cout << "    连接级流控阻塞! 已发=" << conn_fc.bytesSent()
                  << ", 试图再发=" << send_size
                  << ", 上限=" << conn_fc.limit() << "\n";
        // 收到 MAX_DATA 帧
        conn_fc.updateLimit(12000);
        if (conn_fc.canSend(send_size)) {
            std::cout << "    收到 MAX_DATA 后可以发送了!\n";
        }
    }
}
void demoStreamIdParsing() {
    separatorLine("演示 5: 流 ID 解析");
    struct TestCase {
        uint64_t id;
        const char* expected_type;
    };
    std::vector<TestCase> cases = {
        {0,  "客户端发起的双向流 #0"},
        {1,  "服务器发起的双向流 #0"},
        {2,  "客户端发起的单向流 #0"},
        {3,  "服务器发起的单向流 #0"},
        {4,  "客户端发起的双向流 #1"},
        {5,  "服务器发起的双向流 #1"},
        {8,  "客户端发起的双向流 #2"},
        {11, "服务器发起的单向流 #2"},
    };
    for (const auto& tc : cases) {
        auto info = parseStreamId(tc.id);
        std::cout << "  流ID=" << tc.id
                  << ": " << (info.is_client_initiated ? "客户端" : "服务器")
                  << "发起的"
                  << (info.is_bidirectional ? "双向" : "单向")
                  << "流 #" << info.stream_number
                  << "  [期望: " << tc.expected_type << "]\n";
        // 验证重建
        uint64_t rebuilt = makeStreamId(
            info.stream_number,
            info.is_client_initiated,
            info.is_bidirectional
        );
        assert(rebuilt == tc.id && "流ID重建失败!");
    }
    std::cout << "  所有流ID解析测试通过!\n";
}
int main() {
    std::cout << "QUIC 协议核心概念演示 (RFC 9000)\n";
    std::cout << std::string(60, '─') << "\n";
    demoVarintCoding();
    demoPacketNumberCoding();
    demoStreamStateMachine();
    demoFlowControl();
    demoStreamIdParsing();
    std::cout << "\n" << std::string(60, '=') << "\n";
    std::cout << "  所有演示完成!\n";
    std::cout << std::string(60, '=') << "\n";
    return 0;
}

代码结构说明

quic_demo.cpp
├── 第一部分: 可变长整数
│   ├── encodeVarint()  — 编码
│   └── decodeVarint()  — 解码
│
├── 第二部分: 包号编解码
│   ├── encodePacketNumber() — 选择合适字节数
│   └── decodePacketNumber() — 恢复完整包号
│
├── 第三部分: 流状态机
│   ├── SendStreamState — 发送端枚举
│   ├── RecvStreamState — 接收端枚举
│   └── QuicStream      — 状态机类
│
├── 第四部分: 流量控制
│   ├── ConnectionFlowController — 连接级
│   └── StreamFlowController     — 流级
│
└── 第五部分: 流ID工具
    ├── parseStreamId() — 解析类型
    └── makeStreamId()  — 构造ID

附录: 关键公式汇总

反放大限制: 可发字节 ≤ 3 × 已收字节 \text{反放大限制: 可发字节} \leq 3 \times \text{已收字节} 反放大限制可发字节3×已收字节
流级流控约束: offset ≤ MAX_STREAM_DATA \text{流级流控约束: offset} \leq \text{MAX\_STREAM\_DATA} 流级流控约束: offsetMAX_STREAM_DATA
∑ 所有流 最大发送偏移 ≤ MAX_DATA \sum_{\text{所有流}} \text{最大发送偏移} \leq \text{MAX\_DATA} 所有流最大发送偏移MAX_DATA
包号编码最小位数:  B = ⌈ log ⁡ 2 ( P − A ) ⌉ + 1 \text{包号编码最小位数: } B = \lceil\log_2(P - A)\rceil + 1 包号编码最小位数B=log2(PA)⌉+1
有效空闲超时:  min ⁡ ( 客户端 timeout , 服务器 timeout ) \text{有效空闲超时: } \min(\text{客户端 timeout}, \text{服务器 timeout}) 有效空闲超时min(客户端 timeout,服务器 timeout)
空闲超时下限:  3 × PTO \text{空闲超时下限: } 3 \times \text{PTO} 空闲超时下限3×PTO
Final Size = 最大已发偏移 + 1 \text{Final Size} = \text{最大已发偏移} + 1 Final Size=最大已发偏移+1
Stateless Reset Token: HMAC ( 静态密钥 , 连接ID ) [ 0 : 16 ] \text{Stateless Reset Token: } \text{HMAC}(\text{静态密钥}, \text{连接ID})[0:16] Stateless Reset Token: HMAC(静态密钥,连接ID)[0:16]
ACK Delay 实际值: 字段值 × 2 ack_delay_exponent  微秒 \text{ACK Delay 实际值: } \text{字段值} \times 2^{\text{ack\_delay\_exponent}} \text{ 微秒} ACK Delay 实际值字段值×2ack_delay_exponent 微秒

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐