本文覆盖:流优先级与依赖树、流量控制细节、各帧格式定义、错误处理、HTTP 消息映射

1. 流优先级(Stream Priority)

1.1 为什么需要优先级?

一条 TCP 连接上可能同时跑几十条流:有的是关键 CSS(阻塞渲染),有的是懒加载图片(无关紧要)。优先级机制让客户端告诉服务器:“先把资源给重要的流,次要的流稍等”。

重要:优先级只是建议,服务器可以忽略。它不保证执行顺序,只是影响资源分配倾向。

1.2 依赖树(Dependency Tree)

HTTP/2 用一棵树来表达流与流之间的依赖关系:

  • 根节点:虚拟的流 0(stream 0x0),所有流默认都依赖它
  • 父流(parent):被依赖的流
  • 子流(dependent):依赖父流的流,只有父流处理完(或阻塞)才能分到资源
默认依赖创建

流 0 (根)

流 A

流 B

流 C

现在新建流 D,默认依赖 A(非独占):

流 0 (根)

流 A

流 B

流 C

流 D (新建)

B、C、D 是平级的,顺序不固定。

独占依赖(Exclusive Dependency)

如果流 D 以独占方式依赖 A,则 D 插入 A 和 B、C 之间,B、C 变成 D 的子节点:

流 0 (根)

流 A

流 D (独占插入)

流 B

流 C

1.3 权重(Weight)

同一父节点下的子流按权重按比例分配资源,权重范围 [ 1 , 256 ] [1, 256] [1,256]
流 B 获得的资源比例 = w B w B + w C \text{流 B 获得的资源比例} = \frac{w_B}{w_B + w_C}  B 获得的资源比例=wB+wCwB
例如:B 权重 4,C 权重 12,则:
B 的比例 = 4 4 + 12 = 1 4 , C 的比例 = 12 16 = 3 4 \text{B 的比例} = \frac{4}{4 + 12} = \frac{1}{4}, \quad \text{C 的比例} = \frac{12}{16} = \frac{3}{4} 的比例=4+124=41,的比例=1612=43
即 C 获得的资源是 B 的三倍。

1.4 重优先级(Reprioritization)的复杂示例

RFC 7540 给出了一个较复杂的重排例子,原始树为:

x
|
A
├── B
└── C
    ├── D
    │   └── F
    └── E

现在把 A 改成依赖 D(A 成为 D 的子节点)。由于 D 是 A 的后代,先把 D 提升到 A 原来的位置,再把 A 挂到 D 下:
非独占模式(A 和 F 都是 D 的子节点):

x
|
D
├── F
└── A
    ├── B
    └── C
        └── E

独占模式(A 成为 D 唯一的子节点,F 变成 A 的子节点):

x
|
D
└── A
    ├── B
    ├── C
    │   └── E
    └── F

1.5 流关闭后的权重继承

当一个流从依赖树中移除时,它的子流不会凭空消失,而是上移成为被移除流的父流的子节点,并按比例重新分配权重。
例如,A 和 B 共享父节点,C 和 D 依赖 A:

  • 若 A 被移除,C 和 D 上移,和 B 一起成为 A 原父节点的子节点
  • C 和 D 获得的权重 = 原来 A 的权重按 C:D 的权重比例分配

1.6 默认优先级


情况 父流 默认权重
普通新建流 流 0(根) 16
服务器推送的流 关联的请求流 16

2. 错误处理

HTTP/2 将错误分为两类,处理方式完全不同。

2.1 连接错误(Connection Error)

影响整个连接,所有流都不能继续。
处理步骤:

  1. 发送 GOAWAY 帧(携带出错时最后成功处理的流 ID 和错误码)
  2. 关闭 TCP 连接
服务器 客户端 服务器 客户端 发现连接级别错误 HEADERS (stream 7) GOAWAY (last_stream=7, error=PROTOCOL_ERROR) TCP FIN

2.2 流错误(Stream Error)

只影响单条流,其他流正常继续。
处理方式:向出问题的流发送 RST_STREAM 帧,该流关闭,连接保持。

服务器 客户端 服务器 客户端 连接正常,其他流不受影响 DATA (stream 3, 但 stream 3 已关闭) RST_STREAM (stream 3, STREAM_CLOSED)

注意:不能对 RST_STREAM 回复 RST_STREAM,否则会造成无限循环。

2.3 错误码全表


错误码 含义
NO_ERROR 0x0 无错误(如正常关闭连接)
PROTOCOL_ERROR 0x1 协议违规(通用)
INTERNAL_ERROR 0x2 端点内部错误
FLOW_CONTROL_ERROR 0x3 违反流量控制规则
SETTINGS_TIMEOUT 0x4 SETTINGS 未及时得到 ACK
STREAM_CLOSED 0x5 流已关闭后仍收到帧
FRAME_SIZE_ERROR 0x6 帧大小不合法
REFUSED_STREAM 0x7 流在处理前被拒绝(可安全重试)
CANCEL 0x8 流不再需要,主动取消
COMPRESSION_ERROR 0x9 头部压缩上下文损坏
CONNECT_ERROR 0xa CONNECT 请求的连接异常
ENHANCE_YOUR_CALM 0xb 对方行为产生过多负载(限流)
INADEQUATE_SECURITY 0xc TLS 安全性不满足要求
HTTP_1_1_REQUIRED 0xd 要求使用 HTTP/1.1

3. 帧格式详解

3.1 DATA 帧(type=0x0)

承载请求或响应的正文数据

+---------------+
|Pad Length? (8)|          <- 仅当 PADDED 标志置位时存在
+---------------+---------+
|        Data (*)         |  <- 实际数据
+-------------------------+
|       Padding (*)       |  <- 填充字节(全为 0)
+-------------------------+

标志位

标志 含义
END_STREAM 0x1 这是本流最后一帧(触发半关闭或关闭)
PADDED 0x8 存在 Pad Length 字段和填充

规则

  • DATA 帧必须关联到某条流(stream_id 不能为 0)
  • DATA 帧受流量控制约束(包括 Pad Length 和 Padding 字节都计入窗口)
  • 只能在流处于 openhalf-closed (remote) 状态时发送
    为什么有 Padding? 填充可以混淆真实数据大小,防止流量分析攻击(侧信道攻击)。

3.2 HEADERS 帧(type=0x1)

最重要的帧,用于开启一条流并携带头部块(HTTP 请求/响应头)。

+---------------+
|Pad Length? (8)|
+-+-------------+---------+
|E|  Stream Dependency?   |  <- 仅当 PRIORITY 标志存在
+-+---+-------------------+
| Weight? (8)             |  <- 仅当 PRIORITY 标志存在
+-+---+-------------------+
|  Header Block Fragment  |  <- HPACK 压缩后的头部数据
+-------------------------+
|       Padding (*)       |
+-------------------------+

标志位

标志 含义
END_STREAM 0x1 流将在此帧(或随后的 CONTINUATION)之后关闭
END_HEADERS 0x4 头部块传输完毕,不会跟 CONTINUATION 帧
PADDED 0x8 存在填充
PRIORITY 0x20 存在优先级字段(E、Stream Dependency、Weight)

细节:头部字段名必须是小写,含大写字母的请求或响应视为畸形(malformed)。

3.3 PRIORITY 帧(type=0x2)

单独调整某条流的优先级,载荷固定 5 字节:

