协议设计原理:从状态机到向后兼容
0. 元信息
- 主题路径:
docs/topics/network/subtopics/protocol-design/ - 父主题:
network - 主分类:计算机基础
- 辅助分类:工程技术
- 适合对象:写过 TCP/UDP 客户端、读过 Wireshark 抓包,想自己设计或演进二进制/文本协议的开发者
- 建议周期:3~4 周(每周 6~8 小时,含 RFC 阅读、proto 编写、对比实现)
- 前置知识:
tcp-ip-stack;熟悉 TCP 三次握手、IP 报文头、字节(byte)/位(bit)的概念 - 最终目标:能画协议状态机、用 TLV/Protobuf 描述消息、为新字段加版本号和特性开关、解释 rolling upgrade 怎么做、给已有协议设计一次向后兼容的扩展
1. 学习路线
协议分层与角色
→ 报文边界与字节序
→ 定长字段、TLV、变长编码
→ 有限状态机 (FSM)
→ 文本协议 vs 二进制协议
→ 协议描述语言 (Protobuf / FlatBuffers / Cap'n Proto)
→ 版本号、特性开关、协商
→ 向后兼容与 rolling upgrade
→ 失败模式:粘包、半包、重放、降级
→ 设计评审:一份 RFC 的最小骨架
每一步先看一份真实协议(DNS、HTTP/1.1、Protocol Buffers、TLS ClientHello),再回到自己的设计。
2. 阶段周数分配
| 阶段 | 3 周方案 | 4 周方案 | 备注 |
|---|---|---|---|
| 1. 分层与字节序 | 0.5 周 | 0.5 周 | 看一份 DNS 报文,对照 network byte order |
| 2. 字段编码 | 0.5 周 | 0.75 周 | 定长、TLV、varint、length-prefixed |
| 3. 状态机 | 0.5 周 | 0.75 周 | TCP、HTTP/1.1、TLS 1.2 状态转移图 |
| 4. 文本 vs 二进制 | 0.25 周 | 0.25 周 | HTTP/1.1 vs TLS 1.3 record |
| 5. IDL 与序列化 | 0.5 周 | 0.5 周 | 写一份 .proto,用 protoc 生成 |
| 6. 版本与协商 | 0.25 周 | 0.5 周 | TLS version negotiation、HTTP/2 SETTINGS |
| 7. 向后兼容 | 0.25 周 | 0.5 周 | feature flag、未知字段丢弃 |
| 8. 失败模式 | 0.25 周 | 0.25 周 | 粘包、半包、重放、降级攻击 |
3. 九阶段表
| 阶段 | 核心知识 | 实践产出 | 可观察学会标准 |
|---|---|---|---|
| 1. 分层 | 端到端 vs 逐跳、协议头与负载、payload vs control | DNS 报文头逐字段对照 RFC 1035 | 能解释「为什么 TLS Record 不直接绑应用协议」 |
| 2. 字节序 | big-endian (network order) / little-endian、位域、字节对齐 | 一段 C 与一段 Go 分别读写 16/32 位整数 | 能解释网络包为何选 big-endian |
| 3. 字段编码 | 定长、TLV、Pascal 字符串、varint、zigzag | 写一个简单的 varint 编/解码器 | 能手算 varint 编码 300、16384 |
| 4. FSM | 状态、事件、转移、动作、终止态 | TCP 三次握手状态图、TLS 1.2 握手状态图 | 能从状态机角度解释 FIN_WAIT_2 |
| 5. 文本 vs 二进制 | 可读性、解析成本、扩展点、消息大小 | 对比 HTTP/1.1 报文与 Protobuf 编码字节 | 能列出至少 3 个文本协议的解析坑 |
| 6. IDL | .proto 语法、字段编号、wire type、packed、默认值 | 一份可编译的 .proto,跑 protoc --decode_raw | 能解释 field number 为何不能复用 |
| 7. 版本与协商 | 主版本、特性协商、版本回退、HTTP Upgrade、TLS supported_versions | 在自己的协议里加 version 字节并跑两个客户端 | 能解释 TLS 1.2/1.3 协商为何带 supported_versions |
| 8. 向后兼容 | 字段加号不复用、未知字段丢弃、默认值、reserved | 在已有协议里加一个字段并跑旧/新客户端 | 能区分「向后兼容」与「向前兼容」 |
| 9. 失败模式 | 粘包、半包、长度字段被攻击、版本不匹配、重放 | 一份 RFC 风格骨架 + 失败模式清单 | 能给出一份协议的最小不变量清单 |
4. 第一周任务
| 日 | 任务 | 当天交付 |
|---|---|---|
| Day 1 | 用 dig +short example.com 拿到一段 DNS 响应;用 tcpdump -ni any port 53 -X 抓包 | 一份带字段标注的 hexdump(ID、QR、QDCOUNT、ANCOUNT) |
| Day 2 | 用 C 写一个 htonl/ntohl demo,用 Go 写 binary.BigEndian demo,对比同一报文 | 两段代码与运行输出 |
| Day 3 | 手写 varint 编/解码,测试 1、127、128、300、16384 | 编码表 + 测试输出 |
| Day 4 | 画 TCP 连接状态机:从 CLOSED 到 ESTABLISHED,列出每个事件 | 一张状态转移图 + 关键事件清单 |
| Day 5 | 用 protoc --version 检查环境;写一个最小 .proto 并 protoc --decode_raw 观察 wire 格式 | .proto 源码 + decode 输出 |
| Day 6 | TLS 1.3 ClientHello 字段布局(按字节):legacy_version、random、session_id、cipher_suites、extensions | 一张字节布局图 |
| Day 7 | 项目:写一份迷你协议的 RFC 骨架(含消息定义、状态机、版本与失败模式) | 一份 RFC 风格 .md |
Day 7 拆解
步骤 A —— 最小可用(60 分钟)
- 选一个场景:「消息推送通道」或「设备影子同步」;
- 列出 3~5 种消息,每种用一行 TLV 描述;
- 画初始状态机(CLIENT_HELLO → SERVER_HELLO → OPEN → CLOSE);
- 用 Protobuf 写出
.proto; - 写一个 Go 或 Python 客户端,编解码一条消息成功收发。
步骤 B —— 补齐四类边界(45 分钟)
- 加一个版本号字段,演示两个版本协商;
- 加一个 feature flag,演示旧客户端忽略新字段;
- 列出三类失败模式(粘包、长度异常、版本不匹配);
- 把 RFC 骨架写到
notes/day7-rfc-skeleton.md。
5. 阶段通用验收
- 不看资料画出 TCP 与 TLS 1.2 握手状态机;
- 用 varint 手算 5 个数;
- 用
protoc --decode_raw解释一段二进制 Protobuf; - 给一个已有协议加一个新字段,并解释它如何向后兼容;
- 准备至少 3 个版本演进场景(旧→新、新→旧、滚动升级)并说明哪一端会失败;
- 记录 wire format 字节布局(含字节序与对齐);
- 能修改已有协议设计(加字段、删字段、改编码),而不是只能照抄。
6. 最终验收
- 独立画出 TCP、TLS 1.2、TLS 1.3 三份状态机;
- 独立实现一个 varint 编码器;
- 完成至少 6 个实验:DNS 报文解析、字节序对比、varint 编解码、Protobuf decode、TLS 1.3 ClientHello 字段布局、向自己协议加新字段;
- 完成 1 个综合项目:定义一份带版本号、feature flag 和向后兼容策略的小协议,并跑通两个版本的客户端互通;
- 用 15 分钟讲清「加字段 vs 改字段 vs 删字段」分别会破坏哪类兼容性。
7. 综合项目
首选:可演进的迷你 RPC 协议。用 Protobuf 定义 5~8 条消息(含 Request/Response/Heartbeat/VersionNegotiation/FeatureProbe)。实现一个服务端、一个老版本客户端、一个新版本客户端。演示三种场景:旧→新、新→旧、滚动升级期间混合。
备选:MQTT 子集实现 / HTTP/1.1 风格的文本协议 + TLV 扩展头 / 二进制长连接协议。
项目必做要求:
- 协议规范:一份 RFC 风格的
.md,含消息定义、字段编码、状态机、版本策略; - IDL:
.proto或等价描述; - 参考实现:服务端 + 两个版本客户端;
- 兼容性测试:新字段被旧客户端忽略、新客户端拒绝过老的版本号;
- 失败模式清单:粘包、长度字段被攻击、版本降级、重复消息;
- 抓包证据:用 Wireshark 或 tcpdump 抓一次握手与一次正常消息;
- README:运行步骤、目录结构、复盘;
- 复盘:哪些设计在第 2 天就被自己推翻。
交付物放到项目仓库的 notes/:design.md、test.md、retrospective.md。
本主题贡献
本主题沉淀协议设计的工程骨架:状态机 FSM、TLV 与 varint 编码、字节序、Protobuf 字段编号与 rolling upgrade,所有设计都有 RFC 风格 .md + 参考实现。
职责(3 项)
- 画 TCP 三次握手状态机、TLS 1.2 握手状态机、TLS 1.3 1-RTT 握手状态机,能从 FSM 角度解释 FIN_WAIT_2、CLOSE_WAIT、TIME_WAIT 的合法性边界与攻击面;
- 区分定长字段、TLV(type/length/value)、varint(Protobuf 7-bit 编码 + zigzag)、length-prefixed,能解释 wire type 选择对演进的影响,能手算 varint(300)、varint(16384);
- 用
.proto+ field number +reserved字段约束演进,能解释为什么「加字段 + 默认值 = 向后兼容」「删除用 reserved 保留编号」「feature flag 比主版本号更适合长生命周期协议」。
交付物(4 项)
- 一份迷你协议的 RFC 风格
.md(消息定义 + 状态机 + 版本策略 + 失败模式清单 + 不变量); - Protobuf
.proto定义(5~8 条消息,含 Request/Response/Heartbeat/VersionNegotiation/FeatureProbe)+protoc --decode_raw输出对照; - TLS 1.3 ClientHello 字节布局图(legacy_version/random/session_id/cipher_suites/extensions)+
openssl s_client -msg抓包标注; - 两个版本客户端互通(演示三种场景:旧→新、新→旧、滚动升级期间混合),含 wire format 抓包证据。
指标(3 项)
- varint 编码效率:小整数(< 128)占用 1 字节,相对定长 4 字节节省 75%;
- 字段编号冲突率 = 0(CI 检查
reserved字段 + 已删除字段不可重用); - 滚动升级窗口期破坏性变更 = 0(feature flag 默认关闭,逐租户灰度打开)。
8. 推荐开源资料
| 角色 | 资料 | 链接 | 用法 |
|---|---|---|---|
| 教科书 | 《TCP/IP 详解 卷 1:协议》Richard Stevens | https://www.pearson.com/us/higher-education/program/Stevens-TCP-IP-Illustrated-Volume-1-2nd-Edition/PGM318713.html | 协议设计的源头读物,重点看第 2、4、17 章 |
| 教科书 | 《UNIX 网络编程 卷 1》Stevens | https://www.pearson.com/us/higher-education/program/Stevens-UNIX-Network-Programming-Volume-1-3rd-Edition/PGM244697.html | 字节序、socket、IO 模型的工程参考 |
| 协议规范 | Protocol Buffers Encoding | https://protobuf.dev/programming-guides/encoding/ | wire type 与 varint 的权威描述 |
| 协议规范 | RFC 793 — TCP | https://www.rfc-editor.org/rfc/rfc793 | TCP 状态机原文 |
| 协议规范 | RFC 8446 §4.1.2 — TLS 1.3 ClientHello | https://www.rfc-editor.org/rfc/rfc8446#section-4.1.2 | 二进制握手消息的字段布局 |
| 协议规范 | RFC 9000 §17 — QUIC 帧 | https://www.rfc-editor.org/rfc/rfc9000#section-17 | 变长编码与帧格式 |
| 实践 | Protobuf Style Guide | https://protobuf.dev/programming-guides/style/ | 字段编号、命名、兼容性的官方建议 |
| 实践 | gRPC Versioning | https://github.com/grpc/grpc/blob/master/doc/versioning.md | 跨语言兼容性的真实教训 |
复制
.proto或协议片段前看对应 LICENSE:Protobuf 走 BSD-3-Clause,gRPC 走 Apache-2.0,RFC 文本属公共领域但不可冒充 IETF 文档发布。
9. 学习资料汇聚(v0.3 自包含)
本节由本计划生成。资料按用途筛选。协议行为以 RFC 和实现文档为准。
9.1 背景与动机
互联网是「在不可靠网络上构造可靠协议」的工程结果。早期的 NCP 没有分层;TCP/IP 把网络分成链路、网际、传输、应用四层,让每一层只关心自己的语义。这套分层思想直接来自协议设计本身——每一层都要给下一层一组「能做什么、不能做什么」的承诺。
协议设计的本质是约束:字节序定死、字段长度定死、状态转移定死。新字段怎么加、旧字段怎么删、版本怎么谈,都是在「已有实现已经部署」的前提下做手术。我们学它,是因为写 RPC、写 SDK、写配置文件、写数据库协议、写游戏通信,都会撞到同一组问题:粘包、半包、字段被攻击者构造、版本错配。
一句话总结:协议设计是「让两个互不信任的程序,在不可靠链路上达成一致语义」的工程。
9.2 概念地图
flowchart LR
分层[协议分层] --> 头[报文头]
分层 --> 负载[负载 / payload]
字节序[字节序] --> 头
编码[字段编码] --> 定长[定长字段]
编码 --> TLV
编码 --> 变长[varint / length-prefixed]
TLV --> 类型[类型字段]
TLV --> 长度[长度字段]
TLV --> 值[值字段]
编码 --> IDL[IDL: Protobuf / FlatBuffers]
FSM[有限状态机] --> 状态[状态]
FSM --> 事件[事件]
FSM --> 动作[动作]
FSM --> 转移[转移]
版本[版本与协商] --> 主版本[主版本]
版本 --> 特性[特性开关 / feature flag]
版本 --> 协商[版本协商]
兼容[向后兼容] --> 加字段[加字段]
兼容 --> 保留[reserved]
兼容 --> 默认值[默认值]
兼容 --> 丢弃[未知字段丢弃]
失败[失败模式] --> 粘包[粘包 / 半包]
失败 --> 长度攻击[长度字段攻击]
失败 --> 重放[重放]
失败 --> 降级[降级]
关系说明:
- 分层决定报文头与负载的边界;字节序影响所有多字节字段。
- 字段编码是「如何把语义塞进字节」:定长最快,TLV 最常见,变长编码最省空间。
- FSM 给协议「活」的部分:每个事件对应一个或多个状态转移,状态决定下一条消息的合法性。
- 版本与协商、向后兼容是协议演进的能力,决定新老实现能否共处。
- 失败模式提醒我们:长度字段是攻击面,重放需要 nonce 或 sequence,降级需要版本最低保障。
9.3 基础知识讲解
经典论文 / RFC
| 资料 | 角色 | 评分 | 链接 |
|---|---|---|---|
| RFC 793 — Transmission Control Protocol | TCP 状态机与报文头原文 | 5 | https://www.rfc-editor.org/rfc/rfc793 |
| RFC 791 — Internet Protocol | IP 报文头与分片原文 | 4 | https://www.rfc-editor.org/rfc/rfc791 |
| RFC 8446 §4.1.2 — TLS 1.3 ClientHello | 二进制扩展消息的布局示范 | 5 | https://www.rfc-editor.org/rfc/rfc8446#section-4.1.2 |
| RFC 9000 §17 — QUIC Frames | 变长帧格式与 type 编码 | 4 | https://www.rfc-editor.org/rfc/rfc9000#section-17 |
| RFC 1035 §4.1.1 — DNS Header | 经典定长头 | 4 | https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1 |
| Protocol Buffers Encoding | varint、wire type、字段编号的权威描述 | 5 | https://protobuf.dev/programming-guides/encoding/ |
经典书籍
| 资料 | 角色 | 评分 | 链接 |
|---|---|---|---|
| 《TCP/IP 详解 卷 1》Richard Stevens | 协议设计的源头读物 | 5 | https://www.pearson.com/us/higher-education/program/Stevens-TCP-IP-Illustrated-Volume-1-2nd-Edition/PGM318713.html |
| 《UNIX 网络编程 卷 1》Stevens | 字节序、socket、IO 模型 | 5 | https://www.pearson.com/us/higher-education/program/Stevens-UNIX-Network-Programming-Volume-1-3rd-Edition/PGM244697.html |
| 《Designing Data-Intensive Applications》Martin Kleppmann | 跨系统数据格式演进 | 5 | https://dataintensive.net/ |
| 《Network Algorithmics》George Varghese | 协议到算法的实现层 | 4 | https://www.elsevier.com/books/network-algorithmics/varghese/978-0-12-088477-3 |
| 《Protocol》Lieva, Liscano 等编 | 协议工程综述 | 3 | https://www.springer.com/gp/book/9780387365903 |
优秀博客
| 资料 | 角色 | 评分 | 链接 |
|---|---|---|---|
| The Illustrated TLS 1.3 Connection | TLS 1.3 字节级图解 | 5 | https://tls13.xargs.org/ |
| Julia Evans — bitwise | 字节序、位运算、二进制小图 | 5 | https://wizardzines.com/comics/ |
| Protobuf Style Guide | 字段编号与兼容性的官方建议 | 5 | https://protobuf.dev/programming-guides/style/ |
| gRPC Versioning | 跨语言兼容性的真实教训 | 4 | https://github.com/grpc/grpc/blob/master/doc/versioning.md |
| Cloudflare — Binary protocol design | 二进制协议实战教训 | 4 | https://blog.cloudflare.com/binary-protocol-design/ |
| Cloudflare — Introducing QUIC | QUIC 的设计动机 | 4 | https://blog.cloudflare.com/ |
核心人物
| 人物 | 贡献 | 关键出处 |
|---|---|---|
| Jon Postel | RFC 编辑、TCP/IP 关键作者,「Postel 定律」提出者 | RFC 793、RFC 791 |
| Vint Cerf | TCP 共同设计者 | RFC 793 |
| Bob Kahn | TCP 共同设计者 | RFC 793 |
| Richard Stevens | 《TCP/IP 详解》《UNIX 网络编程》 | 系列教材 |
| Kenton Varda | Protobuf、Cap’n Proto 作者 | https://capnproto.org/ |
| Martin Kleppmann | 《DDIA》、跨系统数据格式演进 | https://dataintensive.net/ |
开发方法
| 方法 | 适用 | 关键点 |
|---|---|---|
| 先画状态机再写消息 | 新协议设计 | 状态不变量比字段更稳定 |
| 先定 IDL 再写 wire format | 跨语言 RPC | .proto 当契约,wire 是实现 |
| 字段编号永不重用 | Protobuf 演进 | 删除用 reserved,避免歧义 |
| 未知字段必须丢弃 | 向后兼容 | 解析器见到不认识字段就跳过 |
| 版本协商要带最低保障 | TLS / HTTP Upgrade | 客户端声明能接受的最低版本 |
| 长度字段要校验 | 任意二进制协议 | 拒绝超过缓冲区上限的长度 |
| feature flag 而非主版本号 | 长生命周期协议 | 灰度期间混合版本最常见 |
| 用抓包验证 | 协议上线前 | 实际 wire 字节与 RFC 描述常不一致 |
9.4 经典问题与经典案例
| # | 问题 | 为什么重要 | 最简答案 |
|---|---|---|---|
| 1 | 字节序差异导致跨架构乱码 | x86 小端、网络协议大端 | 协议字段一律用 big-endian,主机字节序转网络字节序 |
| 2 | varint 编码为何节省空间 | 小整数最常见 | 7 位一组,最高位置 1 表示续位 |
| 3 | Protobuf 字段编号为何不能复用 | 编号 = 字段身份 | 删除字段用 reserved number 保留编号 |
| 4 | TLS ClientHello 为何带 supported_versions | 老协议位被占 | 用 extension 重写版本号字段 |
| 5 | 文本协议为何易出解析 bug | 多种空白、引号、转义 | 严格按 RFC 7230 §2 / ABNF 测试 |
| 6 | 状态机里「非法转移」怎么处理 | 攻击者构造奇怪包 | 收到非法事件立即关闭连接并记录 |
| 7 | 长度字段被攻击者放大 | 接收方分配巨大缓冲 | 先读长度,校验上限再读 payload |
| 8 | 向后兼容 vs 向前兼容 | 改了字段老客户端会断 | 加字段 + 默认值 = 向后兼容;删字段 = 破坏向前兼容 |
| 9 | rolling upgrade 如何做 | 服务不能停 | feature flag + 灰度,新旧版本同时运行 |
| 10 | Protocol Buffers 默认值是什么 | 序列化会省略默认值 | bool false、int 0、string "",proto3 无字段存在性概念 |
9.5 学习难点
概念难点
| 难点 | 为什么会卡 | 突破路径 |
|---|---|---|
| 字节序混淆 | 主机序、网络序、文件格式序混在一起 | 用 0x0102 在三种序下看字节顺序 |
| wire type 与字段编号 | Protobuf 文档分开讲,容易记混 | 用 protoc --decode_raw 看真实 wire |
| TLV 的「类型」含义 | 不只是数据类型,还是消息类型 | 区分「消息类型」与「字段类型」 |
| FSM 状态爆炸 | 真实协议状态多到画不下 | 先画主线,异常态放脚注或另一张图 |
| 兼容性方向 | 向后/向前/双向常被混用 | 写一行说明「谁升级、谁不动」 |
思维难点
| 难点 | 为什么会卡 | 突破路径 |
|---|---|---|
| 协议不变量的边界 | 不变量太多难写 | 列 3 条「破一条就拆协议」的核心不变量 |
| 重放攻击怎么挡 | 协议层不防 | nonce、sequence、时间窗、签名任选其一 |
| 降级攻击怎么挡 | 老算法还在 | TLS 1.3 删除老算法,HTTP/2 强制 h2c 限制 |
| 设计期 vs 演进期 | 一开始总想一步到位 | 先定 wire 与状态机,版本与 feature flag 后加 |
| 「兼容」来自默认行为 | 解析器决定兼容 | 改解析器比改文档影响大 |
工程难点
| 难点 | 为什么会卡 | 突破路径 |
|---|---|---|
| 跨语言实现一致性 | 字段顺序、padding、bool 长度都不同 | 用 IDL 生成代码,不手写 |
| 滚动升级期间消息分裂 | 新旧版本同时在线 | 把大改动包成 feature flag,按灰度推送 |
| 抓包验证耗时 | 协议上线后才看出错 | 设计阶段就建一个 reference client + server |
| 长度字段内存放大 | 1 GB payload 申请 | 流式读取,边读边校验 |
| IDL 锁定 | 改 .proto 牵动所有 SDK | 把 wire format 与 IDL 解耦,IDL 只描述 |
9.6 技术标准与接口
9.6.1 Entity
| 名称 | 版本 | 发布组织 | 状态 | 许可证 / 可访问性 |
|---|---|---|---|---|
| TCP | RFC 793 / 9293 | IETF TCPM | 现行标准 | 免费公开 |
| IP | RFC 791 / 8200 | IETF | 现行标准 | 免费公开 |
| DNS | RFC 1035(结合 6891 等 EDNS) | IETF DNSOP | 现行标准 | 免费公开 |
| TLS | 1.3 / RFC 8446 | IETF TLS WG | 现行标准 | 免费公开 |
| QUIC | RFC 9000 | IETF QUIC WG | 现行标准 | 免费公开 |
| Protocol Buffers | proto3 / Edition 2024 | Google / CNCF | 活跃实现 | BSD-3-Clause |
| Cap’n Proto | 持续更新 | 个人项目 | 活跃实现 | MIT |
| FlatBuffers | 持续更新 | Meta | 活跃实现 | Apache-2.0 |
| Apache Avro | 1.11+ | Apache Software Foundation | 活跃实现 | Apache-2.0 |
9.6.2 Scope
- TCP / IP / DNS / TLS / QUIC:传输与网络层协议,本计划把它们当作「真实世界里的设计范例」来读,不是要重新实现。
- IDL(Protobuf / Cap’n Proto / Avro / FlatBuffers):跨语言序列化与协议描述工具。它们解决「同一份数据在不同语言、不同版本下的兼容」。
- 向后兼容策略:包括字段编号保留、未知字段丢弃、默认值、feature flag。这些不是协议,但任何长期协议都要做。
- 不适用:纯本地内存数据结构(用语言原生 struct 即可)、单进程配置(用 YAML/TOML 即可)、一次性导出格式(CSV/Parquet 已够)。
9.6.3 Structure
协议设计者必须掌握的字段布局与编码概念:
// Protobuf 风格的 .proto:演示 IDL + wire type
syntax = "proto3";
message Header {
uint32 magic = 1; // 定长,4 字节 big-endian
uint32 version = 2; // 版本号,永远保留位置
repeated string features = 3; // feature flag 列表
reserved 10 to 19; // 旧字段保留编号
reserved "old_name"; // 旧字段名保留
}
message Envelope {
Header header = 1;
bytes payload = 2; // 长度前缀 + 字节
uint32 crc32 = 3; // 校验
}
// TLS 1.3 ClientHello 字节布局(简化版)
uint16 legacy_version = 0x0303 // TLS 1.2 兼容位
bytes[32] random // 32 字节 client random
opaque session_id<0..32> // 长度前缀 + 字节
opaque cipher_suites<2..2^16-2> // 长度前缀 + 2 字节套件
opaque compression_methods<1..255> // 长度前缀 + 1 字节方法
// extensions 整段走 TLV:type(2) + length(2) + value
opaque extensions<8..2^16-1>
必须掌握的字段、概念与 API:
- 字节序:
htonl/ntohl(C)、encoding/binary(Go)、struct.pack('!I', x)(Python); - varint:Protobuf 的 7-bit 编码与 zigzag;
- TLV:type / length / value,常见于 DNS、ASN.1、TLV 风格的二进制协议;
- FSM:state、event、transition、action、guard;
- Protobuf:
protoc、--decode_raw、--encode_raw、field number、reserved; - TLS:
supported_versions、key_share、signature_algorithms; - HTTP:
Upgrade、ALPN、SETTINGS(HTTP/2 帧 0x4)。
9.6.4 Ecosystem
- 序列化与 IDL:Protobuf(跨语言主流)、Cap’n Proto(零拷贝)、FlatBuffers(嵌入式/游戏)、Avro(数据管道)、Thrift(RPC 历史选项);
- 网络库:gRPC、Twirp、Finagle、Netty、Envoy、Tokio(Rust);
- 协议设计工具:Wireshark、tshark、
protoc、capnp compile、flatc; - 服务网格:Envoy、Linkerd、Istio。它们把协议策略(重试、超时、负载均衡)抽到 sidecar,让应用层协议更聚焦语义;
- 事实标准 vs 标准本身:
Upgrade: h2c是 HTTP/1.1 字段,但 HTTP/2 客户端必须支持——这是事实标准;Protobuf 字段编号语义稳定,但 proto3 与 Editions 在默认值上不同——演进要选对版本。
9.6.5 Depth Tiers
| 层级 | 名称 | 必须看到什么 |
|---|---|---|
| L0 | 知道存在 | 知道协议分层、字节序、TLV、状态机、版本协商这些词 |
| L1 | 看得懂示例 | 能读懂一份 .proto、一段 ClientHello 字节布局、一份 TCP 状态图 |
| L2 | 能正确调用 | 能用 protoc 编解码、用 tcpdump 解析报文、画一份小型协议的状态机 |
| L3 | 能解释与排错 | 能解释 varint 编码、Protobuf 字段编号为何不能复用、rolling upgrade 期间为何要加 feature flag |
| L4 | 能设计与扩展 | 能从零定义一份带版本号、向后兼容策略与失败模式清单的协议,并写出参考实现 |
本计划要求达到:L3。需要维护长生命周期 RPC 或跨语言 SDK 时再进入 L4。
9.6.6 Source
- TCP:RFC 793(https://www.rfc-editor.org/rfc/rfc793);更新见 RFC 9293。
- TLS 1.3:RFC 8446,https://www.rfc-editor.org/rfc/rfc8446。
- QUIC 帧:RFC 9000 §17,https://www.rfc-editor.org/rfc/rfc9000#section-17。
- DNS 头:RFC 1035 §4.1.1,https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1。
- Protobuf Encoding:https://protobuf.dev/programming-guides/encoding/
- Protobuf Style:https://protobuf.dev/programming-guides/style/
- gRPC Versioning:https://github.com/grpc/grpc/blob/master/doc/versioning.md
- 引用版本快照日期:2026-07-30。
10. 常见误区
- 把「能用就行」当成协议设计。半年后会出现版本错配;
- 只画消息格式不画状态机。合法消息仍可能造成非法语义;
- 在 Protobuf 里改字段名当兼容。序列化是按编号,名字是注释;
- 复用字段编号删字段。会让老消息被错解;
- 不校验长度字段。攻击者发一个
length=0xFFFFFFFF直接 OOM; - 用文本协议只因为「好调试」。生产抓包要的是 wire format;
- 把 feature flag 做成主版本号。每次大改都升级主版本会逼所有人同步;
- 忽略字节序。把内存里的 struct 直接
write()出去,跨架构全乱; - 状态机不写不变量。代码 review 时没人知道哪些转移合法;
- rolling upgrade 期间上线破坏性变更。先发 feature flag,再开默认;
- 协议里有「必须按顺序读」的隐式约束。文档不写,新人不会知道;
- 把加密和身份认证混淆。协议层不解决机密性,靠 TLS 或应用层加密;
- 把版本号当字符串比较。「1.10」小于「1.9」会让升级失败。
11. 所有知识点分类(统一规则)
- 编程语言
- 数据结构与算法
- 计算机基础:协议分层、字节序、状态机、版本演进
- 工程技术:IDL、序列化库、跨语言兼容、滚动升级
- Web 与后端:HTTP、gRPC、QUIC、负载均衡
- 前端与客户端
- 数据与人工智能
- 项目与职业能力:协议评审、RFC 写作、抓包验证
- 安全与可靠性:长度校验、版本降级、重放保护
本计划归属:计算机基础 主 + 工程技术 辅。