CalcGuide · 技术博客主页 / 一页纸学习计划
🟡

协议设计原理:从状态机到向后兼容

分类:计算机基础 · 路径:docs/topics/protocol-design/README.md

#protocol#state-machine#versioning#compatibility

理解协议分层、有限状态机、字节序、变长字段、版本演进与向后兼容

父主题

计算机网络:从物理层到 HTTP/3 的全栈协议与排查

子主题(0)

协议设计原理:从状态机到向后兼容

0. 元信息

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 controlDNS 报文头逐字段对照 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 1dig +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 连接状态机:从 CLOSEDESTABLISHED,列出每个事件一张状态转移图 + 关键事件清单
Day 5protoc --version 检查环境;写一个最小 .protoprotoc --decode_raw 观察 wire 格式.proto 源码 + decode 输出
Day 6TLS 1.3 ClientHello 字段布局(按字节):legacy_version、random、session_id、cipher_suites、extensions一张字节布局图
Day 7项目:写一份迷你协议的 RFC 骨架(含消息定义、状态机、版本与失败模式)一份 RFC 风格 .md

Day 7 拆解

步骤 A —— 最小可用(60 分钟)

  1. 选一个场景:「消息推送通道」或「设备影子同步」;
  2. 列出 3~5 种消息,每种用一行 TLV 描述;
  3. 画初始状态机(CLIENT_HELLO → SERVER_HELLO → OPEN → CLOSE);
  4. 用 Protobuf 写出 .proto
  5. 写一个 Go 或 Python 客户端,编解码一条消息成功收发。

步骤 B —— 补齐四类边界(45 分钟)

5. 阶段通用验收

  1. 不看资料画出 TCP 与 TLS 1.2 握手状态机;
  2. 用 varint 手算 5 个数;
  3. protoc --decode_raw 解释一段二进制 Protobuf;
  4. 给一个已有协议加一个新字段,并解释它如何向后兼容;
  5. 准备至少 3 个版本演进场景(旧→新、新→旧、滚动升级)并说明哪一端会失败;
  6. 记录 wire format 字节布局(含字节序与对齐);
  7. 能修改已有协议设计(加字段、删字段、改编码),而不是只能照抄。

6. 最终验收

7. 综合项目

首选:可演进的迷你 RPC 协议。用 Protobuf 定义 5~8 条消息(含 Request/Response/Heartbeat/VersionNegotiation/FeatureProbe)。实现一个服务端、一个老版本客户端、一个新版本客户端。演示三种场景:旧→新、新→旧、滚动升级期间混合。

备选:MQTT 子集实现 / HTTP/1.1 风格的文本协议 + TLV 扩展头 / 二进制长连接协议。

项目必做要求:

  1. 协议规范:一份 RFC 风格的 .md,含消息定义、字段编码、状态机、版本策略;
  2. IDL.proto 或等价描述;
  3. 参考实现:服务端 + 两个版本客户端;
  4. 兼容性测试:新字段被旧客户端忽略、新客户端拒绝过老的版本号;
  5. 失败模式清单:粘包、长度字段被攻击、版本降级、重复消息;
  6. 抓包证据:用 Wireshark 或 tcpdump 抓一次握手与一次正常消息;
  7. README:运行步骤、目录结构、复盘;
  8. 复盘:哪些设计在第 2 天就被自己推翻。

交付物放到项目仓库的 notes/design.mdtest.mdretrospective.md

本主题贡献

本主题沉淀协议设计的工程骨架:状态机 FSM、TLV 与 varint 编码、字节序、Protobuf 字段编号与 rolling upgrade,所有设计都有 RFC 风格 .md + 参考实现。

