RFC 6455-WebSocket学习:WebSocket 协议 RFC 6455 — 从零理解完整指南
原文: RFC 6455, IETF, December 2011
本文从零出发,用中文详细解读协议的每一个细节,配合 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(Key∥GUID)
具体数值:
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(Key∥GUID))
Base64(上面的 20 字节) = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
为什么要这样设计?
这个机制的目的不是加密,而是证明服务器确实理解 WebSocket 协议。一个普通的 HTTP 服务器或者缓存代理不会知道这个 GUID,所以无法伪造正确的 Accept 值,从而防止非 WebSocket 服务器被意外"升级"连接。
3.5 握手流程图
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=oi⊕kj
其中 ⊕ \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 连接在生命周期中经历以下状态:
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++ 示例代码。
目录
- TLS 是什么,解决什么问题
- TLS 1.3 相比 1.2 的重大改动
- 协议总览:三种握手模式
- 完整握手流程详解
- PSK 会话恢复
- 0-RTT 早期数据
- 记录层协议
- 密钥调度系统
- Alert 警告协议
- 安全性分析
- 完整 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 让客户端重试:
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 恢复握手流程
恢复时不需要 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 协议核心。
目录
- 概述 — QUIC 是什么
- 流 (Streams) — 数据的通道
- 流的状态机
- 流量控制 (Flow Control)
- 连接 (Connections)
- 版本协商
- 握手过程
- 地址验证
- 连接迁移
- 连接终止
- 错误处理
- 包与帧的结构
- 数据包化与可靠性
- 数据报大小与 PMTU
- 可变长整数编码
- 包格式详解
- 帧类型与格式详解
- 错误码大全
- 安全考量
- 完整 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 发送方状态机
一条流的发送端会经历以下几个状态:
每个状态的含义:
- Ready(就绪) — 流刚被创建,可以接收应用数据,但还没发出去。
- Send(发送中) — 正在通过 STREAM 帧传输数据,遵守对端的流量控制。
- Data Sent(数据已发) — 所有数据(含 FIN 标志)已发出,等待对端确认。
- Data Recvd(数据已确认) — 终止态,所有数据已被对端 ACK,大功告成。
- Reset Sent(重置已发) — 发送了 RESET_STREAM,告诉对端"我不发了"。
- Reset Recvd(重置已确认) — 终止态,重置操作已被对端确认。
3.2 接收方状态机
每个状态的含义:
- 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} offset≤MAX_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 N−1 处,则 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) + 传输层参数协商
↑ ↑
确定加密密钥 双方声明能力
密码学握手要求:
- 认证的密钥交换 — 服务器必须被认证,客户端可选认证
- 独立的连接密钥 — 每条连接产生不同的密钥
- 传输参数的认证交换 — 防止中间人篡改参数
- 应用协议的认证协商 — 通过 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_exponentmax_ack_delayinitial_source_connection_idoriginal_destination_connection_idpreferred_addressretry_source_connection_idstateless_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 三种终止方式
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 下一段最大=前一段最小−g−2
每个 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):
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 P−A,编码需要的位数 B B B 满足:
B = ⌈ log 2 ( P − A ) ⌉ + 1 B = \lceil \log_2(P - A) \rceil + 1 B=⌈log2(P−A)⌉+1
即用能表示 2 ( P − A ) 2(P-A) 2(P−A) 范围的最小位数,向上取整到 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 候选包号=(期望包号&¬(2n−1))∣T
若候选值 ≤ \leq ≤ 期望值 − 2 n − 1 - 2^{n-1} −2n−1,则加 2 n 2^n 2n;若候选值 > > > 期望值 + 2 n − 1 + 2^{n-1} +2n−1,则减 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} 流级流控约束: offset≤MAX_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(P−A)⌉+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 微秒
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)