+-+------------------------------+
|E|   Stream Dependency (31 位)  |
+-+----------+-------------------+
| Weight (8) |
+------------+
  • E:1 位,是否独占依赖
  • Stream Dependency:依赖的父流 ID
  • Weight:权重值(存储时减 1,读取时加 1,实际范围 [ 1 , 256 ] [1, 256] [1,256]
    可以在流的任何状态下发送,包括 idle 和 closed 状态。

3.4 RST_STREAM 帧(type=0x3)

立即终止一条流,载荷固定 4 字节:

+-------------------------------+
|       Error Code (32 位)      |
+-------------------------------+
  • 不能对 idle 状态的流发 RST_STREAM
  • 不能用 RST_STREAM 回复 RST_STREAM(防无限循环)
  • 发出后必须准备好接收对方在 RST_STREAM 之前已经发出的帧

3.5 SETTINGS 帧(type=0x4)

协商连接参数,每个参数是一个 6 字节的键值对:

+-------------------------------+
|    Identifier (16 位)         |
+-------------------------------+-------------------------------+
|              Value (32 位)                                    |
+---------------------------------------------------------------+

六个标准参数

参数 ID 初始值 含义
SETTINGS_HEADER_TABLE_SIZE 0x1 4096 字节 HPACK 动态表最大大小
SETTINGS_ENABLE_PUSH 0x2 1(开启) 是否允许服务器推送
SETTINGS_MAX_CONCURRENT_STREAMS 0x3 无限制 对方最多可开多少条并发流
SETTINGS_INITIAL_WINDOW_SIZE 0x4 65535 字节 新流的初始流量控制窗口
SETTINGS_MAX_FRAME_SIZE 0x5 16384 字节( 2 14 2^{14} 214 愿意接收的最大帧载荷
SETTINGS_MAX_HEADER_LIST_SIZE 0x6 无限制 愿意接受的最大头部列表大小

SETTINGS 的 ACK 机制:收到 SETTINGS 帧后必须立即回复一个带 ACK 标志(0x1)的空 SETTINGS 帧。如果发送方等待 ACK 超时,可发送 SETTINGS_TIMEOUT 错误。

客户端                             服务器
  |                                   |
  |-- SETTINGS (MAX_STREAMS=100) ---->|
  |<-- SETTINGS (ACK) ----------------|  <- 必须回复
  |                                   |

重要约束

  • SETTINGS 必须在 stream_id=0 上发送(连接级别)
  • 载荷长度必须是 6 的倍数(否则 FRAME_SIZE_ERROR)
  • 收到 SETTINGS 后必须按顺序处理每个参数,不能跳过

3.6 PUSH_PROMISE 帧(type=0x5)

服务器主动推送前的"预告通知",在已有开放流上发送:

+---------------+
|Pad Length? (8)|
+-+-------------+---------+
|R| Promised Stream ID    |  <- 31 位,即将被推送的新流 ID
+-+------------------------+
|  Header Block Fragment  |  <- 合成的请求头(如 :path: /style.css)
+-------------------------+
|       Padding (*)       |
+-------------------------+
  • Promised Stream ID 必须是偶数(服务器发起)且当前处于 idle 状态
  • 只能在 openhalf-closed (remote) 的流上发送
  • 若客户端设置了 SETTINGS_ENABLE_PUSH=0,服务器不能发此帧
  • 客户端可以用 RST_STREAM 拒绝接受推送

3.7 PING 帧(type=0x6)

心跳检测,载荷固定 8 字节(任意内容):

+-------------------------------+
|     Opaque Data (64 位)       |
+-------------------------------+
  • 收到没有 ACK 标志的 PING,必须回复一个带 ACK 且载荷完全相同的 PING
  • 用途:检测连接是否存活;测量 RTT(从发出到收到 ACK 的时间)
  • 必须在 stream_id=0 上发送

3.8 GOAWAY 帧(type=0x7)

优雅关闭连接,告知对方最后处理到哪条流:

+-+-----------------------------+
|R|    Last-Stream-ID (31 位)   |
+-+-----------------------------+
|       Error Code (32 位)      |
+-------------------------------+
|   Additional Debug Data (*)   |  <- 可选调试信息
+-------------------------------+

两阶段优雅关闭(RFC 7540 推荐的服务器维护方案):

阶段 1:服务器发 GOAWAY,last_stream_id = 2^31-1(最大值)
  -> 告知客户端"即将关闭,别发新请求了"
等待一个 RTT(让在途的新请求到达)
阶段 2:服务器发第二个 GOAWAY,last_stream_id = 实际最后处理的流
  -> 明确告知哪些流已被处理,哪些需要客户端在新连接上重试

这样可以避免客户端丢失正在发送中的请求。

3.9 WINDOW_UPDATE 帧(type=0x8)

流量控制的核心帧,归还发送方的发送配额:

+-+-----------------------------+
|R|  Window Size Increment (31) |  <- 增量,范围 [1, 2^31-1]
+-+-----------------------------+
  • stream_id=0:更新连接级别窗口
  • stream_id=N:更新流 N 的窗口
  • 增量为 0 是错误(PROTOCOL_ERROR
  • 窗口累加后不得超过 2 31 − 1 2^{31}-1 2311,否则 FLOW_CONTROL_ERROR

3.10 CONTINUATION 帧(type=0x9)

头部块的延续帧,载荷只有一个字段:

+-------------------------------+
|  Header Block Fragment (*)    |
+-------------------------------+
  • 只有一个标志:END_HEADERS(0x4)
  • 前一帧必须是同一条流上的 HEADERSPUSH_PROMISE 或不带 END_HEADERSCONTINUATION
  • CONTINUATION 帧中间不能插入其他流的任何帧

4. 流量控制深入

4.1 初始窗口大小与 SETTINGS 的交互

连接建立时,所有流的初始窗口大小为 65535 65535 65535 字节。通过 SETTINGS 可以改变新流的初始窗口,但这会影响已存在的活跃流。
新窗口 = 旧窗口 + ( 新 SETTINGS_INITIAL_WINDOW_SIZE − 旧初始值 ) \text{新窗口} = \text{旧窗口} + (\text{新 SETTINGS\_INITIAL\_WINDOW\_SIZE} - \text{旧初始值}) 新窗口=旧窗口+( SETTINGS_INITIAL_WINDOW_SIZE旧初始值)
例如:客户端连接建立后立即发送了 60 KB 数据,然后服务器发来 SETTINGS_INITIAL_WINDOW_SIZE=16384
客户端剩余窗口 = 65535 − 60 × 1024 + ( 16384 − 65535 ) = − 44031  字节(负数!) \text{客户端剩余窗口} = 65535 - 60 \times 1024 + (16384 - 65535) = -44031 \text{ 字节(负数!)} 客户端剩余窗口=6553560×1024+(1638465535)=44031 字节(负数!)
窗口变成负数后,客户端必须停止发送,等待 WINDOW_UPDATE 把窗口拉回正数。

4.2 死锁风险

流量控制实现不当时可能发生死锁:

场景:接收方没有及时读取 TCP 缓冲区
客户端发 DATA -> TCP 缓冲区满 -> 发送阻塞
接收方想发 WINDOW_UPDATE -> 但 WINDOW_UPDATE 也被 TCP 阻塞
-> 双方都在等对方 -> 死锁!

解决方案:接收方必须及时从 TCP receive buffer 读数据,不能积压。

4.3 禁用流量控制的方法

想"禁用"流量控制(用于代理、高吞吐场景):将接收窗口设置为最大值 2 31 − 1 2^{31}-1 2311,并在每次收到数据后立刻发 WINDOW_UPDATE 归还全部额度,这样发送方永远不会被阻塞。
最大窗口大小 = 2 31 − 1 = 2,147,483,647  字节(约 2 GB) \text{最大窗口大小} = 2^{31} - 1 = 2{,}147{,}483{,}647 \text{ 字节(约 2 GB)} 最大窗口大小=2311=2,147,483,647 字节(约 2 GB

5. HTTP 消息映射

5.1 请求的帧序列

一个完整的 HTTP/2 请求由以下帧组成:

必须有:
  HEADERS [+ CONTINUATION*]    <- 包含请求头(:method, :path 等)
可选:
  DATA [多个]                   <- 请求正文(POST/PUT 时)
最后一帧带 END_STREAM 标志

对于 GET 请求(没有正文):

客户端 -> 服务器:
  HEADERS (END_HEADERS, END_STREAM)    <- 一帧搞定,同时关闭请求端

对于 POST 请求(有正文):

客户端 -> 服务器:
  HEADERS (END_HEADERS)                <- 只有头部
  DATA                                 <- 正文片段 1
  DATA (END_STREAM)                    <- 正文片段 2,且标记结束

5.2 响应的帧序列

服务器 -> 客户端:
  [HEADERS (1xx 信息响应)]*           <- 可选,0 个或多个 1xx 响应
  HEADERS (END_HEADERS)               <- 最终响应头(:status 必须存在)
  [DATA]*                             <- 响应正文
  [HEADERS (END_HEADERS, END_STREAM)] <- 可选的 Trailer 头

5.3 伪头部字段(Pseudo-Header Fields)

HTTP/2 用以 : 开头的伪头部替代 HTTP/1.1 的请求行和状态行:
请求伪头部(必须在所有普通头部之前):

伪头部 对应 HTTP/1.1 示例
:method 请求方法 GETPOST
:scheme URI 方案 httpshttp
:authority Host 头部 example.com:443
:path 请求路径+查询 /index.html?q=1

响应伪头部

伪头部 含义
:status HTTP 状态码(如 200404

对应关系:

HTTP/1.1 请求行:GET /index.html HTTP/1.1
          Host: example.com
HTTP/2 等价:
  :method: GET
  :scheme: https
  :authority: example.com
  :path: /index.html
──────────────────────────────────────────
HTTP/1.1 状态行:HTTP/1.1 200 OK
HTTP/2 等价:
  :status: 200

HTTP/2 不传输 HTTP 版本号和响应原因短语(如 “OK”、“Not Found”),这些信息被去掉了。

5.4 Cookie 的特殊处理

HTTP/1.1 中 Cookie 是单个头部,所有 cookie 用 ; 拼在一起:

cookie: a=b; c=d; e=f

HTTP/2 允许把它拆开成多个 Cookie 头部(对 HPACK 压缩更友好):

cookie: a=b
cookie: c=d
cookie: e=f

当需要把 HTTP/2 请求转发给 HTTP/1.1 服务器时,必须把多个 Cookie 头用 "; " 重新拼接回来。

5.5 禁止的 HTTP/1.1 头部

HTTP/2 禁止在消息中出现连接相关的头部,包括:

  • Connection
  • Keep-Alive
  • Proxy-Connection
  • Transfer-Encoding(HTTP/2 不支持 chunked 编码,DATA 帧本身就能分片)
  • Upgrade
    唯一例外:TE 头部可以存在,但其值只能是 trailers

6. 协议扩展机制

HTTP/2 设计了扩展点,允许在不修改核心协议的情况下添加新功能:

扩展点 说明
新帧类型 未知帧类型必须忽略并丢弃(不报错)
新 SETTINGS 参数 未知参数必须忽略
新错误码 未知错误码等同于 INTERNAL_ERROR

关键约束

  • 改变现有协议语义的扩展,必须先协商(通过 SETTINGS 或其他机制)才能使用
  • 扩展帧不能出现在头部块的中间(HEADERS/CONTINUATION 序列之间),否则是 PROTOCOL_ERROR
  • 扩展的初始默认值必须是"禁用"状态(opt-in 而非 opt-out)

7. C++ 完整模拟实现

下面的代码模拟了本文涵盖的核心内容:优先级依赖树、GOAWAY 优雅关闭、SETTINGS 参数协商、各种帧的构造与解析。

// http2_advanced.cpp
// 编译:g++ -std=c++17 -o http2_advanced http2_advanced.cpp
// 覆盖:优先级依赖树、SETTINGS协商、GOAWAY优雅关闭、帧格式构造
#include <cstdint>
#include <cstring>
#include <iostream>
#include <iomanip>
#include <map>
#include <vector>
#include <string>
#include <stdexcept>
#include <algorithm>
#include <cassert>
// ─────────────────────────────────────────────
// 帧类型与标志常量
// ─────────────────────────────────────────────
static constexpr uint8_t FRAME_DATA          = 0x0;
static constexpr uint8_t FRAME_HEADERS       = 0x1;
static constexpr uint8_t FRAME_PRIORITY      = 0x2;
static constexpr uint8_t FRAME_RST_STREAM    = 0x3;
static constexpr uint8_t FRAME_SETTINGS      = 0x4;
static constexpr uint8_t FRAME_PUSH_PROMISE  = 0x5;
static constexpr uint8_t FRAME_PING          = 0x6;
static constexpr uint8_t FRAME_GOAWAY        = 0x7;
static constexpr uint8_t FRAME_WINDOW_UPDATE = 0x8;
static constexpr uint8_t FRAME_CONTINUATION  = 0x9;
static constexpr uint8_t FLAG_END_STREAM     = 0x1;
static constexpr uint8_t FLAG_END_HEADERS    = 0x4;
static constexpr uint8_t FLAG_PADDED         = 0x8;
static constexpr uint8_t FLAG_PRIORITY       = 0x20;
static constexpr uint8_t FLAG_ACK            = 0x1;
// ─────────────────────────────────────────────
// 错误码
// ─────────────────────────────────────────────
static constexpr uint32_t ERR_NO_ERROR           = 0x0;
static constexpr uint32_t ERR_PROTOCOL_ERROR     = 0x1;
static constexpr uint32_t ERR_FLOW_CONTROL_ERROR = 0x3;
static constexpr uint32_t ERR_SETTINGS_TIMEOUT   = 0x4;
static constexpr uint32_t ERR_REFUSED_STREAM     = 0x7;
static constexpr uint32_t ERR_CANCEL             = 0x8;
// ─────────────────────────────────────────────
// SETTINGS 参数 ID
// ─────────────────────────────────────────────
static constexpr uint16_t SETTINGS_HEADER_TABLE_SIZE      = 0x1;
static constexpr uint16_t SETTINGS_ENABLE_PUSH            = 0x2;
static constexpr uint16_t SETTINGS_MAX_CONCURRENT_STREAMS = 0x3;
static constexpr uint16_t SETTINGS_INITIAL_WINDOW_SIZE    = 0x4;
static constexpr uint16_t SETTINGS_MAX_FRAME_SIZE         = 0x5;
static constexpr uint16_t SETTINGS_MAX_HEADER_LIST_SIZE   = 0x6;
// ─────────────────────────────────────────────
// HTTP/2 连接参数(SETTINGS)
// ─────────────────────────────────────────────
struct Http2Settings {
    uint32_t header_table_size      = 4096;     // HPACK 动态表大小
    bool     enable_push            = true;     // 是否允许服务器推送
    uint32_t max_concurrent_streams = UINT32_MAX; // 最大并发流(初始无限制)
    int32_t  initial_window_size    = 65535;    // 流的初始窗口
    uint32_t max_frame_size         = 16384;    // 最大帧载荷(2^14)
    uint32_t max_header_list_size   = UINT32_MAX; // 最大头部列表大小
    // 序列化为 SETTINGS 帧的载荷(每个参数 6 字节)
    std::vector<uint8_t> serialize() const {
        std::vector<uint8_t> buf;
        auto push16 = [&](uint16_t v) {
            buf.push_back((v >> 8) & 0xFF);
            buf.push_back(v & 0xFF);
        };
        auto push32 = [&](uint32_t v) {
            buf.push_back((v >> 24) & 0xFF);
            buf.push_back((v >> 16) & 0xFF);
            buf.push_back((v >>  8) & 0xFF);
            buf.push_back(v & 0xFF);
        };
        // 序列化每个参数(ID=2字节 + Value=4字节)
        push16(SETTINGS_HEADER_TABLE_SIZE); push32(header_table_size);
        push16(SETTINGS_ENABLE_PUSH);       push32(enable_push ? 1 : 0);
        push16(SETTINGS_INITIAL_WINDOW_SIZE); push32(static_cast<uint32_t>(initial_window_size));
        push16(SETTINGS_MAX_FRAME_SIZE);    push32(max_frame_size);
        return buf;
    }
    void print() const {
        std::cout << "  HEADER_TABLE_SIZE      = " << header_table_size << "\n";
        std::cout << "  ENABLE_PUSH            = " << (enable_push ? "true" : "false") << "\n";
        std::cout << "  MAX_CONCURRENT_STREAMS = ";
        if (max_concurrent_streams == UINT32_MAX) std::cout << "unlimited\n";
        else std::cout << max_concurrent_streams << "\n";
        std::cout << "  INITIAL_WINDOW_SIZE    = " << initial_window_size << "\n";
        std::cout << "  MAX_FRAME_SIZE         = " << max_frame_size << "\n";
    }
};
// ─────────────────────────────────────────────
// 优先级信息
// ─────────────────────────────────────────────
struct PriorityInfo {
    uint32_t parent_stream_id = 0;   // 依赖的父流 ID(0 = 依赖根节点)
    uint8_t  weight           = 16;  // 权重,范围 [1, 256]
    bool     exclusive        = false; // 是否是独占依赖
};
// ─────────────────────────────────────────────
// 优先级依赖树
// 管理所有流的父子关系和权重
// ─────────────────────────────────────────────
class PriorityTree {
public:
    // 节点:存储每条流的优先级信息
    struct Node {
        uint32_t stream_id;
        PriorityInfo priority;
        std::vector<uint32_t> children; // 子流 ID 列表
    };
    // 初始化:根节点是流 0
    PriorityTree() {
        nodes_[0] = Node{0, PriorityInfo{0, 16, false}, {}};
    }
    // 添加新流或设置优先级
    // exclusive=false:普通依赖(成为 parent 的子节点之一)
    // exclusive=true:独占依赖(插入到 parent 和其原有子节点之间)
    void set_priority(uint32_t stream_id, uint32_t parent_id,
                      uint8_t weight, bool exclusive)
    {
        // 确保父节点存在(可能是 idle 流,此时用默认优先级创建)
        if (nodes_.find(parent_id) == nodes_.end()) {
            nodes_[parent_id] = Node{parent_id, PriorityInfo{0, 16, false}, {}};
        }
        // 如果流已存在,先从原父节点摘除
        if (nodes_.find(stream_id) != nodes_.end()) {
            uint32_t old_parent = nodes_[stream_id].priority.parent_stream_id;
            auto& old_siblings = nodes_[old_parent].children;
            old_siblings.erase(
                std::remove(old_siblings.begin(), old_siblings.end(), stream_id),
                old_siblings.end()
            );
        }
        // 创建/更新节点
        PriorityInfo info{parent_id, weight, exclusive};
        if (nodes_.find(stream_id) == nodes_.end()) {
            nodes_[stream_id] = Node{stream_id, info, {}};
        } else {
            nodes_[stream_id].priority = info;
        }
        if (exclusive) {
            // 独占模式:把 parent 原有的所有子节点转移给 stream_id
            auto& parent_children = nodes_[parent_id].children;
            for (uint32_t child : parent_children) {
                if (child != stream_id) {
                    nodes_[child].priority.parent_stream_id = stream_id;
                    nodes_[stream_id].children.push_back(child);
                }
            }
            parent_children.clear();
            parent_children.push_back(stream_id);
        } else {
            // 普通模式:直接加入父节点的子列表
            nodes_[parent_id].children.push_back(stream_id);
        }
    }
    // 打印树结构(递归)
    void print() const {
        std::cout << "\n=== 优先级依赖树 ===\n";
        print_node(0, 0);
        std::cout << "====================\n\n";
    }
private:
    std::map<uint32_t, Node> nodes_;
    void print_node(uint32_t id, int depth) const {
        auto it = nodes_.find(id);
        if (it == nodes_.end()) return;
        const Node& n = it->second;
        // 缩进显示层级
        for (int i = 0; i < depth; ++i) std::cout << "    ";
        if (depth > 0) std::cout << "|-- ";
        if (id == 0) {
            std::cout << "[Root: stream 0]\n";
        } else {
            std::cout << "stream " << id
                      << " (weight=" << static_cast<int>(n.priority.weight)
                      << (n.priority.exclusive ? ", exclusive" : "")
                      << ")\n";
        }
        for (uint32_t child : n.children) {
            print_node(child, depth + 1);
        }
    }
};
// ─────────────────────────────────────────────
// GOAWAY 帧构造器
// ─────────────────────────────────────────────
struct GoAwayFrame {
    uint32_t last_stream_id; // 最后处理的流 ID
    uint32_t error_code;     // 关闭原因
    std::string debug_data;  // 可选调试信息
    // 序列化为字节流(不含帧头)
    std::vector<uint8_t> serialize_payload() const {
        std::vector<uint8_t> buf;
        auto push32 = [&](uint32_t v) {
            buf.push_back((v >> 24) & 0xFF);
            buf.push_back((v >> 16) & 0xFF);
            buf.push_back((v >>  8) & 0xFF);
            buf.push_back(v & 0xFF);
        };
        // Last-Stream-ID:最高位保留为 0
        push32(last_stream_id & 0x7FFFFFFF);
        push32(error_code);
        // 追加调试信息
        for (char c : debug_data) buf.push_back(static_cast<uint8_t>(c));
        return buf;
    }
    void print() const {
        const char* err_names[] = {
            "NO_ERROR", "PROTOCOL_ERROR", "INTERNAL_ERROR",
            "FLOW_CONTROL_ERROR", "SETTINGS_TIMEOUT", "STREAM_CLOSED",
            "FRAME_SIZE_ERROR", "REFUSED_STREAM", "CANCEL",
            "COMPRESSION_ERROR", "CONNECT_ERROR", "ENHANCE_YOUR_CALM",
            "INADEQUATE_SECURITY", "HTTP_1_1_REQUIRED"
        };
        std::cout << "[GOAWAY] last_stream=" << last_stream_id
                  << " error=";
        if (error_code <= 0xd) std::cout << err_names[error_code];
        else std::cout << "0x" << std::hex << error_code << std::dec;
        if (!debug_data.empty())
            std::cout << " debug=\"" << debug_data << "\"";
        std::cout << "\n";
    }
};
// ─────────────────────────────────────────────
// PING 帧的 RTT 测量模拟
// ─────────────────────────────────────────────
struct PingFrame {
    uint8_t opaque_data[8]; // 8 字节不透明数据
    // 构造一个 PING 请求(填入随机标识符)
    static PingFrame make_request(uint64_t token) {
        PingFrame f;
        for (int i = 7; i >= 0; --i) {
            f.opaque_data[i] = token & 0xFF;
            token >>= 8;
        }
        return f;
    }
    // 验证 PING 回复的载荷是否和请求相同
    bool matches(const PingFrame& reply) const {
        return std::memcmp(opaque_data, reply.opaque_data, 8) == 0;
    }
    void print(bool is_ack) const {
        std::cout << "[" << (is_ack ? "PING ACK" : "PING") << "] data=0x";
        for (int i = 0; i < 8; ++i)
            std::cout << std::hex << std::setw(2) << std::setfill('0')
                      << static_cast<int>(opaque_data[i]);
        std::cout << std::dec << std::setfill(' ') << "\n";
    }
};
// ─────────────────────────────────────────────
// 伪头部验证器
// 检查 HTTP/2 请求/响应的头部字段是否合法
// ─────────────────────────────────────────────
struct Http2Headers {
    std::map<std::string, std::string> fields;
    void add(const std::string& name, const std::string& value) {
        fields[name] = value;
    }
    // 验证请求头:必须包含 :method, :scheme, :path
    bool validate_request(std::string& error) const {
        // 所有头部名必须是小写
        for (const auto& [k, v] : fields) {
            for (char c : k) {
                if (c >= 'A' && c <= 'Z') {
                    error = "头部字段名包含大写字母:" + k;
                    return false;
                }
            }
        }
        // 必须有三个伪头部(CONNECT 方法除外)
        if (fields.count(":method") == 0) {
            error = "缺少 :method 伪头部";
            return false;
        }
        auto method = fields.at(":method");
        if (method != "CONNECT") {
            if (fields.count(":scheme") == 0) {
                error = "缺少 :scheme 伪头部";
                return false;
            }
            if (fields.count(":path") == 0) {
                error = "缺少 :path 伪头部";
                return false;
            }
            // :path 不能为空(http/https URI)
            if (fields.at(":path").empty()) {
                error = ":path 不能为空";
                return false;
            }
        }
        // 禁止出现连接相关头部
        static const std::vector<std::string> forbidden = {
            "connection", "keep-alive", "proxy-connection",
            "transfer-encoding", "upgrade"
        };
        for (const auto& f : forbidden) {
            if (fields.count(f)) {
                error = "禁止使用连接相关头部:" + f;
                return false;
            }
        }
        return true;
    }
    // 验证响应头:必须包含 :status
    bool validate_response(std::string& error) const {
        if (fields.count(":status") == 0) {
            error = "缺少 :status 伪头部";
            return false;
        }
        return true;
    }
    void print() const {
        for (const auto& [k, v] : fields)
            std::cout << "  " << k << ": " << v << "\n";
    }
};
// ─────────────────────────────────────────────
// 主函数:演示各模块
// ─────────────────────────────────────────────
int main() {
    std::cout << "=== HTTP/2 高级特性演示 ===\n\n";
    // ── 1. SETTINGS 协商 ──────────────────────
    std::cout << "【1】SETTINGS 参数协商\n";
    Http2Settings client_settings;
    client_settings.max_concurrent_streams = 100;
    client_settings.initial_window_size = 131072; // 128 KB(提高吞吐)
    client_settings.enable_push = false;          // 客户端不接受推送
    std::cout << "客户端 SETTINGS:\n";
    client_settings.print();
    auto payload = client_settings.serialize();
    std::cout << "序列化载荷大小:" << payload.size() << " 字节\n";
    std::cout << "(每个参数 6 字节,共 " << payload.size() / 6 << " 个参数)\n\n";
    // ── 2. 优先级依赖树 ────────────────────────
    std::cout << "【2】优先级依赖树演示\n";
    PriorityTree tree;
    // 建立初始树:A(1) 依赖根,B(3) C(5) 依赖 A
    tree.set_priority(1, 0, 16, false);  // 流1 依赖根,权重16
    tree.set_priority(3, 1, 4,  false);  // 流3 依赖流1,权重4
    tree.set_priority(5, 1, 12, false);  // 流5 依赖流1,权重12
    std::cout << "初始树(B=流3, C=流5 依赖 A=流1):";
    tree.print();
    // 权重比例计算
    std::cout << "流3 和 流5 的资源比例:\n";
    double ratio_3 = 4.0 / (4 + 12);
    double ratio_5 = 12.0 / (4 + 12);
    std::cout << "  流3 获得 " << ratio_3 * 100 << "% ("
              << "1/" << static_cast<int>(1.0/ratio_3) << ")\n";
    std::cout << "  流5 获得 " << ratio_5 * 100 << "% ("
              << "3/" << static_cast<int>(1.0/ratio_5) << ")\n\n";
    // 新建流7,以独占方式依赖流1
    std::cout << "新建流7,以【独占】方式依赖流1:";
    tree.set_priority(7, 1, 16, true);
    tree.print();
    // ── 3. GOAWAY 两阶段优雅关闭 ──────────────
    std::cout << "【3】GOAWAY 两阶段优雅关闭\n";
    // 阶段1:告知客户端即将关闭,但还不知道具体最后处理哪个流
    GoAwayFrame goaway1;
    goaway1.last_stream_id = (1u << 31) - 1; // 2^31-1,表示"还没决定"
    goaway1.error_code = ERR_NO_ERROR;
    goaway1.debug_data = "server maintenance";
    std::cout << "阶段1(通知即将关闭):";
    goaway1.print();
    std::cout << "(等待一个 RTT,让在途请求到达)\n";
    // 阶段2:发送实际的最后流 ID
    GoAwayFrame goaway2;
    goaway2.last_stream_id = 7; // 实际最后处理的流是 7
    goaway2.error_code = ERR_NO_ERROR;
    goaway2.debug_data = "";
    std::cout << "阶段2(确认最后流 ID):";
    goaway2.print();
    std::cout << "-> 流ID > 7 的请求可以在新连接上安全重试\n\n";
    // ── 4. PING RTT 测量 ──────────────────────
    std::cout << "【4】PING 心跳检测\n";
    PingFrame ping_req = PingFrame::make_request(0xDEADBEEF12345678ULL);
    ping_req.print(false);
    // 模拟对方回复(载荷必须完全相同)
    PingFrame ping_ack = ping_req;
    ping_ack.print(true);
    if (ping_req.matches(ping_ack)) {
        std::cout << "PING/ACK 载荷匹配,RTT 测量有效\n\n";
    }
    // ── 5. HTTP/2 请求头验证 ──────────────────
    std::cout << "【5】HTTP/2 请求头合法性验证\n";
    // 合法请求
    Http2Headers valid_req;
    valid_req.add(":method",    "POST");
    valid_req.add(":scheme",    "https");
    valid_req.add(":authority", "api.example.com");
    valid_req.add(":path",      "/v1/data");
    valid_req.add("content-type", "application/json");
    valid_req.add("content-length", "128");
    std::string err;
    std::cout << "合法 POST 请求头:\n";
    valid_req.print();
    bool ok = valid_req.validate_request(err);
    std::cout << "验证结果:" << (ok ? "通过" : "失败:" + err) << "\n\n";
    // 非法请求:使用了禁止的头部
    Http2Headers invalid_req;
    invalid_req.add(":method", "GET");
    invalid_req.add(":scheme", "https");
    invalid_req.add(":path",   "/");
    invalid_req.add("connection", "keep-alive");  // 禁止!
    std::cout << "非法请求头(含 Connection 头):\n";
    invalid_req.print();
    ok = invalid_req.validate_request(err);
    std::cout << "验证结果:" << (ok ? "通过" : "失败:" + err) << "\n\n";
    // 非法请求:缺少 :path
    Http2Headers missing_path;
    missing_path.add(":method", "GET");
    missing_path.add(":scheme", "https");
    std::cout << "非法请求头(缺少 :path):\n";
    missing_path.print();
    ok = missing_path.validate_request(err);
    std::cout << "验证结果:" << (ok ? "通过" : "失败:" + err) << "\n\n";
    // ── 6. Cookie 拆分与重组 ──────────────────
    std::cout << "【6】Cookie 拆分(HTTP/2 中对 HPACK 更友好)\n";
    std::string http1_cookie = "a=b; c=d; e=f";
    std::cout << "HTTP/1.1 格式:cookie: " << http1_cookie << "\n";
    // 拆分成多个独立的 cookie 字段
    std::vector<std::string> http2_cookies = {"a=b", "c=d", "e=f"};
    std::cout << "HTTP/2 格式:\n";
    for (const auto& c : http2_cookies)
        std::cout << "  cookie: " << c << "\n";
    // 重新拼接(转发给 HTTP/1.1 时)
    std::string rejoined;
    for (size_t i = 0; i < http2_cookies.size(); ++i) {
        if (i > 0) rejoined += "; ";
        rejoined += http2_cookies[i];
    }
    std::cout << "重新拼接(转发给 HTTP/1.1):cookie: " << rejoined << "\n";
    assert(rejoined == http1_cookie);
    std::cout << "拼接结果与原始 HTTP/1.1 格式一致\n\n";
    std::cout << "=== 演示完毕 ===\n";
    return 0;
}

7.1 预期运行输出

=== HTTP/2 高级特性演示 ===
【1】SETTINGS 参数协商
客户端 SETTINGS:
  HEADER_TABLE_SIZE      = 4096
  ENABLE_PUSH            = false
  MAX_CONCURRENT_STREAMS = 100
  INITIAL_WINDOW_SIZE    = 131072
  MAX_FRAME_SIZE         = 16384
序列化载荷大小:24 字节
(每个参数 6 字节,共 4 个参数)
【2】优先级依赖树演示
初始树(B=流3, C=流5 依赖 A=流1):
=== 优先级依赖树 ===
[Root: stream 0]
    |-- stream 1 (weight=16)
        |-- stream 3 (weight=4)
        |-- stream 5 (weight=12)
====================
流3 和 流5 的资源比例:
  流3 获得 25% (1/4)
  流5 获得 75% (1/1)
新建流7,以【独占】方式依赖流1:
=== 优先级依赖树 ===
[Root: stream 0]
    |-- stream 1 (weight=16)
        |-- stream 7 (weight=16, exclusive)
            |-- stream 3 (weight=4)
            |-- stream 5 (weight=12)
====================
【3】GOAWAY 两阶段优雅关闭
阶段1(通知即将关闭):[GOAWAY] last_stream=2147483647 error=NO_ERROR debug="server maintenance"
(等待一个 RTT,让在途请求到达)
阶段2(确认最后流 ID):[GOAWAY] last_stream=7 error=NO_ERROR
-> 流ID > 7 的请求可以在新连接上安全重试
【4】PING 心跳检测
[PING] data=0xdeadbeef12345678
[PING ACK] data=0xdeadbeef12345678
PING/ACK 载荷匹配,RTT 测量有效
【5】HTTP/2 请求头合法性验证
合法 POST 请求头:
  :authority: api.example.com
  :method: POST
  :path: /v1/data
  :scheme: https
  content-length: 128
  content-type: application/json
验证结果:通过
非法请求头(含 Connection 头):
  :method: GET
  :path: /
  :scheme: https
  connection: keep-alive
验证结果:失败:禁止使用连接相关头部:connection
非法请求头(缺少 :path):
  :method: GET
  :scheme: https
验证结果:失败:缺少 :path 伪头部
【6】Cookie 拆分(HTTP/2 中对 HPACK 更友好)
HTTP/1.1 格式:cookie: a=b; c=d; e=f
HTTP/2 格式:
  cookie: a=b
  cookie: c=d
  cookie: e=f
重新拼接(转发给 HTTP/1.1):cookie: a=b; c=d; e=f
拼接结果与原始 HTTP/1.1 格式一致
=== 演示完毕 ===

8. 总结

8.1 各帧用途速查

帧类型

DATA (0x0)
正文数据

HEADERS (0x1)
头部 + 开流

PRIORITY (0x2)
调整优先级

RST_STREAM (0x3)
强制关流

SETTINGS (0x4)
连接参数协商

PUSH_PROMISE (0x5)
服务器推送预告

PING (0x6)
心跳 / RTT

GOAWAY (0x7)
优雅关连接

WINDOW_UPDATE (0x8)
归还流量窗口

CONTINUATION (0x9)
头部延续

8.2 流量控制窗口大小范围

1 ≤ WINDOW_UPDATE 增量 ≤ 2 31 − 1 1 \leq \text{WINDOW\_UPDATE 增量} \leq 2^{31} - 1 1WINDOW_UPDATE 增量2311
0 ≤ 窗口当前值 ≤ 2 31 − 1 0 \leq \text{窗口当前值} \leq 2^{31} - 1 0窗口当前值2311
窗口超过 2 31 − 1 2^{31}-1 2311 时必须报 FLOW_CONTROL_ERROR;窗口可以变成负数(当 SETTINGS 改小初始窗口时),此时发送方必须停止发送。
HTTP/2 精髓 = 流 ⏟ 多路复用 + 帧 ⏟ 二进制 + HPACK ⏟ 头部压缩 + 优先级树 ⏟ 资源调度 + GOAWAY ⏟ 优雅关闭 \boxed{ \text{HTTP/2 精髓} = \underbrace{\text{流}}_{\text{多路复用}} + \underbrace{\text{帧}}_{\text{二进制}} + \underbrace{\text{HPACK}}_{\text{头部压缩}} + \underbrace{\text{优先级树}}_{\text{资源调度}} + \underbrace{\text{GOAWAY}}_{\text{优雅关闭}} } HTTP/2 精髓=多路复用 +二进制 +头部压缩 HPACK+资源调度 优先级树+优雅关闭 GOAWAY

HTTP/2 RFC 7540 深度中文学习笔记

涵盖内容:第 56–96 页(第 8–12 章 + 附录 A)

本笔记面向完全零基础读者,所有概念均从零开始解释。

目录

  1. 整体知识地图
  2. 第 8 章:HTTP/2 中的请求与响应语义
  3. 第 9 章:附加 HTTP 要求与注意事项
  4. 第 10 章:安全考虑
  5. 第 11 章:IANA 注册
  6. 附录 A:TLS 1.2 密码套件黑名单
  7. 综合 C++ 实战代码示例

整体知识地图

HTTP/2 RFC 7540 高级主题

第8章
HTTP语义映射

第9章
连接与TLS要求

第10章
安全考虑

第11章
IANA注册

格式错误报文处理

请求/响应帧序列示例

请求可靠性机制

服务器推送 Server Push

CONNECT隧道方法

连接复用与持久化

421状态码

TLS 1.2限制

密码套件要求

服务器权威性验证

跨协议攻击

中间人封装攻击

缓存推送安全

DoS防护

压缩安全

填充Padding

隐私指纹

第 8 章:HTTP/2 中的请求与响应语义

8.1.2.6 格式错误的请求与响应

什么是"格式错误"?

想象你发了一封信,信封格式完全正确,但里面装的是一张全是乱码的纸,或者漏掉了必须写的收件人姓名。
HTTP/2 里的"格式错误(Malformed)"就是这个意思——帧序列本身合法,但逻辑内容违反规则
格式错误的情形包括:

情形1:出现了多余的帧(不该有 DATA 帧却有了)
情形2:出现了被明令禁止的头字段
情形3:缺少必须存在的头字段(如 :method、:path 等伪头)
情形4:头字段名中包含大写字母(HTTP/2 要求全小写)
情形5:content-length 的值 ≠ 所有 DATA 帧 payload 长度之和
content-length 校验规则

content-length = ∑ i = 1 n DATA_frame_payload [ i ] \text{content-length} = \sum_{i=1}^{n} \text{DATA\_frame\_payload}[i] content-length=i=1nDATA_frame_payload[i]
若二者不等,整个请求/响应被视为格式错误。
唯一例外:某些响应(例如 204 No Content、304 Not Modified)本身就没有消息体,即使 content-length 非零,也不算错误——因为协议规定它们没有 DATA 帧,content-length 此时只是一个元数据标注。

处理规则
中间节点(代理):           不得转发格式错误的报文
检测到格式错误时:           必须当作"流错误"处理,错误码 = PROTOCOL_ERROR
服务器收到格式错误请求时:   可以先发一个 HTTP 响应,再关闭或重置流
客户端收到格式错误响应时:   绝对不能接受(must not accept)

为什么要这么严格?这是为了防御针对 HTTP 解析器的攻击。过于宽松的解析会暴露实现漏洞。

8.1.3 请求/响应示例

这一节用具体例子说明 HTTP/1.1 的报文如何映射到 HTTP/2 的帧序列。

关键概念:伪头字段(Pseudo-Header Fields)

HTTP/2 用以 : 开头的特殊字段来传递 HTTP/1.1 的"请求行"和"状态行"信息:

伪头字段 对应 HTTP/1.1 含义 示例值
:method 请求方法 GET / POST
:scheme URI scheme http / https
:path 请求路径 /resource
:authority Host 头 example.org
:status 状态码(响应用) 200 / 304

示例一:GET 请求(无消息体)

HTTP/1.1 写法:

GET /resource HTTP/1.1
Host: example.org
Accept: image/jpeg

HTTP/2 帧序列:

HEADERS 帧
  标志位: END_STREAM(无后续 DATA 帧)
          END_HEADERS(无后续 CONTINUATION 帧)
  内容:
    :method   = GET
    :scheme   = https
    :path     = /resource
    host      = example.org
    accept    = image/jpeg

为什么 END_STREAM 在 HEADERS 帧上? 因为 GET 没有消息体,流在发完头部之后就结束了,不需要 DATA 帧。

示例二:304 Not Modified 响应(无消息体)

HTTP/1.1 写法:

HTTP/1.1 304 Not Modified
ETag: "xyzzy"
Expires: Thu, 23 Jan ...

HTTP/2 帧序列:

HEADERS 帧
  标志位: END_STREAM
          END_HEADERS
  内容:
    :status = 304
    etag    = "xyzzy"
    expires = Thu, 23 Jan ...
示例三:POST 请求(有消息体)
POST /resource HTTP/1.1
Host: example.org
Content-Type: image/jpeg
Content-Length: 123
{binary data}

映射到 HTTP/2:

HEADERS 帧
  标志位: -END_STREAM(有后续 DATA)
          -END_HEADERS(有后续 CONTINUATION)
  内容:
    :method = POST
    :path   = /resource
    :scheme = https
CONTINUATION 帧
  标志位: +END_HEADERS(头部在此结束)
  内容:
    content-type   = image/jpeg
    host           = example.org
    content-length = 123
DATA 帧
  标志位: +END_STREAM(流在此结束)
  内容: {binary data}

帧序列流程图

服务器 客户端 服务器 客户端 POST 请求示例 :method=POST :path=/resource content-type, host, content-length binary data :status=200, content-type, content-length binary data HEADERS (-END_STREAM, -END_HEADERS) CONTINUATION (+END_HEADERS) DATA (+END_STREAM) HEADERS (-END_STREAM, +END_HEADERS) DATA (+END_STREAM)
示例四:含 100 Continue + Trailer 的完整交互

这是最复杂的案例。客户端请求包含 Expect: 100-continue,服务器先回一个"先别急"的信号,再回正式响应,最后还有尾部头(Trailer)。

客户端   --> 服务器: HEADERS(请求头,带 Expect: 100-continue)
服务器   --> 客户端: HEADERS(:status=100 Continue,-END_STREAM,+END_HEADERS)
客户端   --> 服务器: DATA(正式消息体,-END_STREAM)
服务器   --> 客户端: HEADERS(:status=200 OK,-END_STREAM,+END_HEADERS)
服务器   --> 客户端: DATA(响应体,-END_STREAM)
服务器   --> 客户端: HEADERS(Trailer 尾部头,+END_STREAM,+END_HEADERS)
                     例如: foo = bar

Trailer(尾部头) 是什么?它是紧跟在 DATA 帧之后发送的附加头字段,携带 END_STREAM 标志,表示流正式结束。在 HTTP/1.1 的 Chunked 编码中也有这个概念。

8.1.4 请求可靠性机制

问题背景

HTTP/1.1 的一个老难题:当网络连接在发送请求后突然断掉,客户端无法知道服务器是否已经开始处理请求
对于幂等操作(GET、DELETE)可以重试;但对于非幂等操作(POST——比如"支付"),重试就可能导致"重复支付"!

HTTP/2 提供的两种保障机制

机制一:GOAWAY 帧的 Last-Stream-ID
GOAWAY 帧里包含一个"我处理到了哪个流编号"的字段。

GOAWAY 帧
  Last-Stream-ID = N
含义:
  流编号 <= N 的请求:可能已处理,不要重试(除非确认)
  流编号 >  N 的请求:绝对没处理,可以安全重试
流编号示意:
  Stream 1  [已处理]    --|
  Stream 3  [已处理]    --|  Last-Stream-ID = 5
  Stream 5  [已处理]    --|
  Stream 7  [未处理]    --> 安全重试
  Stream 9  [未处理]    --> 安全重试

机制二:RST_STREAM 帧 + REFUSED_STREAM 错误码
服务器用 RST_STREAM(错误码 = REFUSED_STREAM)重置一个流,意思是:
“这个流我根本没有开始处理,你可以放心重试。”
规则:

  • 服务器说"没处理"之前,必须确实没有处理。一旦把帧传给应用层,就不能用 REFUSED_STREAM 了。
  • GOAWAY 的 Last-Stream-ID 也必须 >= REFUSED_STREAM 所在的流编号,保持一致性。
    PING 帧的辅助作用:PING 可以让客户端探测连接是否还活着,防止"连接已经被中间节点(NAT/负载均衡)悄悄丢弃"的情况。

8.2 服务器推送(Server Push)

什么是服务器推送?

传统 HTTP:浏览器请求 HTML → 解析 HTML 发现需要 CSS/JS/图片 → 再发请求 → 等待……
HTTP/2 服务器推送:服务器发现"你请求了这个 HTML,你肯定也需要这个 CSS",主动把 CSS 推过去,不等你问。

传统流程:
客户端 --[GET /index.html]--> 服务器
客户端 <--[HTML 内容]--------- 服务器
客户端 --[GET /style.css]---> 服务器   (浏览器解析 HTML 后才知道需要)
客户端 <--[CSS 内容]---------- 服务器
推送流程:
客户端 --[GET /index.html]---> 服务器
客户端 <--[PUSH_PROMISE: /style.css]-- 服务器  (提前告知)
客户端 <--[HTML 内容]----------- 服务器
客户端 <--[CSS 内容(推送)]----- 服务器  (不用再请求)
PUSH_PROMISE 帧

服务器推送的"预告",包含:

  • 完整的请求头部(假装客户端发了这个请求)
  • Promised Stream ID:用于后续传输推送内容的流编号(必须是偶数,因为服务器发起的流用偶数编号)
    时序要求:PUSH_PROMISE 必须在引用该资源的 DATA 帧之前发送
服务器 客户端 服务器 客户端 假请求头: :method=GET, :path=/style.css Stream 2 是推送流 HEADERS (GET /index.html, Stream 1) PUSH_PROMISE (Stream 1, Promise Stream 2) HEADERS (Stream 1, :status=200, HTML内容头) DATA (Stream 1, HTML body, +END_STREAM) HEADERS (Stream 2, :status=200, CSS内容头) DATA (Stream 2, CSS body, +END_STREAM)
推送的约束条件

推送的"请求"必须满足

约束 说明
可缓存 必须是 cacheable 方法(通常是 GET)
安全方法 必须是 safe 方法(不改变服务器状态)
无请求体 推送不能有 request body
权威性 :authority 必须是服务器有权威的域名

客户端的控制权

  • 可以发送 SETTINGS_ENABLE_PUSH = 0 完全禁止推送
  • 可以发送 RST_STREAM(CANCEL 或 REFUSED_STREAM)拒绝特定推送
  • 可以用 SETTINGS_MAX_CONCURRENT_STREAMS 限制并发推送数量
    重要规则
  • 客户端不能主动推送。如果服务器收到 PUSH_PROMISE 帧,这是连接级别错误(PROTOCOL_ERROR)。
  • 推送流的状态变化:
    • 服务器发 PUSH_PROMISE → 流进入 reserved (local)
    • 客户端视角 → 流进入 reserved (remote)
    • 服务器发送推送内容的 HEADERS → 流变为 half-closed (remote) (服务器视角)
    • 服务器发 END_STREAM → 流变为 closed
    • 客户端永远不发 END_STREAM(因为客户端没有推送这个概念)
推送内容的缓存

可缓存的推送内容可以由客户端缓存,且被视为已经在源服务器验证过(即使有 no-cache 指令,只要推送流还开着就算有效)。

8.3 CONNECT 方法

CONNECT 的作用

CONNECT 是一个特殊的 HTTP 方法,用来建立隧道(Tunnel)。最典型的用法是:HTTPS 代理。

你的浏览器 ---[CONNECT example.com:443]--> 代理服务器
代理服务器  ---[建立 TCP 连接]----------> example.com:443
代理服务器 <--[TCP 连接成功]-------------- example.com:443
你的浏览器 <--[200 Connection Established]-- 代理服务器
             |-- 从此之后,所有数据都通过代理"透传" --|
你的浏览器 ---[TLS ClientHello + 加密 HTTP]-> 代理服务器 --> example.com

代理只是"搬运数据",看不懂里面的 TLS 加密内容。

HTTP/2 中 CONNECT 的帧格式

HTTP/2 中 CONNECT 用单条流承载整个隧道,映射规则:

字段 说明
:method CONNECT 固定值
:scheme 必须省略 不同于普通请求
:path 必须省略 不同于普通请求
:authority host:port 目标服务器地址,如 example.com:443

若缺少 :authority 或保留了 :scheme/:path,视为格式错误。

CONNECT 流的数据传输
HEADERS(CONNECT 请求)
    ↓
DATA 帧(客户端发的数据 = 代理转发给 TCP 服务器)
DATA 帧(代理从 TCP 服务器收到的数据 = 发给客户端)
    ↓
DATA + END_STREAM(等价于 TCP FIN)

TCP 层与 HTTP/2 帧的对应关系

TCP 行为 HTTP/2 对应
FIN(正常关闭) DATA 帧 + END_STREAM 标志
RST(异常重置) RST_STREAM 帧,错误码 CONNECT_ERROR
收到 TCP 的 FIN 发 DATA + END_STREAM 给对端
收到 TCP 的 RST 发 RST_STREAM 给对端

注意:CONNECT 流上只允许 DATA 帧和流管理帧(RST_STREAM、WINDOW_UPDATE、PRIORITY)。其他帧类型出现在 CONNECT 流上是错误。

第 9 章:附加 HTTP 要求与注意事项

9.1 连接管理

连接持久化

HTTP/2 的连接是长连接,设计上应该:

  • 客户端不在不需要时主动断开
  • 服务器尽量保持连接,但空闲时可以关闭
  • 关闭前应该先发 GOAWAY 帧,让双方优雅地知道连接要结束了
服务器 客户端 服务器 客户端 正常关闭流程 等待进行中的请求处理完毕 知道哪些请求可以重试 GOAWAY (Last-Stream-ID=N) 关闭 TCP 连接
连接数量限制

客户端对同一个 host:port 应该只维护一条 HTTP/2 连接。可以开多条的例外情况:

  • 流 ID 快耗尽时(每条连接最多约 2 31 − 1 2^{31}-1 2311 条流)
  • 需要刷新 TLS 密钥材料
  • 连接遇到错误需要替换
    用相同的 SNI(Server Name Indication)和 TLS 客户端证书时,不应重复建立连接。
9.1.1 连接复用

同一条连接可以复用给不同的 URI 源(Origin),条件是:
对于 HTTPS URI

  • 服务器证书必须对目标主机名有效(包括通配符证书)
  • 例如:证书 *.example.com 可以让 a.example.comb.example.com 共用一条连接
    潜在风险:TLS 终端(中间的反向代理)可能根据 SNI 决定把请求路由给哪台服务器,连接复用可能导致请求被错误路由。
9.1.2 421 Misdirected Request 状态码

当服务器不认为自己对该请求有权威性时,返回 421。

客户端用 a.example.com 的连接发了 b.example.com 的请求
         ↓
服务器发现自己没有 b.example.com 的权威
         ↓
服务器返回 421 Misdirected Request
         ↓
客户端可以新建连接重试(即使是非幂等方法也可以)

421 的特性

  • 默认可缓存
  • 不能由代理生成(只能由原始服务器生成)

9.2 TLS 特性要求

HTTP/2 over TLS 必须使用 TLS 1.2 或更高版本

9.2.1 TLS 1.2 的额外限制

限制项 要求 原因
TLS 压缩 必须禁用 压缩导致 CRIME 类信息泄露攻击
TLS 重协商 必须禁用 安全漏洞,且可能造成连接长时间不可用
DHE 密钥长度 至少 2048 位,客户端必须接受最大 4096 位 防止密钥暴力破解
ECDHE 密钥长度 至少 224 位 防止密钥暴力破解

为什么禁止 TLS 压缩?
TLS 压缩与应用层内容共享压缩上下文。攻击者若能控制部分请求内容,可以通过观察压缩后的密文长度来推断秘密数据(CRIME 攻击):

攻击思路(CRIME 攻击):
  目标:猜测 Cookie 的值
  方法:
    构造 "Cookie: session=A..." 并看压缩后长度
    构造 "Cookie: session=B..." 并看压缩后长度
    若某个猜测与真实 Cookie 重复内容更多,压缩比更高 → 长度更短
    反复缩小范围,最终还原 Cookie

为什么禁止重协商?
TLS 重协商允许在已有连接上重新协商安全参数。HTTP/2 中若允许重协商,可以用来针对特定资源触发重新认证,有安全隐患。不过有一个例外:连接刚建立时(连接前言之前)可以通过重协商提交客户端证书。

9.2.2 TLS 1.2 密码套件要求

HTTP/2 必须支持(且优先使用):

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

使用 P-256 椭圆曲线。这条密码套件提供:

  • 椭圆曲线 Diffie-Hellman 密钥交换(前向保密)
  • RSA 身份验证
  • AES-128-GCM 对称加密(认证加密,防篡改)
  • SHA-256 消息摘要
    附录 A 中有长达数页的黑名单——不推荐使用的密码套件,以下小节会解释原因。

第 10 章:安全考虑

10.1 服务器权威性(Server Authority)

HTTP/2 沿用 HTTP/1.1 的权威性定义:

  • http:// URI:通过 DNS 解析确认 IP 地址属于同一主机
  • https:// URI:TLS 证书中的域名必须匹配,且验证通过
    这与第 8 章中服务器推送、连接复用等功能密切相关——只有有权威性的服务器才能推送资源。

10.2 跨协议攻击(Cross-Protocol Attacks)

攻击场景:攻击者诱导客户端向某服务器发起 HTTP/2 请求,但该服务器实际上期望的是另一种协议(如 SMTP、FTP 等)。
防御措施
TLS + ALPN(Application-Layer Protocol Negotiation)是最强防御。ALPN 在 TLS 握手阶段就确定了应用协议,若服务器不支持 HTTP/2,握手就会失败。
明文 HTTP/2(h2c)的防御很弱:连接前言里虽然有一个特殊字符串干扰 HTTP/1.1 解析器,但对其他协议没有保护。

10.3 中间人封装攻击(Intermediary Encapsulation Attacks)

问题:HTTP/2 的头部编码允许表达某些在 HTTP/1.1 中非法的字符。若中间代理把 HTTP/2 请求翻译为 HTTP/1.1,可能产生安全漏洞。
两个具体危险字符

CR  (ASCII 0x0d, \r)  回车
LF  (ASCII 0x0a, \n)  换行
NUL (ASCII 0x00)      空字符

若头字段值中包含 \r\n,翻译为 HTTP/1.1 时可能被解释为头部行的分隔符,从而注入额外头字段(头注入攻击)。
规则:任何包含非法字符的请求/响应必须被视为格式错误(PROTOCOL_ERROR)。

10.4 推送响应的缓存安全

多租户场景下的隐患:

情形:
  example.com 服务多个租户:tenant_A 和 tenant_B
  每个租户控制 URI 空间的一部分
攻击:
  tenant_A 通过推送,把 /tenant_B/resource 的内容替换成自己的内容
  结果:其他用户缓存了 tenant_A 伪造的内容
防御规则:
  服务器必须确保租户只能推送自己名下的资源
  非权威性来源的推送不得使用或缓存

10.5 拒绝服务(DoS)考虑

HTTP/2 比 HTTP/1.1 需要维护更多状态,因此更容易遭受 DoS 攻击。

10.5.1 头部块大小限制

问题:头部没有硬性大小上限。超大头部会:

  • 强迫端点缓冲整个头块(因为路由关键字段可能在末尾)
  • 耗尽内存
    应对
  • SETTINGS_MAX_HEADER_LIST_SIZE:建议对端限制头大小(但只是建议,不是强制)
  • 服务器可返回 431 Request Header Fields Too Large
潜在的滥用方式

攻击手法 效果
频繁发送无意义 SETTINGS 帧变更 浪费对端处理资源
大量小的空 DATA / CONTINUATION 帧 浪费帧头解析资源
WINDOW_UPDATE / PRIORITY 帧泛滥 无效流量
大量 PUSH_PROMISE 帧 消耗 reserved 状态流的内存;超限可报 ENHANCE_YOUR_CALM

服务器推送的 DoS 防御:客户端应该限制 reserved (remote) 状态的流数量,超过限制可以用错误码 ENHANCE_YOUR_CALM 处理。

10.5.2 CONNECT 的资源放大

CONNECT 的特殊风险:

建立一条 HTTP/2 流(开销小)
       ↓
代理需要建立一条 TCP 连接(开销大)
       ↓
即使 HTTP/2 流关闭,TCP 连接仍处于 TIME_WAIT 状态(内核资源)

所以 SETTINGS_MAX_CONCURRENT_STREAMS 单独不足以限制 CONNECT 的资源消耗,还需要专门的速率限制。

10.6 压缩的安全使用

BREACH 攻击:HTTP 响应压缩的变体 CRIME 攻击。

原理(BREACH / CRIME):
  同一个压缩上下文中包含:
    ① 攻击者控制的明文(如 URL 参数)
    ② 秘密数据(如 CSRF Token)
  两者内容若有重叠,压缩比更高 → 密文更短
  观察密文长度 → 推断秘密数据

规则

  • 不得将攻击者可控数据和机密数据放在同一个压缩上下文
  • 来源不确定时不得压缩
  • TLS 通用流压缩必须禁用(见 9.2)

10.7 填充(Padding)的安全作用

Padding 是什么? HTTP/2 帧可以在实际数据后面附加随机字节,使帧大小看起来不同。
Padding 的局限性

好处:让攻击者难以通过帧大小推断数据内容
局限:
  ① 随机 Padding 但分布可预测 → 保护效果很弱
  ② 固定大小 Padding → 数据跨越固定边界时仍泄露信息
  ③ 不如限制/禁用压缩有效
结论:Padding 是辅助手段,不是核心防御

中间代理可以:

  • 保留 DATA 帧的 Padding
  • 去掉 HEADERS 和 PUSH_PROMISE 帧的 Padding(允许)
  • 修改 Padding 大小(允许,若有更好的保护理由)

10.8 隐私考虑

HTTP/2 的某些特性可以被用来对客户端进行指纹识别(Fingerprinting)

可观察特征 指纹信息
SETTINGS 参数值 客户端实现类型/版本
流控窗口管理方式 实现特征
流优先级分配 浏览器类型
对 PING/SETTINGS 的响应延迟 网络特征

连接复用的隐私影响

  • 单条 TCP 连接 → 同一网站内的操作可被关联
  • 跨域复用 → 不同网站之间的操作可被关联追踪

第 11 章:IANA 注册

IANA(互联网号码分配机构)负责维护 HTTP/2 用到的各种注册表。

11.1 HTTP/2 标识字符串

HTTP/2 用 ALPN 来协商协议:

标识字符串 十六进制序列 用途
h2 0x68 0x32 HTTP/2 over TLS
h2c 0x68 0x32 0x63 HTTP/2 over 明文 TCP

11.2 帧类型注册表


帧类型 代码 章节
DATA 0x0 6.1
HEADERS 0x1 6.2
PRIORITY 0x2 6.3
RST_STREAM 0x3 6.4
SETTINGS 0x4 6.5
PUSH_PROMISE 0x5 6.6
PING 0x6 6.7
GOAWAY 0x7 6.8
WINDOW_UPDATE 0x8 6.9
CONTINUATION 0x9 6.10

11.3 SETTINGS 参数注册表


名称 代码 初始值 含义
HEADER_TABLE_SIZE 0x1 4096 HPACK 头压缩表大小(字节)
ENABLE_PUSH 0x2 1 是否启用服务器推送(0=禁用)
MAX_CONCURRENT_STREAMS 0x3 无限制 最大并发流数
INITIAL_WINDOW_SIZE 0x4 65535 初始流控窗口大小(字节)
MAX_FRAME_SIZE 0x5 16384 单帧最大载荷(字节)
MAX_HEADER_LIST_SIZE 0x6 无限制 头字段列表最大大小(建议值)

11.4 错误码注册表


名称 代码 含义
NO_ERROR 0x0 优雅关闭,无错误
PROTOCOL_ERROR 0x1 协议错误(通用)
INTERNAL_ERROR 0x2 实现内部错误
FLOW_CONTROL_ERROR 0x3 违反流量控制限制
SETTINGS_TIMEOUT 0x4 SETTINGS ACK 超时
STREAM_CLOSED 0x5 已关闭的流收到帧
FRAME_SIZE_ERROR 0x6 帧大小不合法
REFUSED_STREAM 0x7 流未被处理,可重试
CANCEL 0x8 流被取消
COMPRESSION_ERROR 0x9 头压缩状态不一致
CONNECT_ERROR 0xa CONNECT 隧道的 TCP 错误
ENHANCE_YOUR_CALM 0xb 对端行为超出处理能力(DoS 防御)
INADEQUATE_SECURITY 0xc TLS 安全参数不满足要求
HTTP_1_1_REQUIRED 0xd 此请求需要用 HTTP/1.1

其他注册项

  • HTTP2-Settings 头字段:HTTP/1.1 Upgrade 机制使用,携带 SETTINGS 参数的 Base64URL 编码
  • PRI 方法:HTTP/2 连接前言使用(PRI * HTTP/2.0),防止被 HTTP/1.1 解析器当作正常请求
  • 421 状态码:Misdirected Request

附录 A:TLS 1.2 密码套件黑名单

为什么要有黑名单?

不是所有密码套件都安全。以下类别的密码套件应该避免与 HTTP/2 一起使用:
类别一:无加密

TLS_NULL_WITH_NULL_NULL   -- 完全没有加密
TLS_*_WITH_NULL_*         -- 加密算法为 NULL

类别二:RC4 流密码

TLS_*_WITH_RC4_*
原因:RC4 已知有多个严重漏洞(BEAST、RC4 偏差攻击)

类别三:弱出口级密钥

TLS_*_EXPORT_*
原因:这些套件在 1990 年代为了满足美国出口限制而人为削弱密钥长度(40/56 位),现代计算机可以暴力破解

类别四:DES/3DES

TLS_*_WITH_DES_*
TLS_*_WITH_3DES_*
原因:DES 密钥只有 56 位,早已被破解;3DES 有 Sweet32 攻击(生日攻击)

类别五:CBC 模式而非 GCM

TLS_*_WITH_AES_*_CBC_*
原因:CBC 模式容易受到 BEAST、POODLE 等填充预言攻击
      GCM(Galois/Counter Mode)是认证加密,更安全

类别六:无前向保密(Non-PFS)

TLS_RSA_WITH_*(静态 RSA 密钥交换)
TLS_DH_*(非 DHE,即静态 DH)
原因:无前向保密,历史流量在私钥泄露后可被解密

推荐的安全密码套件(白名单典范)

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  ECDHE: 椭圆曲线 Diffie-Hellman(前向保密)
  RSA:   身份验证
  AES-128-GCM: 认证加密
  SHA256: 消息摘要

密钥交换算法比较

密钥交换方式

静态 RSA
黑名单

静态 DH
黑名单

DHE
有前向保密
但 CBC 黑名单

ECDHE
有前向保密
GCM = 推荐

综合 C++ 实战代码示例

以下代码模拟 HTTP/2 帧序列的验证逻辑,展示本章多个核心概念的实现思路。

/*
 * http2_concepts.cpp
 *
 * 模拟 HTTP/2 的以下概念(从零实现简化版本):
 *   1. 帧类型和错误码定义(第 11 章注册表)
 *   2. 格式错误检测(8.1.2.6)
 *   3. 服务器推送约束检查(8.2)
 *   4. GOAWAY 可重试流判断(8.1.4)
 *   5. TLS 密码套件安全检查(9.2.2)
 *
 * 编译:g++ -std=c++17 http2_concepts.cpp -o http2_concepts
 */
#include <cstdint>
#include <iostream>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
// ============================================================
//  第 11 章:帧类型枚举(来自 IANA 帧类型注册表)
// ============================================================
enum class FrameType : uint8_t {
    DATA          = 0x0,   // 数据帧:承载 HTTP 消息体
    HEADERS       = 0x1,   // 头部帧:承载 HTTP 头字段
    PRIORITY      = 0x2,   // 优先级帧
    RST_STREAM    = 0x3,   // 重置流帧
    SETTINGS      = 0x4,   // 设置帧
    PUSH_PROMISE  = 0x5,   // 推送预告帧
    PING          = 0x6,   // 心跳帧
    GOAWAY        = 0x7,   // 离开帧
    WINDOW_UPDATE = 0x8,   // 流量控制更新帧
    CONTINUATION  = 0x9,   // 头部续传帧
};
// ============================================================
//  第 11 章:错误码枚举(来自 IANA 错误码注册表)
// ============================================================
enum class Http2ErrorCode : uint32_t {
    NO_ERROR            = 0x0,   // 无错误(优雅关闭时使用)
    PROTOCOL_ERROR      = 0x1,   // 协议错误(格式错误时使用)
    INTERNAL_ERROR      = 0x2,   // 内部实现错误
    FLOW_CONTROL_ERROR  = 0x3,   // 流量控制违规
    SETTINGS_TIMEOUT    = 0x4,   // SETTINGS ACK 超时
    STREAM_CLOSED       = 0x5,   // 向已关闭的流发送帧
    FRAME_SIZE_ERROR    = 0x6,   // 帧大小错误
    REFUSED_STREAM      = 0x7,   // 流未被处理(可安全重试)
    CANCEL              = 0x8,   // 流被取消
    COMPRESSION_ERROR   = 0x9,   // 头压缩状态不一致
    CONNECT_ERROR       = 0xa,   // CONNECT 方法 TCP 错误
    ENHANCE_YOUR_CALM   = 0xb,   // 对端行为超出容忍(DoS 防御)
    INADEQUATE_SECURITY = 0xc,   // TLS 安全参数不足
    HTTP_1_1_REQUIRED   = 0xd,   // 此请求需要 HTTP/1.1
};
// ============================================================
//  简化的 HTTP/2 头字段结构
// ============================================================
struct HeaderField {
    std::string name;    // 字段名,HTTP/2 要求全小写
    std::string value;   // 字段值
    HeaderField(std::string n, std::string v)
        : name(std::move(n)), value(std::move(v)) {}
};
// ============================================================
//  8.1.2.6:格式错误检测器
// ============================================================
class MalformedRequestDetector {
public:
    // 必须存在的伪头字段(对于普通请求)
    // 这些是 HTTP/2 的"请求行"等价物
    static const std::set<std::string> kRequiredPseudoHeaders;
    // 检查头字段名是否合法
    // HTTP/2 规定:头字段名必须全部小写(大写是格式错误)
    static bool has_uppercase_header_name(const std::vector<HeaderField>& headers) {
        for (const auto& h : headers) {
            // 跳过伪头字段(以 : 开头的)
            if (!h.name.empty() && h.name[0] == ':') {
                continue;
            }
            for (char c : h.name) {
                if (c >= 'A' && c <= 'Z') {
                    return true;  // 发现大写字母,格式错误
                }
            }
        }
        return false;
    }
    // 检查是否缺少必要的伪头字段
    static bool missing_required_pseudo_headers(const std::vector<HeaderField>& headers) {
        std::set<std::string> found;
        for (const auto& h : headers) {
            if (!h.name.empty() && h.name[0] == ':') {
                found.insert(h.name);
            }
        }
        // 检查所有必须存在的伪头是否都找到了
        for (const auto& required : kRequiredPseudoHeaders) {
            if (found.find(required) == found.end()) {
                return true;  // 缺少必要伪头字段
            }
        }
        return false;
    }
    // 检查头字段值中是否包含非法字符(\r \n \0)
    // 防御中间人封装攻击(10.3)
    static bool has_illegal_header_value_chars(const std::vector<HeaderField>& headers) {
        for (const auto& h : headers) {
            for (char c : h.value) {
                // CR(0x0d)、LF(0x0a)、NUL(0x00) 是非法字符
                if (c == '\r' || c == '\n' || c == '\0') {
                    return true;
                }
            }
        }
        return false;
    }
    // 综合格式错误检查
    // 返回错误描述,若无错误返回空字符串
    static std::string check(
        const std::vector<HeaderField>& headers,
        int64_t content_length_value,   // -1 表示没有 content-length 头
        int64_t actual_data_length      // 实际收到的 DATA 帧数据总长
    ) {
        // 检查1:大写字母
        if (has_uppercase_header_name(headers)) {
            return "格式错误:头字段名含大写字母 (PROTOCOL_ERROR)";
        }
        // 检查2:缺少必要伪头
        if (missing_required_pseudo_headers(headers)) {
            return "格式错误:缺少必要的伪头字段 (PROTOCOL_ERROR)";
        }
        // 检查3:非法字符
        if (has_illegal_header_value_chars(headers)) {
            return "格式错误:头字段值含非法字符 CR/LF/NUL (PROTOCOL_ERROR)";
        }
        // 检查4:content-length 与实际数据长度不匹配
        // 公式: content-length == sum(DATA_frame_payload[i])
        if (content_length_value >= 0 && content_length_value != actual_data_length) {
            return "格式错误:content-length (" +
                   std::to_string(content_length_value) +
                   ") 与实际数据长度 (" +
                   std::to_string(actual_data_length) +
                   ") 不符 (PROTOCOL_ERROR)";
        }
        return "";  // 没有格式错误
    }
};
// 普通请求(非 CONNECT)必须包含这三个伪头
const std::set<std::string> MalformedRequestDetector::kRequiredPseudoHeaders = {
    ":method",
    ":scheme",
    ":path"
};
// ============================================================
//  8.2:服务器推送约束检查器
// ============================================================
class ServerPushValidator {
public:
    // 推送的"虚拟请求"必须满足的条件
    // 对应 RFC 7540 8.2 节的规则
    // 检查1:方法必须是安全且可缓存的
    // 安全方法:不改变服务器状态(GET、HEAD、OPTIONS)
    // 可缓存方法:GET、HEAD(POST 通常不可缓存)
    static bool is_safe_and_cacheable_method(const std::string& method) {
        // HTTP/2 Server Push 只允许安全且可缓存的方法
        // 实践中几乎都是 GET
        return (method == "GET" || method == "HEAD");
    }
    // 检查2:没有请求体
    // 有请求体的资源不能被推送(无法确定用什么请求体)
    static bool has_no_request_body(const std::vector<HeaderField>& headers) {
        for (const auto& h : headers) {
            if (h.name == "content-length") {
                // 如果有 content-length,必须为 0
                try {
                    return std::stoi(h.value) == 0;
                } catch (...) {
                    return false;
                }
            }
        }
        return true;  // 没有 content-length 头,意味着没有请求体
    }
    // 检查3::authority 必须存在(服务器权威性)
    static bool has_authority(const std::vector<HeaderField>& headers) {
        for (const auto& h : headers) {
            if (h.name == ":authority" && !h.value.empty()) {
                return true;
            }
        }
        return false;
    }
    // 综合验证推送请求是否合法
    static std::string validate_push_request(const std::vector<HeaderField>& headers) {
        // 找出 :method 的值
        std::string method;
        for (const auto& h : headers) {
            if (h.name == ":method") {
                method = h.value;
            }
        }
        if (!is_safe_and_cacheable_method(method)) {
            return "推送错误:方法 '" + method + "' 不是安全/可缓存方法 (PROTOCOL_ERROR)";
        }
        if (!has_no_request_body(headers)) {
            return "推送错误:推送请求不能有请求体 (PROTOCOL_ERROR)";
        }
        if (!has_authority(headers)) {
            return "推送错误:缺少 :authority 伪头 (PROTOCOL_ERROR)";
        }
        return "";  // 推送请求合法
    }
};
// ============================================================
//  8.1.4:基于 GOAWAY 的可重试流判断
// ============================================================
class RetryabilityChecker {
public:
    // 判断某条流的请求在收到 GOAWAY 后是否可以安全重试
    //
    // 参数:
    //   stream_id       - 要检查的流 ID
    //   goaway_last_id  - GOAWAY 帧中的 Last-Stream-ID
    //
    // 规则:
    //   流 ID > Last-Stream-ID → 服务器保证没处理 → 可以重试
    //   流 ID <= Last-Stream-ID → 可能已处理 → 不确定,不能自动重试
    static bool is_safe_to_retry_after_goaway(uint32_t stream_id,
                                               uint32_t goaway_last_id) {
        // 大于 Last-Stream-ID 的流:服务器保证没有处理
        return stream_id > goaway_last_id;
    }
    // 判断 REFUSED_STREAM 的流是否可以重试
    // REFUSED_STREAM 表示服务器明确承诺"我没有处理这个流"
    // 所以永远可以重试(包括非幂等方法如 POST)
    static bool is_safe_to_retry_refused_stream() {
        // REFUSED_STREAM 意味着流从未被处理 → 总是安全重试
        return true;
    }
};
// ============================================================
//  9.2.2:TLS 密码套件安全性检查(黑名单检查)
// ============================================================
class TlsCipherSuiteChecker {
public:
    // 不安全密码套件黑名单(精选示例,真实黑名单见附录 A)
    // 这里只列举几个典型的黑名单成员用于演示
    static const std::set<std::string> kBlacklist;
    // HTTP/2 必须支持的最低安全密码套件
    static const std::string kRequiredCipherSuite;
    // 检查某个密码套件是否在黑名单中
    static bool is_blacklisted(const std::string& cipher_suite) {
        return kBlacklist.find(cipher_suite) != kBlacklist.end();
    }
    // 检查协商结果是否满足 HTTP/2 最低安全要求
    static std::string evaluate_negotiated_cipher(const std::string& cipher_suite) {
        if (is_blacklisted(cipher_suite)) {
            return "警告:密码套件 '" + cipher_suite +
                   "' 在黑名单中,可能引发 INADEQUATE_SECURITY 错误";
        }
        // 检查是否是推荐的安全套件
        if (cipher_suite == kRequiredCipherSuite) {
            return "密码套件安全:使用了推荐的 ECDHE + AES-GCM + SHA256";
        }
        // 不在黑名单,也不是标准推荐套件
        return "密码套件可接受:不在黑名单中,但建议使用 " + kRequiredCipherSuite;
    }
};
// 精选典型黑名单条目(附录 A 有完整列表)
const std::set<std::string> TlsCipherSuiteChecker::kBlacklist = {
    "TLS_NULL_WITH_NULL_NULL",          // 无任何加密
    "TLS_RSA_WITH_RC4_128_SHA",         // RC4 已被攻破
    "TLS_RSA_WITH_DES_CBC_SHA",         // DES 56 位,可暴力破解
    "TLS_RSA_WITH_3DES_EDE_CBC_SHA",    // 3DES 有 Sweet32 攻击
    "TLS_RSA_WITH_AES_128_CBC_SHA",     // 无前向保密(静态 RSA)
    "TLS_RSA_WITH_AES_128_CBC_SHA256",  // 无前向保密(静态 RSA)
    "TLS_RSA_WITH_AES_128_GCM_SHA256",  // GCM 好但无前向保密
};
// HTTP/2 必须支持的密码套件(9.2.2 节要求)
const std::string TlsCipherSuiteChecker::kRequiredCipherSuite =
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256";
// ============================================================
//  演示主程序
// ============================================================
int main() {
    std::cout << "========================================\n";
    std::cout << "HTTP/2 RFC 7540 概念演示程序\n";
    std::cout << "========================================\n\n";
    // --- 演示1:格式错误检测 ---
    std::cout << "[演示1] 格式错误检测(8.1.2.6)\n";
    std::cout << "------------------------------\n";
    // 测试案例A:正常请求
    {
        std::vector<HeaderField> normal_headers = {
            {":method", "GET"},
            {":scheme", "https"},
            {":path",   "/resource"},
            {"host",    "example.org"},   // 全小写,正确
            {"accept",  "image/jpeg"},
        };
        auto err = MalformedRequestDetector::check(normal_headers, -1, 0);
        std::cout << "正常 GET 请求: "
                  << (err.empty() ? "格式正确" : err) << "\n";
    }
    // 测试案例B:头字段名含大写
    {
        std::vector<HeaderField> bad_headers = {
            {":method", "GET"},
            {":scheme", "https"},
            {":path",   "/resource"},
            {"Host",    "example.org"},   // 大写 H,HTTP/2 中非法
        };
        auto err = MalformedRequestDetector::check(bad_headers, -1, 0);
        std::cout << "大写头字段名: "
                  << (err.empty() ? "格式正确" : err) << "\n";
    }
    // 测试案例C:content-length 不匹配
    {
        std::vector<HeaderField> post_headers = {
            {":method",         "POST"},
            {":scheme",         "https"},
            {":path",           "/upload"},
            {"content-length",  "100"},   // 声称 100 字节
        };
        // 但实际只收到了 80 字节的 DATA 帧数据
        auto err = MalformedRequestDetector::check(post_headers, 100, 80);
        std::cout << "content-length 不匹配: "
                  << (err.empty() ? "格式正确" : err) << "\n";
    }
    // 测试案例D:头字段值含 \r\n(注入攻击)
    {
        std::vector<HeaderField> inject_headers = {
            {":method", "GET"},
            {":scheme", "https"},
            {":path",   "/"},
            {"custom",  "value\r\nX-Injected: evil"},  // 注入攻击
        };
        auto err = MalformedRequestDetector::check(inject_headers, -1, 0);
        std::cout << "头注入攻击检测: "
                  << (err.empty() ? "格式正确" : err) << "\n";
    }
    std::cout << "\n";
    // --- 演示2:服务器推送验证 ---
    std::cout << "[演示2] 服务器推送约束检查(8.2)\n";
    std::cout << "------------------------------\n";
    // 测试A:合法推送(GET 方法,无请求体,有 authority)
    {
        std::vector<HeaderField> push_ok = {
            {":method",    "GET"},
            {":scheme",    "https"},
            {":path",      "/style.css"},
            {":authority", "example.com"},
        };
        auto err = ServerPushValidator::validate_push_request(push_ok);
        std::cout << "合法推送请求: "
                  << (err.empty() ? "推送请求合法" : err) << "\n";
    }
    // 测试B:POST 推送(非法:POST 不安全且通常不可缓存)
    {
        std::vector<HeaderField> push_bad = {
            {":method",    "POST"},
            {":scheme",    "https"},
            {":path",      "/api/data"},
            {":authority", "example.com"},
        };
        auto err = ServerPushValidator::validate_push_request(push_bad);
        std::cout << "非法推送(POST 方法): "
                  << (err.empty() ? "推送请求合法" : err) << "\n";
    }
    std::cout << "\n";
    // --- 演示3:GOAWAY 可重试判断 ---
    std::cout << "[演示3] GOAWAY 可重试流判断(8.1.4)\n";
    std::cout << "------------------------------\n";
    {
        uint32_t last_id = 5;  // 服务器处理到了流 5
        std::cout << "GOAWAY Last-Stream-ID = " << last_id << "\n";
        for (uint32_t id : {1u, 3u, 5u, 7u, 9u}) {
            bool safe = RetryabilityChecker::is_safe_to_retry_after_goaway(id, last_id);
            std::cout << "  Stream " << id << ": "
                      << (safe ? "可以安全重试" : "不确定是否已处理,不应自动重试")
                      << "\n";
        }
    }
    std::cout << "\n";
    // --- 演示4:TLS 密码套件安全性检查 ---
    std::cout << "[演示4] TLS 密码套件黑名单检查(9.2.2 + 附录A)\n";
    std::cout << "------------------------------\n";
    std::vector<std::string> test_suites = {
        "TLS_NULL_WITH_NULL_NULL",              // 极度不安全
        "TLS_RSA_WITH_RC4_128_SHA",             // RC4 黑名单
        "TLS_RSA_WITH_AES_128_CBC_SHA",         // 黑名单(无前向保密)
        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",// 推荐!
        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",// 可接受(不在黑名单)
    };
    for (const auto& suite : test_suites) {
        auto result = TlsCipherSuiteChecker::evaluate_negotiated_cipher(suite);
        std::cout << "  [" << suite.substr(0, 30) << "...]\n"
                  << "    -> " << result << "\n";
    }
    std::cout << "\n========================================\n";
    std::cout << "演示完毕\n";
    std::cout << "========================================\n";
    return 0;
}

知识总结脑图

HTTP/2 RFC 7540 第8-12章核心要点

第8章 HTTP语义

第9章 连接与TLS

第10章 安全

第11章 IANA

格式错误规则
大写头/缺伪头/非法字符
content-length不匹配

帧序列示例
GET=单HEADERS
POST=HEADERS+CONTINUATION+DATA
有Trailer时需额外HEADERS

请求可靠性
GOAWAY Last-Stream-ID
REFUSED_STREAM可重试
PING探活

服务器推送
PUSH_PROMISE帧
推送流偶数编号
可禁用/限制/拒绝

CONNECT隧道
省略:scheme/:path
DATA=TCP数据
END_STREAM=FIN

连接持久化
SHOULD只建一条连接
关闭前发GOAWAY

421状态码
服务器无权威
客户端可重试

TLS要求
禁TLS压缩
禁重协商
最低TLS 1.2

密码套件
必须支持ECDHE+AES-GCM
黑名单超过150条

跨协议攻击
ALPN防御
h2c防御弱

头注入攻击
CR/LF/NUL非法
必须拒绝格式错误请求

DoS防御
限头部大小
限推送流数
ENHANCE_YOUR_CALM

压缩安全
BREACH/CRIME攻击
禁止混合压缩上下文

隐私指纹
SETTINGS可指纹
流控/优先级可指纹

核心规则速查表


主题 关键规则
头字段名大小写 HTTP/2 必须全小写;大写 = 格式错误 = PROTOCOL_ERROR
content-length 校验 必须等于所有 DATA 帧长度之和;否则 = 格式错误
服务器推送方法 只能 GET/HEAD(安全且可缓存)
客户端推送 绝对禁止(服务器收到 PUSH_PROMISE = 连接级错误)
CONNECT 伪头 只有 :method=CONNECT 和 :authority;省略 :scheme :path
TLS 版本 最低 TLS 1.2
TLS 压缩 必须禁用(防 CRIME)
TLS 重协商 必须禁用(连接前言前除外)
必须支持的密码套件 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
头字段值非法字符 CR/LF/NUL 是非法字符(防注入攻击)
GOAWAY 可重试 流 ID > Last-Stream-ID 才可重试
REFUSED_STREAM 任何方法(包括 POST)都可重试
连接数 SHOULD 对同一 host:port 只维护一条连接
Padding 辅助防御,不能替代限制压缩

DNS RFC 1034 深度中文学习笔记

域名系统——概念与设施(Domain Names: Concepts and Facilities)

面向零基础读者,所有概念从零解释,配合图示、代码与类比。

目录

  1. 整体知识地图
  2. 第1章 备忘录状态
  3. 第2章 背景与设计目标
  4. 第3章 域名空间与资源记录
  5. 第4章 名称服务器
  6. 第5章 解析器
  7. 第6章 综合场景演示
  8. C++ 实战代码
  9. 核心速查表

整体知识地图

DNS 域名系统

域名空间
树形层次结构

名称服务器
Name Server

解析器
Resolver

节点与标签
Node / Label

资源记录 RR
Resource Record

区域 Zone
权威数据单元

权威服务器
Authoritative

递归查询
Recursive

迭代查询
Iterative

区域传输
Zone Transfer

存根解析器
Stub Resolver

缓存管理
Cache / TTL

查询算法
SLIST/SBELT

A 记录 主机地址

NS 记录 名称服务器

CNAME 别名记录

MX 邮件交换

SOA 区域起始

PTR 反向查找

第1章 备忘录状态

RFC 1034 是 DNS 的概念性介绍文档,详细的实现规范在其配套文档 RFC-1035 中。
本 RFC 废弃了早期的 RFC-882、RFC-883 和 RFC-973。
DNS 是官方协议的一个子集,同时保留了扩展性——研究者可以不断提出新的数据类型和查询类型实验。读者需注意:文档中的示例数值仅用于教学,不代表真实的当前数据。

第2章 背景与设计目标

2.1 域名系统的历史由来

旧时代的痛苦:HOSTS.TXT

在 DNS 出现之前,整个互联网的主机名与 IP 地址的对应关系,全部写在一个叫 HOSTS.TXT 的文件里,存放于 NIC(网络信息中心)。
每台主机想知道 example.com 是哪个 IP,就得下载这个文件。
问题非常严重:
带宽消耗问题:设有 N N N 台主机,每次 NIC 更新文件,所有主机都要来下载,总流量正比于 N 2 N^2 N2。当主机数量爆炸性增长时,NIC 的出口带宽完全撑不住。
总下载流量 ∝ N 2 \text{总下载流量} \propto N^2 总下载流量N2
管理中心化问题:本地组织想改个主机名,必须通知 NIC,等 NIC 更新 HOSTS.TXT 后,全网才能看到变化,周期可能是数天。
结构缺失问题:HOSTS.TXT 是平铺结构,没有层次,无法表达"这台机器属于某组织某部门"这样的关系。

DNS 的解决思路

一句话总结:把一个集中式的大文件,变成一棵分布式的树,让树的每个分支由对应的组织自己维护。

HOSTS.TXT(旧):           DNS(新):
┌──────────────────┐        分布式树形空间
│ host1 = 1.2.3.4  │  -->   .(根)
│ host2 = 5.6.7.8  │        ├── edu
│ host3 = 9.0.0.1  │        │   └── mit
│ ...(数千行)     │        └── com
└──────────────────┘            └── example
  所有人都要下这个文件           每层由对应组织管理

2.2 DNS 设计目标

RFC 明确列出了七条设计目标,逐一理解:
目标1:一致的命名空间
名字不应该嵌入网络地址或路由信息。名字就是名字,与网络拓扑无关。
(旧做法:有时用 IP 地址的一部分来命名主机,这很脆弱)
目标2:分布式维护
数据库太大,且更新频繁,不能由一个中心节点维护。必须分散到各组织,配合本地缓存提升性能。
目标3:数据控制权归源头
谁的数据谁做主。数据的拥有者决定缓存时效(TTL)、更新频率等。
目标4:通用性,支持多种应用
DNS 不只服务于 HTTP,还要支持邮件(MX 记录)、主机查找、任意未来应用。所有数据按**类型(type)打标签,查询可以精确到某类型。
目标5:支持不同协议族
同一个名字可能在不同协议体系里有不同格式的地址。DNS 用
类(class)**字段来区分(例如 IN 表示 Internet,CH 表示 Chaos 网络)。
目标6:独立于传输层
DNS 消息可以走 UDP(轻量快速),也可以走 TCP(可靠,适合大数据或区域传输)。
目标7:适配各种主机能力
个人 PC 和大型分时主机都能使用 DNS,只是方式不同(PC 可能使用存根解析器)。

2.3 使用假设

DNS 的设计基于以下几条现实假设:
数据量增长模型

  • 初期:数据量与主机数成正比
  • 未来:随着邮箱等信息加入,将与用户数成正比
    数据变化频率:大部分数据(主机地址、邮箱绑定)变化很慢,但系统需支持少量快速变化的数据(最快可到秒/分钟级)。
    管理边界:通常与组织结构对应。每个组织的域必须有至少两台冗余名称服务器
    可信服务器:客户端可以指定信任的服务器,不强制接受外部引用。
    最终一致性:DNS 不保证所有副本同时更新,而是通过 TTL 控制副本的有效期,采用"分发+超时刷新"模型。
    查询方式
  • 递归查询(Recursive):服务器代替客户端追问到底,最终返回答案。
  • 迭代查询(Iterative):服务器告诉客户端"去问别人",客户端自己去追。
    RFC 规定:迭代查询必须实现,递归查询是可选功能。
mit.edu 服务器 .edu 服务器 根服务器 解析器 应用程序 mit.edu 服务器 .edu 服务器 根服务器 解析器 应用程序 迭代查询流程(Iterative) 查 www.mit.edu 查 www.mit.edu 去问 .edu 服务器 查 www.mit.edu 去问 mit.edu 服务器 查 www.mit.edu 答案: 18.x.x.x 18.x.x.x

2.4 DNS 的三大组成部分

系统调用

DNS 查询协议

读取

区域传输

用户程序

解析器 Resolver

名称服务器 Name Server

域名空间+资源记录
Domain Name Space + RR


组件 职责 类比
域名空间 + 资源记录 定义树形命名结构和数据格式 图书馆的目录分类体系
名称服务器(Name Server) 存储并回答关于域名树的查询 图书馆的馆员
解析器(Resolver) 代表用户向名称服务器发起查询 帮你找书的助理

三个视角的 DNS

  • 用户视角:整个 DNS 是一棵单一的大树,通过本地解析器访问任何节点。
  • 解析器视角:DNS 由无数台名称服务器组成,每台持有部分数据,数据对解析器来说是"基本静态"的。
  • 名称服务器视角:DNS 由一系列**区域(Zone)**构成,服务器管理自己负责的区域,定期从主服务器刷新数据,同时并发处理查询请求。

第3章 域名空间与资源记录

3.1 名称空间规范与术语

树形结构

域名空间是一棵有根的树(Rooted Tree)。每个节点(含叶子节点)可以挂载一组资源记录(RR)。

                    .(根,空标签)
                    |
        ┌───────────┼───────────┐
       MIL          EDU         ARPA
        |            |            |
   ┌────┴────┐       |       ┌────┴────┐
  BRL  NOSC DARPA  MIT      IN-ADDR  SRI-NIC
              |     |
              |   ┌─┴──┐
              |  LCS  ACHILLES
             ISI  |
              |  XX
         ┌────┴────┐
         A    C   VAXA  VENERA
标签(Label)规则

每个节点有一个标签(Label)

  • 长度:0 到 63 个八位字节(octet)
  • 同一父节点下的兄弟节点,标签不能相同(大小写不敏感)
  • 根节点的标签是空字符串(长度为 0)
域名的构成

从某节点沿路径向上到根,依次拼接各标签,用 . 分隔,即构成该节点的域名(Domain Name)
例如:节点 XX,路径为 XX → LCS → MIT → EDU → 根,域名为 XX.LCS.MIT.EDU.(末尾的 . 表示根)。
内部表示(程序内部使用):

域名 "www.example.com." 的内部二进制表示:
┌──────┬───┬───┬───┬──────────┬───┬───────┬───┬───┐
│ 3    │ w │ w │ w │ 7        │ e │ x ... │ 3 │com│
│(长度)│   │   │   │(长度)    │   │       │   │   │
└──────┴───┴───┴───┴──────────┴───┴───────┴───┴───┘
最后以 长度=0 的字节结束(代表根)
完整域名 vs 相对域名

类型 特征 示例
绝对域名 末尾有 .,从根开始 poneria.ISI.EDU.
相对域名 末尾无 .,需结合本地域补全 poneria(在 ISI.EDU 域内)

总长度限制:域名的所有标签字节数之和(含长度字节)不超过 255 字节。
∑ i ( label_length i + 1 ) ≤ 255 \sum_{i} (\text{label\_length}_i + 1) \leq 255 i(label_lengthi+1)255

子域关系

AB 的子域,当且仅当 A 的域名以 B 的域名结尾。
例如:A.B.C.DB.C.DC.DD.(根)的子域。

3.2 管理指南

DNS 技术规范不强制规定树的结构,组织可以自由选择如何划分子域。但有一些通用建议:

  • 计划未来会拆分为多个区域的域,应在顶部提供分支,方便将来无需重命名即可拆分。
  • 标签名避免使用特殊字符、前导数字——这会破坏旧软件的兼容性。

3.3 技术指南

要让 DNS 为某类对象(如主机、邮箱)提供命名服务,需要定义两件事:

  1. 映射规则:如何把对象名转换为域名(如何查询)
  2. RR 类型和数据格式:如何描述该对象
    主机示例:主机名本身就是合法域名,A 记录存储 IP 地址,PTR 记录(在 IN-ADDR.ARPA 下)实现反向查找(IP→主机名)。
    邮箱示例HOSTMASTER@SRI-NIC.ARPA 映射为域名 HOSTMASTER.SRI-NIC.ARPA.
    规则:<local-part> 变成单个标签(内部的 . 不分割),<mail-domain>. 分割。

3.4 示例名称空间(图示)

下图是文档中反复引用的示例树(部分,非完整互联网):

. 根

MIL

EDU

ARPA

BRL

NOSC

DARPA

UCI

MIT

ISI

UDEL

YALE

IN-ADDR

SRI-NIC

ACC

LCS

ACHILLES

XX

A

C

VAXA

VENERA

Mockapetris

3.5 首选名称语法

DNS 允许任意二进制标签,但为了与旧系统(如 HOSTS.TXT、email)兼容,推荐使用以下语法:

<domain>    ::= <subdomain> | " "(根)
<subdomain> ::= <label> | <subdomain> "." <label>
<label>     ::= <letter> [ [ <ldh-str> ] <let-dig> ]
<ldh-str>   ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
<let-dig-hyp>::= <let-dig> | "-"
<let-dig>   ::= <letter> | <digit>

总结规则:

  • 字母开头
  • 字母或数字结尾
  • 中间可含字母、数字、连字符-
  • 不区分大小写
  • 单个标签不超过 63 个字符
    合法示例:A.ISI.EDUXX.LCS.MIT.EDUSRI-NIC.ARPA

3.6 资源记录(Resource Records,RR)

资源记录是 DNS 数据的基本单位。一条 RR 包含以下字段:

字段 类型 说明
owner 域名 该 RR 所属的域名节点
type 16 位整数 资源类型(A/NS/CNAME/MX/SOA/PTR/HINFO 等)
class 16 位整数 协议族(IN=Internet, CH=Chaos)
TTL 32 位整数(秒) 该记录可被缓存的最长时间
RDATA 可变长度 具体数据,含义由 type 和 class 决定

常用类型详解

类型 RDATA 格式 用途
A 32 位 IPv4 地址 主机地址(IN 类)
NS 域名(名称服务器主机名) 指定区域的权威名称服务器
CNAME 域名(规范名称) 为另一个名字创建别名
MX 16 位优先级 + 域名(邮件主机名) 邮件交换,数值越小优先级越高
SOA 多个字段(见下) 区域管理参数,每个区域只有一条
PTR 域名 反向查找(IP→名称)
HINFO CPU 类型字符串 + OS 字符串 主机硬件信息

SOA 记录的字段(区域起始记录):

SOA 记录格式示例:
.   IN  SOA  SRI-NIC.ARPA.  HOSTMASTER.SRI-NIC.ARPA. (
                870611    ; serial   - 序列号,版本标识
                1800      ; refresh  - 辅助服务器多少秒后检查更新(30分钟)
                300       ; retry    - 检查失败后重试间隔(5分钟)
                604800    ; expire   - 辅助服务器失联多久后丢弃数据(1周)
                86400     ; minimum  - 所有RR的最小TTL(1天)
            )
字段解释:
  主域名服务器  = SRI-NIC.ARPA.
  负责人邮箱   = HOSTMASTER@SRI-NIC.ARPA(. 代替 @)
TTL 的含义

TTL 告诉缓存方:这条记录最多可以存放多长时间,到期后必须重新查询

缓存生命周期示意:
时间轴:
0s ──[获取 RR,TTL=86400]──► 86400s ──[必须丢弃,重新查询]──►
TTL 建议:
  - 典型主机记录:几天(86400s = 1天)
  - 预计要改变的记录:提前降低 TTL,改完再恢复
  - 零 TTL(TTL=0):禁止缓存
3.6.1 RR 文本表示

在区域主文件(Master File)中,RR 的文本格式为:

owner  TTL  class  type  RDATA

简化规则:

  • owner 为空 → 与上一条 RR 相同
  • classTTL 可省略(沿用默认或前一条)
  • 类型助记符总是在最后(type mnemonic)
    示例:
ISI.EDU.        MX      10  VENERA.ISI.EDU.
                MX      10  VAXA.ISI.EDU.
VENERA.ISI.EDU. A       128.9.0.32
                A       10.1.0.52
VAXA.ISI.EDU.   A       10.2.0.27
                A       128.9.0.33

这里有 6 条 RR,分属 3 个域名节点,每个节点有 2 条 RR。
不同 class 的示例:

XX.LCS.MIT.EDU.  IN   A   10.0.0.44     ; Internet 地址
                 CH   A   MIT.EDU. 2420  ; Chaos 地址
3.6.2 别名与规范名称(CNAME)

问题:同一资源有多个名字(例如 C.ISI.EDUUSC-ISIC.ARPA 是同一台主机)。
CNAME 记录解决这个问题:

USC-ISIC.ARPA   IN  CNAME  C.ISI.EDU.   ← USC-ISIC.ARPA 是别名
C.ISI.EDU.      IN  A      10.0.0.52    ← C.ISI.EDU 是规范名

CNAME 的特殊行为

  • 某节点有 CNAME 记录时,不应有其他 RR(保证别名和规范名不会数据不一致)
  • 查询某名称时若找到 CNAME,自动以规范名重新查询(除非查询类型本身是 CNAME)
  • 链式 CNAME 应当被追踪,循环 CNAME 应报错
    反向查找应指向规范名
; 正确:PTR 指向规范名
52.0.0.10.IN-ADDR.ARPA.   IN   PTR   C.ISI.EDU.
; 错误:PTR 指向别名(会造成额外间接层)
52.0.0.10.IN-ADDR.ARPA.   IN   PTR   USC-ISIC.ARPA.

3.7 查询(Queries)

查询是发给名称服务器的消息,包含:

  • QNAME:目标域名
  • QTYPE:查询的资源类型
  • QCLASS:查询的协议类
    DNS 消息结构(4 个 Section):
+──────────────────────────────────+
│ Header(头部):opcode、标志位等   │
+──────────────────────────────────+
│ Question(问题区):QNAME/QTYPE   │
+──────────────────────────────────+
│ Answer(回答区):直接回答查询的RR  │
+──────────────────────────────────+
│ Authority(权威区):权威服务器RR  │
+──────────────────────────────────+
│ Additional(附加区):可能有用的RR │
+──────────────────────────────────+

QTYPE 的特殊值

QTYPE 含义
A / MX / NS 等 精确匹配某类型
AXFR 区域传输(只在 TCP 上,用于同步完整区域数据)
MAILB 匹配所有邮箱相关 RR(MB、MG 等)
* 匹配所有 RR 类型

QCLASS 的特殊值

  • 具体类(IN、CH):精确匹配
  • *:匹配所有 class(但这类查询永远无法权威,因为服务器不一定知道所有 class)
3.7.1 标准查询示例

邮件程序查询 ISI.EDU 的邮件交换记录:

查询:QNAME=ISI.EDU., QTYPE=MX, QCLASS=IN
回答区:
  ISI.EDU.   MX   10   VENERA.ISI.EDU.
  ISI.EDU.   MX   10   VAXA.ISI.EDU.
附加区(服务器主动提供,猜测你之后会需要):
  VAXA.ISI.EDU.    A   10.2.0.27
  VAXA.ISI.EDU.    A   128.9.0.33
  VENERA.ISI.EDU.  A   10.1.0.52
  VENERA.ISI.EDU.  A   128.9.0.32
3.7.2 反向查询(可选)

将资源映射回域名的反向查找。实现可选,但所有服务器必须能理解反向查询消息(哪怕只返回"未实现"错误)。
注意:反向查询不能保证完整性和唯一性(DNS 按名称组织,不按资源),返回结果不应缓存
正确的 IP→主机名反向查找方式是用 IN-ADDR.ARPA 域(PTR 记录),而非反向查询。

第4章 名称服务器

4.1 简介

名称服务器是 DNS 数据库的仓库。核心职责:

用本地数据回答查询——要么给出答案,要么告诉客户端去哪里找答案。
关键特性:

  • 每个区域至少要有两台名称服务器(冗余)
  • 服务器的回答来自权威数据(zone data)缓存数据(cached data),响应头中会标明来源
  • 服务器知道自己对哪些区域有权威性

4.2 数据库如何划分为区域

两种分割方式

1. 按 class 分割:不同协议族(IN、CH)的数据完全独立,平行存在。
2. 按名称空间"切割"(cuts):在相邻节点之间画一刀,把整棵树切成若干连通子图,每个子图就是一个区域(Zone)

区域切割示意(竖线代表切割点):
         .(根)[区域A]
        / \
      MIL  EDU [区域A]
      |     |
    -----  -----  ← 切割点
    BRL   MIT [区域B]  ISI [区域C]
           |
          LCS

每个区域都是连通的,有且只有一个"顶点节点"(离根最近的节点)。区域名称通常就是该顶点的域名。

区域数据的四个部分

Zone 区域数据

区域内所有节点的权威数据
Authoritative Data

区域顶点节点的数据
Top Node SOA + NS

子区域的委托数据
Delegated Subzone NS

胶水记录
Glue RRs

胶水记录(Glue RRs) 解决"自举问题":

问题场景:
  父区域的 NS 记录说:LCS.MIT.EDU 的服务器是 XX.LCS.MIT.EDU.
  但要知道 XX.LCS.MIT.EDU 的 IP,就要查询 LCS.MIT.EDU 区域…
  而我们还没连上那个区域!→ 死循环!
解决方案:Glue RRs
  父区域额外携带 XX.LCS.MIT.EDU 的 A 记录(地址)
  这些 A 记录不是权威的,只用于"引路"
  有了 IP,才能去访问子区域服务器,才能获得权威数据
区域委托(Delegation)

当组织想管理自己的子域时:

  1. 联系父域管理员,协商委托
  2. 在父域中添加 NS 记录(指向新区域的服务器)和胶水记录(服务器的 IP)
  3. 新区域在自己的 SOA 中声明权威性

角色 操作
父区域管理员 添加 NS 记录 + Glue A 记录到父区域
新子域管理员 建立主文件,配置名称服务器,SOA 声明权威
两者需保持一致 NS 记录两侧(父/子)内容必须相同

4.3 名称服务器内部机制

4.3.1 查询与响应

名称服务器有两种工作模式:
非递归模式(必须实现):

  • 只用本地数据回答
  • 可以返回:答案、名称错误(NE)、或对更近的服务器的引用(Referral)
    递归模式(可选):
  • 服务器扮演客户端,代为向其他服务器查询,最终返回答案或错误(不会返回引用)
  • 不是所有客户端都能使用递归服务,服务器可以限制
    两个协商位

位标志 所在方 含义
RD(递归期望) 查询方 我希望你提供递归服务
RA(递归可用) 响应方 我能提供递归服务(不代表本次使用了)

客户端通过检查响应中 RA 和 RD 均为 1,来确认本次确实使用了递归模式。
递归响应的三种情况

  1. 答案(可能附带 CNAME 链)
  2. 名称错误(名字不存在)
  3. 临时错误
    非递归响应的情况
  4. 权威名称错误
  5. 临时错误
  6. 答案 + 来源标记(zone 还是 cache)
  7. 对更近服务器的引用
4.3.2 查询处理算法

名称服务器回答标准查询的 6 步算法:

3a. 完整匹配

3b. 碰到委托点 NS

3c. 某标签不存在

1. 设置 RA 标志
判断是否支持递归

RD=1 且支持递归?

5. 用本地解析器递归查询
将结果写入应答区

2. 在可用区域中
找最近的祖先区域

找到区域?

3. 在区域中逐标签向下匹配 QNAME

4. 在缓存中匹配

匹配结果

找到节点
处理 CNAME 或复制 RR 到应答区

引用:NS 到权威区
Glue 到附加区

有通配符 * ?

用通配符合成 RR

名称错误 NXDOMAIN

从缓存复制匹配 RR

6. 附加区补充有用 RR
结束

步骤3 详解——通配符(Wildcard)处理
通配符 RR 的 owner 形如 *.X.COM,规则:

  • 匹配 X.COM 下任意名称的查询(如 Z.X.COMY.X.COM
  • 不匹配 X.COM 本身
  • B.X.COM 有显式 RR,则通配符对 B.X.COM 及其子域(A.B.X.COM失效
  • 通配符不跨越区域委托边界
    通配符示例
X.COM           MX  10  A.X.COM      ; X.COM 本身的邮件交换
*.X.COM         MX  10  A.X.COM      ; X.COM 下任意名称的邮件交换
A.X.COM         A   1.2.3.4          ; A.X.COM 有显式记录
A.X.COM         MX  10  A.X.COM      ; A.X.COM 的显式邮件交换(抑制通配符)
*.A.X.COM       MX  10  A.X.COM      ; A.X.COM 子域的邮件交换

结果:

  • Z.X.COM 的 MX 查询 → 匹配 *.X.COM,返回 A.X.COM
  • A.X.COM 的 MX 查询 → 匹配显式记录,返回 A.X.COM(通配符被压制)
  • XX.COM 的 MX 查询 → 不匹配(XX.COM 不属于 X.COM)
4.3.4 否定响应缓存(可选)

名称错误(名字不存在)也可以被缓存,以避免对同一不存在名字反复查询。
机制:服务器在权威响应的附加区附上 SOA 记录,SOA 中的 MINIMUM 字段指定该否定结果可缓存多久。

4.3.5 区域维护与传输

主-辅结构

AXFR 区域传输

AXFR 区域传输

也可从辅助同步

编辑主文件
更新 SERIAL

主服务器
Primary

辅助服务器1
Secondary

辅助服务器2
Secondary

管理员

辅助服务器的刷新流程

辅助服务器通过 SOA 的 SERIAL 字段检测主服务器数据是否有更新。SERIAL 使用序列空间算术(避免32位整数溢出的绕回问题)。

SOA 参数:
  SERIAL  = 区域版本号,每次修改必须增大
  REFRESH = 每隔多少秒向主服务器检查一次(如 1800s = 30分钟)
  RETRY   = 检查失败后的重试间隔(如 300s = 5分钟)
  EXPIRE  = 如果 EXPIRE 秒内始终无法联系主服务器,丢弃本地副本(如 604800s = 1周)
  MINIMUM = 该区域所有 RR 的默认最小 TTL(如 86400s = 1天)
辅助服务器工作流程:
  加载区域后等待 REFRESH 秒
    ↓
  查询主服务器的 SOA,比较 SERIAL
    ├── SERIAL 未变 → 数据有效,再等 REFRESH 秒
    ├── SERIAL 变大 → 发起 AXFR 请求,下载完整区域
    └── 查询失败 → 每 RETRY 秒重试,超过 EXPIRE → 丢弃区域

AXFR 传输(区域传输)必须使用 TCP(因为数据量大,需要可靠传输)。第一条和最后一条消息必须包含区域顶点节点的数据。

第5章 解析器

5.1 简介

解析器是用户程序与 DNS 之间的中间件,位于用户的本地机器上,负责:

  • 接受用户程序的查询请求(通常是系统调用或库函数)
  • 向名称服务器发起 DNS 查询
  • 管理缓存,减少网络请求
    解析器的最重要目标:让大多数查询从缓存中直接回答,减少网络延迟和服务器负载
    共享缓存(多进程/多机器共用)比各自独立的缓存高效得多。

5.2 客户端-解析器接口

三个典型功能

功能1:主机名 → IP 地址

用户调用:gethostbyname("www.example.com")
  ↓
解析器发起 QTYPE=A 的 DNS 查询
  ↓
返回 32 位 IPv4 地址列表

建议返回多个地址(主机可能有多个 IP),但为了兼容旧 API,也可以只返回一个。
功能2:IP 地址 → 主机名
IP 地址字节序倒置,拼上 IN-ADDR.ARPA 后缀,发起 PTR 查询:

查询 IP 1.2.3.4 对应的主机名:
  将 1.2.3.4 倒转 → 4.3.2.1
  拼接后缀 → 4.3.2.1.IN-ADDR.ARPA.
  发起 QTYPE=PTR 查询
  返回主机名字符串

为什么要倒转字节序?
普通域名树从左(最具体)到右(最通用):host.domain.tld
IP 地址从左(最通用,网络前缀)到右(最具体,主机号):192.168.1.5
为了让 IP 也能在同一棵树里用同样的方式组织(更通用的在树的高层),必须把字节序倒转:5.1.168.192.IN-ADDR.ARPA.
功能3:通用查找
直接指定 QNAME、QTYPE、QCLASS,返回所有匹配 RR(原始格式,含 TTL)。

可能的返回结果

结果类型 含义
一条或多条 RR 成功找到请求的数据
名称错误(NE) 名字不存在(如拼写错误)
数据未找到错误 名字存在,但没有对应类型的 RR(如查邮件服务器却只有A记录)
临时错误 网络问题,不是数据不存在,不应报告为"不存在"

注意:前两种错误(NE 和数据未找到)在 API 层面可以合并,但底层不应混淆——否则会导致无效的重复查询。

5.3 解析器内部机制

5.3.1 存根解析器(Stub Resolver)

最简单的解析器实现:把所有工作外包给一台支持递归的名称服务器。

本地机器                          外部递归服务器
  ┌──────────┐                  ┌─────────────┐
  │ 应用程序  │─── DNS 查询(RD=1)→│  递归服务器  │─── 追问各级服务器
  │ Stub      │←── 答案 ─────────│             │
  └──────────┘                  └─────────────┘
优点:本地几乎不需要逻辑,适合资源受限的 PC
缺点:依赖外部服务器,TCP 可能有负担,重传策略难以优化

存根解析器需要一个配置文件,列出递归服务器的 IP。

5.3.2 解析器数据结构

完整解析器维护以下关键数据结构:

数据结构 含义
SNAME 当前搜索的目标域名
STYPE 查询类型(QTYPE)
SCLASS 查询类(QCLASS)
SLIST 候选名称服务器列表(含地址、匹配距离、历史成功率),动态更新
SBELT 安全带(Safety Belt):从配置文件初始化,存放兜底服务器(通常2个根+2个本地)
CACHE 缓存:存储历史响应结果,存储绝对过期时间而非剩余 TTL

SLIST 的匹配计数(match count)
匹配计数 = 从根向下,SNAME 与候选服务器所管辖区域的公共标签数。
数值越大,说明候选服务器越"接近"目标。
match_count ( S ) = 从根往下,SNAME 与服务器区域的公共标签数 \text{match\_count}(S) = \text{从根往下,SNAME 与服务器区域的公共标签数} match_count(S)=从根往下,SNAME 与服务器区域的公共标签数
SBELT 的 match count 初始化为 − 1 -1 1(最不"接近",兜底使用)。

5.3.3 解析器核心算法

4 步循环:

a. 答案/名称错误

b. 更近的委托

c. CNAME

d. 服务器故障

1. 检查本地缓存
或本地共享 zone

找到答案?

返回给客户端

2. 确定要问哪些服务器
查 SNAME 的 NS RR
从最近的祖先开始

找到 NS?

使用 SBELT 兜底服务器

3. 向 SLIST 中的服务器发查询
超时轮换,所有地址都尝试

4. 分析响应

响应类型?

缓存 + 返回客户端

更新 SLIST → 回到步骤2

更新 SNAME → 回到步骤1

从 SLIST 删除 → 回到步骤3

步骤2 的优先级策略

  1. 避免无限循环(绑定工作量上限)
  2. 尽量得到答案
  3. 避免不必要的传输
  4. 尽快得到答案
    SLIST 中的服务器可以按优先级排序:本地网络上的服务器优先,同时参考历史响应时间统计。

第6章 综合场景演示

6.1 区域文件示例

根区域(部分)
.       IN      SOA     SRI-NIC.ARPA. HOSTMASTER.SRI-NIC.ARPA. (
                        870611          ; serial
                        1800            ; refresh 每30分钟检查更新
                        300             ; retry  失败后5分钟重试
                        604800          ; expire  1周后丢弃
                        86400           ; minimum 最小TTL=1天
                        )
                NS      A.ISI.EDU.      ; 根服务器1
                NS      C.ISI.EDU.      ; 根服务器2
                NS      SRI-NIC.ARPA.   ; 根服务器3
MIL.    86400   NS      SRI-NIC.ARPA.   ; MIL 委托给这两台
        86400   NS      A.ISI.EDU.
EDU.    86400   NS      SRI-NIC.ARPA.   ; EDU 委托给这两台
        86400   NS      C.ISI.EDU.
SRI-NIC.ARPA.   A       26.0.0.73       ; Glue:根服务器的地址
                A       10.0.0.51
                MX      0 SRI-NIC.ARPA.
                HINFO   DEC-2060 TOPS20
USC-ISIC.ARPA.  CNAME   C.ISI.EDU.      ; 别名记录
; 反向记录(PTR)
73.0.0.26.IN-ADDR.ARPA.  PTR   SRI-NIC.ARPA.
52.0.0.10.IN-ADDR.ARPA.  PTR   C.ISI.EDU.
EDU 区域(部分)
EDU.  IN SOA SRI-NIC.ARPA. HOSTMASTER.SRI-NIC.ARPA. (
                870729
                1800
                300
                604800
                86400
                )
              NS SRI-NIC.ARPA.
              NS C.ISI.EDU.
ISI   172800 NS VAXA.ISI        ; ISI.EDU 委托(相对名,基于 EDU 原点)
              172800 NS A.ISI
              172800 NS VENERA.ISI.EDU.
VAXA.ISI  172800 A 10.2.0.27    ; Glue:ISI 服务器地址
          172800 A 128.9.0.33
MIT.EDU.  43200 NS XX.LCS.MIT.EDU.   ; MIT.EDU 委托
          43200 NS ACHILLES.MIT.EDU.
XX.LCS.MIT.EDU.   43200 A 10.0.0.44  ; Glue
ACHILLES.MIT.EDU. 43200 A 18.72.0.8

注意:区域文件中可以混用相对名和绝对名。有末尾 . 是绝对名;无末尾 . 是相对名,会被补上区域原点(origin)。

6.2 标准查询示例详解

示例1:QNAME=SRI-NIC.ARPA, QTYPE=A(权威回答)
查询(发给 C.ISI.EDU):
  Header:   OPCODE=SQUERY
  Question: QNAME=SRI-NIC.ARPA., QCLASS=IN, QTYPE=A
响应(来自 C.ISI.EDU,权威数据):
  Header:   OPCODE=SQUERY, RESPONSE, AA   ← AA=1 表示权威回答
  Question: QNAME=SRI-NIC.ARPA., QCLASS=IN, QTYPE=A
  Answer:   SRI-NIC.ARPA. 86400 IN A 26.0.0.73
            SRI-NIC.ARPA. 86400 IN A 10.0.0.51

若发给非权威服务器(从缓存回答):

  Header:   OPCODE=SQUERY, RESPONSE      ← AA=0 表示非权威(来自缓存)
  Answer:   SRI-NIC.ARPA. 1777 IN A 10.0.0.51  ← TTL 已衰减(不再是86400)

AA 位和 TTL 是区分权威/非权威的两个关键信号。

示例2:QTYPE=*(通配类型查询)
权威服务器回应 QTYPE=* 的 SRI-NIC.ARPA 查询:
  Answer:
    SRI-NIC.ARPA. 86400 IN A     26.0.0.73
                          A     10.0.0.51
                          MX    0 SRI-NIC.ARPA.
                          HINFO DEC-2060 TOPS20

不同的非权威服务器可能只缓存了不同类型的数据(一台缓存了 A,另一台缓存了 HINFO),这正说明了 QTYPE=* 的缓存语义不完整性。

示例3:QTYPE=MX(附加区的作用)
查询:QNAME=SRI-NIC.ARPA., QTYPE=MX
答案区:
  SRI-NIC.ARPA. 86400 IN MX 0 SRI-NIC.ARPA.
附加区(服务器主动提供,因为使用 MX 必然需要地址):
  SRI-NIC.ARPA. 86400 IN A 26.0.0.73
  SRI-NIC.ARPA. 86400 IN A 10.0.0.51
示例4:名称不存在(NXDOMAIN)
查询:QNAME=SIR-NIC.ARPA.(拼错了!)
响应:
  Header:    OPCODE=SQUERY, RESPONSE, AA, RCODE=NE  ← NE=名称错误
  Authority: .  SOA SRI-NIC.ARPA. HOSTMASTER.SRI-NIC.ARPA.
                    870611 1800 300 604800 86400

Authority 区中的 SOA 是"否定缓存信息"——告诉解析器,在 MINIMUM(86400s)内,这个名字不存在,不需要再查了。

示例5:引用(Referral)
查询:QNAME=BRL.MIL(发给 C.ISI.EDU,但它只管 EDU 和 ARPA)
响应(不是权威回答,是引用):
  Header:    OPCODE=SQUERY, RESPONSE     ← 无 AA
  Answer:    空
  Authority: MIL. 86400 IN NS SRI-NIC.ARPA.
             MIL. 86400 IN NS A.ISI.EDU.
  Additional: A.ISI.EDU.    A 26.3.0.103  ← Glue:告诉你去哪里找
              SRI-NIC.ARPA. A 26.0.0.73
              SRI-NIC.ARPA. A 10.0.0.51

C.ISI.EDU 说:“我不管 MIL,你去问 SRI-NIC.ARPA 或 A.ISI.EDU,它们的地址我给你了。”

示例6:CNAME 处理
查询:QNAME=USC-ISIC.ARPA, QTYPE=A(发给 A.ISI.EDU)
响应:
  Answer:
    USC-ISIC.ARPA. 86400 IN CNAME C.ISI.EDU.   ← 先返回 CNAME
    C.ISI.EDU.     86400 IN A     10.0.0.52     ← 再返回最终答案

AA 位表示 USC-ISIC.ARPA 这条 CNAME 是权威的,但不代表 C.ISI.EDU 的 A 记录也是权威的。
(A.ISI.EDU 碰巧同时对这两个域有权威,所以能一次返回全部)
若发给 C.ISI.EDU(它不一定有 C.ISI.EDU 的 A 记录在缓存里):

  Answer:    USC-ISIC.ARPA. 86400 IN CNAME C.ISI.EDU.  ← 只有 CNAME
  Authority: ISI.EDU. 172800 IN NS VAXA.ISI.EDU.        ← 告诉你去哪找 C.ISI.EDU
                               NS A.ISI.EDU.
                               NS VENERA.ISI.EDU.
  Additional: 各服务器地址...

6.3 解析器实战追踪

场景:冷启动后查询 ISI.EDU 的 MX 记录

SBELT(配置文件中的兜底服务器):

Match count = -1
SRI-NIC.ARPA.   26.0.0.73  10.0.0.51
A.ISI.EDU.      26.3.0.103

第一轮:缓存为空,向 SBELT 中的服务器发查询。
SRI-NIC.ARPA 返回委托(更近的引用):

Authority:  ISI.EDU. NS VAXA.ISI.EDU.
                     NS A.ISI.EDU.
                     NS VENERA.ISI.EDU.
Additional: 各服务器地址

解析器更新 SLIST(match count 从 -1 提升到 3,因为 ISI.EDU 有3个标签匹配):

Match count = 3
A.ISI.EDU.       26.3.0.103
VAXA.ISI.EDU.    10.2.0.27   128.9.0.33
VENERA.ISI.EDU.  10.1.0.52   128.9.0.32

第二轮:向 SLIST 发查询,最终从某台 ISI.EDU 的权威服务器得到:

Answer:
  ISI.EDU. MX 10 VENERA.ISI.EDU.
  ISI.EDU. MX 20 VAXA.ISI.EDU.
Additional:
  VAXA.ISI.EDU.    A 10.2.0.27  / A 128.9.0.33
  VENERA.ISI.EDU.  A 10.1.0.52  / A 128.9.0.32

结果缓存,返回客户端。

C++ 实战代码

以下代码模拟 DNS 的核心数据结构与查询逻辑,包含:

  • 资源记录(RR)结构
  • 区域(Zone)管理
  • 简化的名称服务器查询算法
  • 简化的解析器缓存与查询流程
/*
 * dns_simulator.cpp
 *
 * 模拟 DNS RFC 1034 的核心概念(教学用简化版本)
 * 涵盖:
 *   - 资源记录(RR)数据结构
 *   - 区域(Zone)加载与查询
 *   - TTL 缓存管理
 *   - 迭代查询流程(Iterative Resolution)
 *   - CNAME 追踪
 *
 * 编译:g++ -std=c++17 dns_simulator.cpp -o dns_simulator
 */
#include <chrono>
#include <cstdint>
#include <iostream>
#include <map>
#include <optional>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include <stdexcept>
// ============================================================
//  工具函数:域名标准化(转小写,保证末尾有 .)
// ============================================================
static std::string normalize_name(const std::string& name) {
    std::string result = name;
    // 转换为小写(DNS 不区分大小写)
    std::transform(result.begin(), result.end(), result.begin(),
                   [](unsigned char c) { return std::tolower(c); });
    // 保证末尾是 .(绝对域名)
    if (result.empty() || result.back() != '.') {
        result += '.';
    }
    return result;
}
// ============================================================
//  资源记录类型枚举(来自 RFC 1034 Section 3.6)
// ============================================================
enum class RRType {
    A,      // 主机地址(IPv4)
    NS,     // 名称服务器
    CNAME,  // 规范名称(别名)
    MX,     // 邮件交换
    SOA,    // 区域起始
    PTR,    // 反向查找指针
    HINFO,  // 主机信息
    ANY,    // 通配类型(* 查询)
};
// 类型到字符串的映射,便于打印
static std::string rr_type_str(RRType t) {
    switch (t) {
        case RRType::A:     return "A";
        case RRType::NS:    return "NS";
        case RRType::CNAME: return "CNAME";
        case RRType::MX:    return "MX";
        case RRType::SOA:   return "SOA";
        case RRType::PTR:   return "PTR";
        case RRType::HINFO: return "HINFO";
        case RRType::ANY:   return "*";
        default:            return "UNKNOWN";
    }
}
// ============================================================
//  资源记录(RR)结构体
//  对应 RFC 1034 Section 3.6:owner / type / class / TTL / RDATA
// ============================================================
struct ResourceRecord {
    std::string owner;    // 所属域名(已标准化,末尾带 .)
    RRType      type;     // 资源类型
    std::string rr_class; // 协议类("IN" 或 "CH")
    uint32_t    ttl;      // 存活时间(秒)
    std::string rdata;    // 资源数据(文本表示,简化)
    // 用于缓存:记录何时写入缓存(单调时钟时间点)
    // 实际比较时用 (当前时间 - cached_at) 与 ttl 比较
    std::chrono::steady_clock::time_point cached_at;
    bool is_authoritative = false; // 是否来自权威数据(zone 数据)
    void print(const std::string& prefix = "") const {
        std::cout << prefix
                  << owner << " "
                  << ttl << " "
                  << rr_class << " "
                  << rr_type_str(type) << " "
                  << rdata << "\n";
    }
};
// ============================================================
//  DNS 消息结构(简化版)
//  对应 RFC 1034 Section 3.7:四个 Section
// ============================================================
struct DnsResponse {
    bool                      authoritative = false; // AA 位
    bool                      is_referral   = false; // 是引用还是答案
    std::string               qname;                 // 查询名
    RRType                    qtype;                 // 查询类型
    std::vector<ResourceRecord> answer;     // 回答区
    std::vector<ResourceRecord> authority;  // 权威区(委托信息)
    std::vector<ResourceRecord> additional; // 附加区(辅助信息)
    // 判断是否得到了有效答案
    bool has_answer() const { return !answer.empty(); }
    void print() const {
        std::cout << "  [响应] AA=" << authoritative
                  << " Referral=" << is_referral << "\n";
        if (!answer.empty()) {
            std::cout << "  [Answer]\n";
            for (const auto& rr : answer) rr.print("    ");
        }
        if (!authority.empty()) {
            std::cout << "  [Authority]\n";
            for (const auto& rr : authority) rr.print("    ");
        }
        if (!additional.empty()) {
            std::cout << "  [Additional]\n";
            for (const auto& rr : additional) rr.print("    ");
        }
    }
};
// ============================================================
//  区域(Zone)类
//  对应 RFC 1034 Section 4.2:区域是权威数据的基本单元
// ============================================================
class Zone {
public:
    std::string zone_name;   // 区域顶点名称(如 "isi.edu.")
    uint32_t    min_ttl = 86400; // SOA MINIMUM
    // zone_name → [list of RRs]
    // 使用 map 保证顺序稳定,便于调试
    std::map<std::string, std::vector<ResourceRecord>> records;
    // 向区域中添加一条资源记录
    void add_record(const ResourceRecord& rr) {
        std::string key = normalize_name(rr.owner);
        records[key].push_back(rr);
    }
    // 在区域内查找匹配 QNAME 和 QTYPE 的 RR
    // 返回 DnsResponse(权威数据)
    DnsResponse query(const std::string& qname_raw, RRType qtype) const {
        DnsResponse resp;
        resp.qname = qname_raw;
        resp.qtype = qtype;
        std::string qname = normalize_name(qname_raw);
        // 精确查找 QNAME 对应的所有 RR
        auto it = records.find(qname);
        if (it == records.end()) {
            // 名字不存在,尝试查找通配符 *.parent_domain
            // 简化实现:只检查直接通配符
            std::string parent = get_parent(qname);
            std::string wildcard = "*." + parent;
            auto wit = records.find(wildcard);
            if (wit != records.end()) {
                // 通配符命中,复制 RR 但将 owner 替换为 QNAME
                for (const auto& rr : wit->second) {
                    if (qtype == RRType::ANY || rr.type == qtype) {
                        ResourceRecord synth = rr;
                        synth.owner = qname; // 合成 RR:owner 改为查询名
                        resp.answer.push_back(synth);
                    }
                }
                if (!resp.answer.empty()) {
                    resp.authoritative = true;
                    return resp;
                }
            }
            // 真的不存在:名称错误(NXDOMAIN),返回权威标记
            resp.authoritative = true;
            return resp;
        }
        const auto& node_rrs = it->second;
        // 检查是否有 CNAME(且查询类型不是 CNAME)
        // 若有,返回 CNAME(由调用者决定是否追踪)
        for (const auto& rr : node_rrs) {
            if (rr.type == RRType::CNAME && qtype != RRType::CNAME) {
                resp.answer.push_back(rr);
                resp.authoritative = true;
                // 标记:需要以 CNAME 的规范名重新查询
                return resp;
            }
        }
        // 正常匹配:收集所有符合 QTYPE 的 RR
        for (const auto& rr : node_rrs) {
            if (qtype == RRType::ANY || rr.type == qtype) {
                resp.answer.push_back(rr);
            }
        }
        if (!resp.answer.empty()) {
            resp.authoritative = true;
        } else {
            // 名字存在但无对应类型(数据未找到)
            resp.authoritative = true; // 权威确认"不存在该类型"
        }
        return resp;
    }
    // 检查 QNAME 是否属于本区域(用于判断是否需要委托)
    bool is_authoritative_for(const std::string& qname_raw) const {
        std::string qname = normalize_name(qname_raw);
        std::string zname = normalize_name(zone_name);
        // qname 以 zname 结尾,说明在本区域权威范围内
        if (qname.size() < zname.size()) return false;
        return qname.substr(qname.size() - zname.size()) == zname;
    }
private:
    // 获取一个域名的父域(去掉第一个标签)
    static std::string get_parent(const std::string& name) {
        size_t dot = name.find('.');
        if (dot == std::string::npos || dot + 1 >= name.size()) {
            return "."; // 父域是根
        }
        return name.substr(dot + 1);
    }
};
// ============================================================
//  简单缓存(对应 RFC 1034 Section 5.3.2)
//  存储 TTL 已转换为绝对过期时间的 RR
// ============================================================
class DnsCache {
public:
    // 缓存 key = normalized_name + ":" + rr_type
    using Key = std::string;
    struct CacheEntry {
        std::vector<ResourceRecord> records;
        std::chrono::steady_clock::time_point expires_at; // 绝对过期时间
    };
    // 向缓存写入一批 RR
    void store(const std::vector<ResourceRecord>& rrs) {
        for (const auto& rr : rrs) {
            if (rr.ttl == 0) continue; // TTL=0 不缓存
            std::string key = make_key(rr.owner, rr.type);
            auto now = std::chrono::steady_clock::now();
            auto& entry = cache_[key];
            // 简化:覆盖同类型的旧缓存
            entry.records.clear();
            entry.records.push_back(rr);
            // 存储绝对过期时间(当前时间 + TTL秒)
            entry.expires_at = now + std::chrono::seconds(rr.ttl);
        }
    }
    // 从缓存查找(自动过滤过期条目)
    std::vector<ResourceRecord> lookup(const std::string& name,
                                        RRType type) const {
        std::string key = make_key(normalize_name(name), type);
        auto it = cache_.find(key);
        if (it == cache_.end()) return {};
        auto now = std::chrono::steady_clock::now();
        if (now > it->second.expires_at) {
            // 已过期(只读操作,不修改缓存;过期条目将在下次写入时覆盖)
            return {};
        }
        // 计算剩余 TTL(返回给调用者,反映缓存老化)
        auto remaining_secs = std::chrono::duration_cast<std::chrono::seconds>(
            it->second.expires_at - now).count();
        std::vector<ResourceRecord> result = it->second.records;
        for (auto& rr : result) {
            // 更新 TTL 为剩余存活时间(这就是缓存中 TTL 与权威值不同的原因)
            rr.ttl = static_cast<uint32_t>(
                std::max<long long>(0, remaining_secs));
            rr.is_authoritative = false; // 缓存数据非权威
        }
        return result;
    }
private:
    mutable std::unordered_map<Key, CacheEntry> cache_;
    static std::string make_key(const std::string& name, RRType type) {
        return name + ":" + rr_type_str(type);
    }
};
// ============================================================
//  简化的名称服务器(Name Server)
//  持有若干 Zone,可回答查询
// ============================================================
class NameServer {
public:
    std::string server_name;    // 服务器自身的名称(如 "c.isi.edu.")
    void add_zone(const Zone& z) {
        zones_.push_back(z);
    }
    // 处理一次 DNS 查询(非递归模式)
    // 返回权威答案或引用(Referral)
    DnsResponse handle_query(const std::string& qname, RRType qtype) const {
        std::cout << "  [" << server_name << "] 收到查询: "
                  << qname << " / " << rr_type_str(qtype) << "\n";
        std::string norm_qname = normalize_name(qname);
        // 找到与 QNAME 最匹配(最接近)的权威区域
        const Zone* best_zone = nullptr;
        size_t best_len = 0;
        for (const auto& z : zones_) {
            std::string zn = normalize_name(z.zone_name);
            // 检查 QNAME 是否以 zone_name 结尾
            if (norm_qname.size() >= zn.size() &&
                norm_qname.substr(norm_qname.size() - zn.size()) == zn) {
                if (zn.size() > best_len) {
                    best_len = zn.size();
                    best_zone = &z;
                }
            }
        }
        if (!best_zone) {
            // 本服务器无任何相关区域,返回空(调用方需用引用继续)
            DnsResponse resp;
            resp.qname = qname;
            resp.qtype = qtype;
            resp.authoritative = false;
            std::cout << "    -> 无权威区域,无法处理\n";
            return resp;
        }
        std::cout << "    -> 使用区域: " << best_zone->zone_name << "\n";
        DnsResponse resp = best_zone->query(qname, qtype);
        if (!resp.has_answer()) {
            std::cout << "    -> 未找到记录(NXDOMAIN 或数据不存在)\n";
        } else {
            std::cout << "    -> 找到 " << resp.answer.size() << " 条记录\n";
        }
        return resp;
    }
private:
    std::vector<Zone> zones_;
};
// ============================================================
//  简化的解析器(Resolver)
//  对应 RFC 1034 Section 5.3.3 的 4 步算法
// ============================================================
class Resolver {
public:
    DnsCache cache;
    // 配置已知的名称服务器(简化:直接存引用)
    void add_server(const NameServer* ns) {
        servers_.push_back(ns);
    }
    // 主查询接口:解析 QNAME/QTYPE
    // 返回最终 RR 列表(空表示失败)
    std::vector<ResourceRecord> resolve(const std::string& qname,
                                         RRType qtype,
                                         int max_cname_depth = 5) {
        std::cout << "\n=== 解析器查询: " << qname
                  << " / " << rr_type_str(qtype) << " ===\n";
        std::string current_name = qname;
        // 最多追踪 max_cname_depth 层 CNAME,防止循环
        for (int depth = 0; depth <= max_cname_depth; ++depth) {
            // 步骤1:检查缓存
            {
                auto cached = cache.lookup(current_name, qtype);
                if (!cached.empty()) {
                    std::cout << "  [缓存命中] " << current_name
                              << " TTL=" << cached[0].ttl << "s 剩余\n";
                    return cached;
                }
            }
            // 步骤2~4:向已知服务器查询
            std::vector<ResourceRecord> result;
            bool found = false;
            for (const auto* ns : servers_) {
                DnsResponse resp = ns->handle_query(current_name, qtype);
                if (!resp.has_answer()) continue;
                // 检查响应中是否有 CNAME
                if (resp.answer.size() == 1 &&
                    resp.answer[0].type == RRType::CNAME &&
                    qtype != RRType::CNAME) {
                    // 追踪 CNAME:将 current_name 换成规范名
                    std::string canonical = resp.answer[0].rdata;
                    std::cout << "  [CNAME] " << current_name
                              << " -> " << canonical << "\n";
                    cache.store(resp.answer); // 缓存 CNAME 本身
                    current_name = canonical;
                    found = true;
                    break; // 以新名字重新查询
                }
                // 得到正常答案
                cache.store(resp.answer); // 缓存结果
                cache.store(resp.additional); // 缓存附加区数据
                result = resp.answer;
                found = true;
                break;
            }
            if (!found) {
                std::cout << "  [失败] 所有服务器都未能回答 " << current_name << "\n";
                return {};
            }
            if (!result.empty()) {
                return result; // 成功
            }
            // result 为空但 found=true 说明正在追踪 CNAME,继续循环
        }
        std::cout << "  [失败] CNAME 链过长(超过 "
                  << max_cname_depth << " 层)\n";
        return {};
    }
    // 反向查找辅助:IP 地址转 IN-ADDR.ARPA 域名
    // RFC 1034 Section 5.2.1 功能2
    static std::string ip_to_arpa(const std::string& ip) {
        // 将 "1.2.3.4" 转为 "4.3.2.1.IN-ADDR.ARPA."
        std::vector<std::string> octets;
        std::stringstream ss(ip);
        std::string octet;
        while (std::getline(ss, octet, '.')) {
            octets.push_back(octet);
        }
        if (octets.size() != 4) {
            throw std::invalid_argument("无效的 IPv4 地址: " + ip);
        }
        // 倒转字节序
        return octets[3] + "." + octets[2] + "." +
               octets[1] + "." + octets[0] + ".IN-ADDR.ARPA.";
    }
private:
    std::vector<const NameServer*> servers_;
};
// ============================================================
//  演示:构建示例 DNS 域(基于 RFC 1034 Section 6 的示例)
// ============================================================
// 构造一个简化的 ISI.EDU 区域
static Zone build_isi_edu_zone() {
    Zone z;
    z.zone_name = "isi.edu.";
    z.min_ttl   = 86400;
    auto add = [&](const std::string& owner, RRType type,
                   uint32_t ttl, const std::string& rdata) {
        ResourceRecord rr;
        rr.owner    = normalize_name(owner);
        rr.type     = type;
        rr.rr_class = "IN";
        rr.ttl      = ttl;
        rr.rdata    = rdata;
        rr.is_authoritative = true;
        z.add_record(rr);
    };
    // ISI.EDU 的 MX 记录(RFC 1034 示例中的 ISI.EDU MX 数据)
    add("ISI.EDU",    RRType::MX, 86400, "10 VENERA.ISI.EDU.");
    add("ISI.EDU",    RRType::MX, 86400, "20 VAXA.ISI.EDU.");
    // 各主机的 A 记录
    add("VENERA.ISI.EDU", RRType::A, 86400, "10.1.0.52");
    add("VENERA.ISI.EDU", RRType::A, 86400, "128.9.0.32");
    add("VAXA.ISI.EDU",   RRType::A, 86400, "10.2.0.27");
    add("VAXA.ISI.EDU",   RRType::A, 86400, "128.9.0.33");
    add("A.ISI.EDU",      RRType::A, 86400, "26.3.0.103");
    add("C.ISI.EDU",      RRType::A, 86400, "10.0.0.52");
    // CNAME 别名(USC-ISIC.ARPA → C.ISI.EDU)
    add("USC-ISIC.ARPA", RRType::CNAME, 86400, "C.ISI.EDU.");
    return z;
}
// 构造根区域(简化)
static Zone build_root_zone() {
    Zone z;
    z.zone_name = ".";
    z.min_ttl   = 86400;
    auto add = [&](const std::string& owner, RRType type,
                   uint32_t ttl, const std::string& rdata) {
        ResourceRecord rr;
        rr.owner    = normalize_name(owner);
        rr.type     = type;
        rr.rr_class = "IN";
        rr.ttl      = ttl;
        rr.rdata    = rdata;
        rr.is_authoritative = true;
        z.add_record(rr);
    };
    // 根区的 NS 记录(委托给 SRI-NIC.ARPA)
    add("SRI-NIC.ARPA", RRType::A,  86400, "26.0.0.73");
    add("SRI-NIC.ARPA", RRType::A,  86400, "10.0.0.51");
    add("SRI-NIC.ARPA", RRType::MX, 86400, "0 SRI-NIC.ARPA.");
    add("ACC.ARPA",     RRType::A,  86400, "26.6.0.65");
    // PTR 反向记录
    add("73.0.0.26.IN-ADDR.ARPA", RRType::PTR, 86400, "SRI-NIC.ARPA.");
    add("65.0.6.26.IN-ADDR.ARPA", RRType::PTR, 86400, "ACC.ARPA.");
    return z;
}
// ============================================================
//  主函数:演示各种 DNS 查询场景
// ============================================================
int main() {
    std::cout << "=================================================\n";
    std::cout << "DNS RFC 1034 模拟器演示\n";
    std::cout << "=================================================\n\n";
    // --- 构建名称服务器 ---
    NameServer c_isi_edu;
    c_isi_edu.server_name = "c.isi.edu.";
    c_isi_edu.add_zone(build_root_zone());
    c_isi_edu.add_zone(build_isi_edu_zone());
    // --- 构建解析器,使用 C.ISI.EDU 作为上游服务器 ---
    Resolver resolver;
    resolver.add_server(&c_isi_edu);
    // ── 演示1:查询 ISI.EDU 的 MX 记录 ──────────────────────
    std::cout << "【演示1】查询 ISI.EDU 的邮件交换记录(MX)\n";
    {
        auto rrs = resolver.resolve("ISI.EDU", RRType::MX);
        std::cout << "  最终结果:\n";
        for (const auto& rr : rrs) rr.print("    ");
    }
    // ── 演示2:第二次查询,应该命中缓存 ─────────────────────
    std::cout << "\n【演示2】再次查询 ISI.EDU MX(应命中缓存)\n";
    {
        auto rrs = resolver.resolve("ISI.EDU", RRType::MX);
        std::cout << "  最终结果(来自缓存):\n";
        for (const auto& rr : rrs) rr.print("    ");
    }
    // ── 演示3:查询 VENERA.ISI.EDU 的 A 记录 ─────────────────
    std::cout << "\n【演示3】查询 VENERA.ISI.EDU 的 IP 地址(A 记录)\n";
    {
        auto rrs = resolver.resolve("VENERA.ISI.EDU", RRType::A);
        std::cout << "  最终结果:\n";
        for (const auto& rr : rrs) rr.print("    ");
    }
    // ── 演示4:CNAME 追踪 USC-ISIC.ARPA → C.ISI.EDU ─────────
    std::cout << "\n【演示4】查询 USC-ISIC.ARPA 的地址(CNAME 追踪)\n";
    {
        auto rrs = resolver.resolve("USC-ISIC.ARPA", RRType::A);
        std::cout << "  最终结果:\n";
        for (const auto& rr : rrs) rr.print("    ");
    }
    // ── 演示5:直接查询 CNAME 类型(不追踪)─────────────────
    std::cout << "\n【演示5】直接查询 USC-ISIC.ARPA 的 CNAME 记录\n";
    {
        auto rrs = resolver.resolve("USC-ISIC.ARPA", RRType::CNAME);
        std::cout << "  最终结果(CNAME 本身):\n";
        for (const auto& rr : rrs) rr.print("    ");
    }
    // ── 演示6:反向查找(IP → 主机名)───────────────────────
    std::cout << "\n【演示6】反向查找 IP 26.6.0.65 对应的主机名\n";
    {
        std::string arpa_name = Resolver::ip_to_arpa("26.6.0.65");
        std::cout << "  IP 转 ARPA 域名: " << arpa_name << "\n";
        auto rrs = resolver.resolve(arpa_name, RRType::PTR);
        std::cout << "  最终结果:\n";
        for (const auto& rr : rrs) rr.print("    ");
    }
    // ── 演示7:查询不存在的名字(NXDOMAIN)───────────────────
    std::cout << "\n【演示7】查询不存在的名字 SIR-NIC.ARPA(拼写错误)\n";
    {
        auto rrs = resolver.resolve("SIR-NIC.ARPA", RRType::A);
        if (rrs.empty()) {
            std::cout << "  结果: 名称不存在(NXDOMAIN)\n";
        }
    }
    // ── 演示8:ip_to_arpa 工具函数验证 ───────────────────────
    std::cout << "\n【演示8】ip_to_arpa 工具函数测试\n";
    {
        auto test_ip = [](const std::string& ip) {
            std::cout << "  " << ip << " -> "
                      << Resolver::ip_to_arpa(ip) << "\n";
        };
        test_ip("1.2.3.4");
        test_ip("10.0.0.44");
        test_ip("128.9.0.32");
    }
    std::cout << "\n=================================================\n";
    std::cout << "演示完毕\n";
    std::cout << "=================================================\n";
    return 0;
}

核心速查表


概念 关键要点
域名树 有根的树,节点有 0~63 字节标签,路径从叶到根构成域名
绝对域名 末尾有 .,从根开始;无末尾 . 是相对名(需补全原点)
域名大小写 存储保留大小写,比较不区分大小写
域名总长度 所有标签字节 + 长度字节之和 ≤ 255 \leq 255 255
标签字符限制 字母开头,字母/数字结尾,中间可含 -,最长 63 字节
RR 基本结构 owner / type / class / TTL / RDATA
TTL 含义 缓存中该记录的最长存活时间(秒);0 表示不缓存
CNAME 规则 CNAME 节点不能有其他 RR;查询自动追踪;循环应报错
区域(Zone) 连通的命名空间片段,有唯一顶点,可被独立管理和传输
Glue 记录 子区域服务器在父区域中的 A 记录,解决自举问题
SOA 作用 区域管理参数(SERIAL/REFRESH/RETRY/EXPIRE/MINIMUM)
区域传输(AXFR) 辅助服务器从主服务器下载完整区域,必须用 TCP
递归 vs 迭代 递归:服务器代追;迭代:服务器给引用,客户端自追
RA 位 服务器声明"我支持递归",不代表本次使用了递归
RD 位 客户端声明"我要递归服务"
反向查找 IP 字节倒转 + .IN-ADDR.ARPA.,发 PTR 查询
SLIST 解析器的候选服务器列表,按匹配计数(接近度)排序
SBELT 解析器的"安全带",match count=-1,兜底服务器列表
否定缓存 响应附加 SOA,告知解析器 MINIMUM 秒内不必再查该不存在的名字
通配符 RR *.X 匹配 X 下任何子名(至少一个完整标签);显式 RR 压制通配符
AA 位 权威回答标志;无 AA = 缓存数据(TTL 已衰减)

Logo

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

更多推荐