职责(3 项)

  1. 画 TCP 三次握手状态机、TLS 1.2 握手状态机、TLS 1.3 1-RTT 握手状态机,能从 FSM 角度解释 FIN_WAIT_2、CLOSE_WAIT、TIME_WAIT 的合法性边界与攻击面;
  2. 区分定长字段、TLV(type/length/value)、varint(Protobuf 7-bit 编码 + zigzag)、length-prefixed,能解释 wire type 选择对演进的影响,能手算 varint(300)、varint(16384);
  3. .proto + field number + reserved 字段约束演进,能解释为什么「加字段 + 默认值 = 向后兼容」「删除用 reserved 保留编号」「feature flag 比主版本号更适合长生命周期协议」。

交付物(4 项)

  1. 一份迷你协议的 RFC 风格 .md(消息定义 + 状态机 + 版本策略 + 失败模式清单 + 不变量);
  2. Protobuf .proto 定义(5~8 条消息,含 Request/Response/Heartbeat/VersionNegotiation/FeatureProbe)+ protoc --decode_raw 输出对照;
  3. TLS 1.3 ClientHello 字节布局图(legacy_version/random/session_id/cipher_suites/extensions)+ openssl s_client -msg 抓包标注;
  4. 两个版本客户端互通(演示三种场景:旧→新、新→旧、滚动升级期间混合),含 wire format 抓包证据。

指标(3 项)

  1. varint 编码效率:小整数(< 128)占用 1 字节,相对定长 4 字节节省 75%;
  2. 字段编号冲突率 = 0(CI 检查 reserved 字段 + 已删除字段不可重用);
  3. 滚动升级窗口期破坏性变更 = 0(feature flag 默认关闭,逐租户灰度打开)。

8. 推荐开源资料

角色资料链接用法
教科书《TCP/IP 详解 卷 1:协议》Richard Stevenshttps://www.pearson.com/us/higher-education/program/Stevens-TCP-IP-Illustrated-Volume-1-2nd-Edition/PGM318713.html协议设计的源头读物,重点看第 2、4、17 章
教科书《UNIX 网络编程 卷 1》Stevenshttps://www.pearson.com/us/higher-education/program/Stevens-UNIX-Network-Programming-Volume-1-3rd-Edition/PGM244697.html字节序、socket、IO 模型的工程参考
协议规范Protocol Buffers Encodinghttps://protobuf.dev/programming-guides/encoding/wire type 与 varint 的权威描述
协议规范RFC 793 — TCPhttps://www.rfc-editor.org/rfc/rfc793TCP 状态机原文
协议规范RFC 8446 §4.1.2 — TLS 1.3 ClientHellohttps://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 Guidehttps://protobuf.dev/programming-guides/style/字段编号、命名、兼容性的官方建议
实践gRPC Versioninghttps://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]
  兼容 --> 默认值[默认值]
  兼容 --> 丢弃[未知字段丢弃]
  失败[失败模式] --> 粘包[粘包 / 半包]
  失败 --> 长度攻击[长度字段攻击]
  失败 --> 重放[重放]
  失败 --> 降级[降级]

关系说明:

9.3 基础知识讲解

经典论文 / RFC

资料角色评分链接
RFC 793 — Transmission Control ProtocolTCP 状态机与报文头原文5https://www.rfc-editor.org/rfc/rfc793
RFC 791 — Internet ProtocolIP 报文头与分片原文4https://www.rfc-editor.org/rfc/rfc791
RFC 8446 §4.1.2 — TLS 1.3 ClientHello二进制扩展消息的布局示范5https://www.rfc-editor.org/rfc/rfc8446#section-4.1.2
RFC 9000 §17 — QUIC Frames变长帧格式与 type 编码4https://www.rfc-editor.org/rfc/rfc9000#section-17
RFC 1035 §4.1.1 — DNS Header经典定长头4https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1
Protocol Buffers Encodingvarint、wire type、字段编号的权威描述5https://protobuf.dev/programming-guides/encoding/

经典书籍

资料角色评分链接
《TCP/IP 详解 卷 1》Richard Stevens协议设计的源头读物5https://www.pearson.com/us/higher-education/program/Stevens-TCP-IP-Illustrated-Volume-1-2nd-Edition/PGM318713.html
《UNIX 网络编程 卷 1》Stevens字节序、socket、IO 模型5https://www.pearson.com/us/higher-education/program/Stevens-UNIX-Network-Programming-Volume-1-3rd-Edition/PGM244697.html
《Designing Data-Intensive Applications》Martin Kleppmann跨系统数据格式演进5https://dataintensive.net/
《Network Algorithmics》George Varghese协议到算法的实现层4https://www.elsevier.com/books/network-algorithmics/varghese/978-0-12-088477-3
《Protocol》Lieva, Liscano 等编协议工程综述3https://www.springer.com/gp/book/9780387365903

优秀博客

资料角色评分链接
The Illustrated TLS 1.3 ConnectionTLS 1.3 字节级图解5https://tls13.xargs.org/
Julia Evans — bitwise字节序、位运算、二进制小图5https://wizardzines.com/comics/
Protobuf Style Guide字段编号与兼容性的官方建议5https://protobuf.dev/programming-guides/style/
gRPC Versioning跨语言兼容性的真实教训4https://github.com/grpc/grpc/blob/master/doc/versioning.md
Cloudflare — Binary protocol design二进制协议实战教训4https://blog.cloudflare.com/binary-protocol-design/
Cloudflare — Introducing QUICQUIC 的设计动机4https://blog.cloudflare.com/

核心人物

人物贡献关键出处
Jon PostelRFC 编辑、TCP/IP 关键作者,「Postel 定律」提出者RFC 793、RFC 791
Vint CerfTCP 共同设计者RFC 793
Bob KahnTCP 共同设计者RFC 793
Richard Stevens《TCP/IP 详解》《UNIX 网络编程》系列教材
Kenton VardaProtobuf、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,主机字节序转网络字节序
2varint 编码为何节省空间小整数最常见7 位一组,最高位置 1 表示续位
3Protobuf 字段编号为何不能复用编号 = 字段身份删除字段用 reserved number 保留编号
4TLS ClientHello 为何带 supported_versions老协议位被占用 extension 重写版本号字段
5文本协议为何易出解析 bug多种空白、引号、转义严格按 RFC 7230 §2 / ABNF 测试
6状态机里「非法转移」怎么处理攻击者构造奇怪包收到非法事件立即关闭连接并记录
7长度字段被攻击者放大接收方分配巨大缓冲先读长度,校验上限再读 payload
8向后兼容 vs 向前兼容改了字段老客户端会断加字段 + 默认值 = 向后兼容;删字段 = 破坏向前兼容
9rolling upgrade 如何做服务不能停feature flag + 灰度,新旧版本同时运行
10Protocol 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

名称版本发布组织状态许可证 / 可访问性
TCPRFC 793 / 9293IETF TCPM现行标准免费公开
IPRFC 791 / 8200IETF现行标准免费公开
DNSRFC 1035(结合 6891 等 EDNS)IETF DNSOP现行标准免费公开
TLS1.3 / RFC 8446IETF TLS WG现行标准免费公开
QUICRFC 9000IETF QUIC WG现行标准免费公开
Protocol Buffersproto3 / Edition 2024Google / CNCF活跃实现BSD-3-Clause
Cap’n Proto持续更新个人项目活跃实现MIT
FlatBuffers持续更新Meta活跃实现Apache-2.0
Apache Avro1.11+Apache Software Foundation活跃实现Apache-2.0

9.6.2 Scope

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:

9.6.4 Ecosystem

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

10. 常见误区

11. 所有知识点分类(统一规则)

  1. 编程语言
  2. 数据结构与算法
  3. 计算机基础:协议分层、字节序、状态机、版本演进
  4. 工程技术:IDL、序列化库、跨语言兼容、滚动升级
  5. Web 与后端:HTTP、gRPC、QUIC、负载均衡
  6. 前端与客户端
  7. 数据与人工智能
  8. 项目与职业能力:协议评审、RFC 写作、抓包验证
  9. 安全与可靠性:长度校验、版本降级、重放保护

本计划归属:计算机基础 主 + 工程技术 辅。


直接依赖(1)

查看知识图谱