第 16 章 亲手设计一个协议:从需求到字节
前面 15 章,你一直在”读”协议。现在,你要”写”一个。当你亲手设计时,那些学过的每个概念——边界、幂等、错误码、扩展点、不变量——会一个接一个跳出来逼你决策。
16.0 引子:一个听起来简单的需求
小美:老哥,公司要做智能硬件了。一万个传感器要往云端上报数据(温度、湿度、电量),还要能接收云端下发的指令(比如”重启”)。你设计一个通信协议吧。
你:这……用 HTTP 不就行了?传感器发 POST,云端下发指令用轮询。
老张:传感器是电池供电的,HTTP 头一大堆,太耗流量。而且传感器网络不稳定,经常断。还有,这些设备卖出去就更新不了固件了。
墨叔:看到没有?一个”简单”的需求,一旦加上真实约束(电池、弱网、不可升级),HTTP 就不合适了。今天,我们亲手设计一个协议。
16.1 先把问题问对:在写任何字节之前
墨叔:大多数新手设计协议,第一反应是”定报文格式”。这是错的起点,第 1 章我们就说过。
在写任何字节之前,先回答五个问题。这五个问题的答案,决定了你协议的一切。
问题一:约束是什么?
| 约束 | 我们的场景 | 影响 |
|---|---|---|
| 算力 | 传感器,MCU 级,弱 | 报文要简单、解析要轻 |
| 网络 | 弱网、易断、按流量计费 | 要省流量、要支持重连 |
| 电量 | 电池供电 | 少发、少握手 |
| 生命周期 | 卖出去不可升级 | 兼容性是生死线(第 15 章) |
| 中间设备 | 运营商网络、NAT | 要考虑 NAT 会断连接 |
你:所以设计协议的第一步,是”搞清楚约束”?
墨叔:对。协议没有”最优解”,只有”在给定约束下的最优解”。 你的约束是”弱算力、弱网、省电、不可升级”,所以你的协议要”轻、省、耐断、可演进”。换个约束(比如数据中心内部、千兆网),答案完全不同。
问题二:同步还是异步?
你:同步就是”请求-响应”,异步就是”发完不管”?
墨叔:对。我们的传感器是”上报数据”——它发完就完事,不需要等云端确认每一笔。这是异步为主。但”下发指令”又需要”云端知道设备收到了没”——这是同步。所以我们的协议要两者都支持。
问题三:文本还是二进制?
你:这题我会。传感器要省流量、要轻,用二进制。文本(HTTP/JSON)头太大。
墨叔:对,但要记住代价(第 2 章):二进制不可调试、有字节序问题。所以好的二进制协议,一定会配”调试工具”和”明确的字节序约定”。
问题四:自描述还是预定义 schema?
你:自描述就是”报文里带字段名”(像 JSON),预定义就是”字段名在 schema 里,报文字节里只有值”?
墨叔:对。传感器场景,我们预定义 schema(字段名写死在协议文档里,报文里只传值)——因为省流量,且设备的字段是固定的。自描述(JSON)适合”字段经常变”的场景,预定义适合”字段固定、要极致省流量”的场景。
问题五:怎么定边界?
墨叔:第 2 章讲过三种边界方案。我们选哪个?
你:二进制协议,用”长度前缀”最合适——每个报文字节开头先写”这个报文多长”。
墨叔:对。长度前缀是二进制协议的标准做法。而且要记住第 2 章的教训:长度是”承诺”,必须精确,不匹配就是协议错误。
16.2 第一步:定”不变量”(哪些东西 20 年不变)
墨叔:第 15 章说过,设计协议要先定”不变量”——那些”动不得的地基”。为什么这个要最先定?
你:因为不变量一旦定错,后面全错,而且改不动?
墨叔:对。我们的协议,不变量是什么?
墨叔:我提议这四条,你感受一下为什么要先定死它们:
- 报文 = 定长头 + 变长体:所有报文,开头都是固定长度的”头”,后面跟变长的”体”。这个结构 20 年不变。
- 头的第一个字段,永远是”版本号”:这样任何未来版本,都能先读版本号,决定怎么解析。
- 长度字段的位置固定:头的固定位置有”报文总长度”,边界判定永远靠它。
- 字节序 = 大端(Big-Endian):多字节数字一律大端,永不改变。
你:为什么”版本号放第一位”和”字节序定死”这么重要?
墨叔:因为它们是最底层、最基础、最不能改的东西:
- 版本号放第一位:让”未来版本”和”旧版本”能共存——接收方读第一个字节就知道”这是哪个版本,该怎么解析”。没有这个,未来无法演进。
- 字节序定死(大端):多字节数字(长度、版本)的解析方式一旦不一致,两边读到的数字就完全不同。这是”二进制协议”的第一号坑,必须第一天定死。
16.3 第二步:定报文骨架(字节布局)
墨叔:现在可以画字节了。我设计一个”头”:
字节 0 : 版本号(Version),1 字节,当前 = 0x01
字节 1 : 报文类型(Type),1 字节
字节 2-3 : Flags(标志位),2 字节,按位使用
字节 4-5 : 报文总长度(Length),2 字节,大端,含头
字节 6-7 : 请求 ID(Request ID),2 字节
字节 8... : 负载(Payload)
你:为什么要”请求 ID”?
墨叔:好问题,这是第 3 章和第 7 章埋下的东西。请求 ID 解决两件事:
- 乱序/并发:如果设备可以发多个未完成的请求,请求 ID 让响应能”对上号”(第 7 章、第 12 章的流 ID)。
- 去重/幂等:设备重发时带上相同的请求 ID,云端能识别”这是重发,别重复处理”(第 3 章的 Idempotency-Key)。
你:所以这个”8 字节的定长头”,就是我们的”地基”?
墨叔:对。这 8 字节,就是我们的”不变量”。 未来加任何功能,都不能动这 8 字节的结构(可以扩展,但不能破坏)。
16.4 第三步:定报文类型与错误模型
墨叔:定”报文类型”和”错误分类”。这对应第 3、4 章学的方法和状态码。
报文类型
Type 0x01 : 上报数据(DATA) —— 设备 → 云,异步,不需要响应
Type 0x02 : 下发指令(COMMAND) —— 云 → 设备
Type 0x03 : 确认(ACK) —— 对 COMMAND 的响应
Type 0x04 : 错误(ERROR) —— 出错时返回
错误模型:像状态码一样分类
墨叔:第 4 章说过,状态码分类的黄金标准是”能不能重试”。我们的错误码也这么分:
错误码 1 位(放在负载第一个字节):
0x00 : 成功
0x01 : 客户端错误(不可重试,设备的问题,比如格式错)
0x02 : 服务端错误(可重试,云端的问题,比如数据库挂了)
0x03 : 版本不支持(设备该升级,或云端回退兼容)
你:所以错误码第一位就是”能不能重试”,和 HTTP 的 4xx/5xx 一样?
墨叔:精确。你在第 4 章学的”错误分类学”,在这里直接复用。 这就是为什么前面要花那么多篇幅讲状态码——它不是 HTTP 特有的,是”协议设计”的通用智慧。
16.5 第四步:定幂等、重试与去重
墨叔:第 3 章的核心问题来了——这个协议怎么处理”重试”?
传感器网络不稳定,设备发完数据可能没收到 ACK,它会重发。如果云端不处理幂等,同一条数据会被重复上报两次。
你:用请求 ID 去重?
墨叔:对。设计:
- 设备发每条数据,带一个递增的”消息序号”(在负载里)。
- 云端记录”每个设备已经收到的最大序号”。
- 收到重复序号 → 丢弃(已处理过)。
- 收到跳号的 → 知道丢了,让设备补发。
你:这就像”消息队列的 at-least-once + 幂等消费”?
墨叔:对。记住这个模式的本质:
弱网协议(或任何不可靠传输)的可靠性,靠”序号 + 去重 + 补发”来保证。 发送方”至少发一次”(重试),接收方”幂等处理”(去重),就能做到”恰好一次”的业务效果。
这就是第 3 章”幂等”、第 7 章”幽灵连接”、第 14 章”WebSocket 断线补拉”这些问题的统一答案。
16.6 第五步:定版本与扩展机制(这是重头戏)
墨叔:现在到了最关键的一步——如何让这个协议能演进(第 15 章的核心)。
版本号:留给”不兼容的根本变革”
头第一个字节是版本号 0x01。未来如果要做”不兼容的大改”,就升 0x02。但版本号是重武器,不能频繁用(第 15 章)。
扩展点:日常演进靠它
墨叔:我们协议里有三个扩展点,每个都配了”优雅降级”:
扩展点一:Flags(标志位)。 头的第 2-3 字节是 16 个 bit。每个 bit 表示一个”可选特性”。接收方遇到不认识的 bit,就忽略它(降级 = 忽略)。这对应 HTTP 的”未知首部忽略”。
扩展点二:报文类型。 未来加新类型(比如 0x05 = 固件更新),老设备收到不认识的类型,返回”版本不支持”错误码,而不是崩溃。这对应 HTTP 的”未知方法返回 405/501”。
扩展点三:负载里的 TLV(Type-Length-Value)。 负载里用 TLV 结构来承载字段:
Type (1字节) | Length (1字节) | Value (Length 字节)
你:TLV 是什么?为什么用它?
墨叔:TLV 是一种经典的自描述扩展结构。它的精髓是:
接收方遇到不认识的 Type,可以根据 Length 跳过它,继续解析下一个 TLV。 这样,未来加新字段,老设备能”跳过”它而不是崩溃。
这对应 HTTP 的”未知首部忽略”,是二进制协议里最重要的扩展机制。
你:所以 Flags、类型、TLV 这三个扩展点,都配了”老设备优雅降级”?
墨叔:对。这正是第 15 章说的:“扩展点的精髓,在降级,不在扩展。” 每个扩展点,都要想清楚”老设备遇到它时会怎样”。
16.7 第六步:定安全(重放攻击是重点)
墨叔:第 11 章讲过”HTTPS 不防重放”。我们的传感器协议,同样面临重放攻击。
你:重放攻击?攻击者抓一个”上报”报文,原样重发?
墨叔:对。更危险的是”下发指令”的重放——攻击者抓一个”重启”指令,重复发给设备,让设备反复重启。
防御:三重机制,都是从第 11 章学来的:
- 加密:整个报文用对称密钥(如 AES)加密。中间人看不懂内容。
- 防篡改:报文带一个 HMAC 签名,防止被改。
- 防重放:报文带时间戳 + 随机数(nonce),接收方拒绝”时间戳过旧”和”nonce 已用过”的报文。
你:所以安全也要”分层”——加密、签名、防重放,各管一段?
墨叔:对。这又是第 11 章”四层安全模型”的落地。没有一层是万能的,要一层一层叠。
16.8 第七步:定握手与能力协商
墨叔:设备第一次连上云端,要做一个”握手”,做两件事:认证 + 能力协商。
设备 → 云:握手请求(设备 ID + 认证凭证 + 能力位图)
云 → 设备:握手响应(会话密钥 + 服务器能力位图 + 心跳间隔)
能力位图(Feature Bitmap):一个 bit 表示一个能力(比如”支持固件更新""支持压缩”)。双方交换能力位图,各自知道对方支持什么。
你:这对应第 12 章 ALPN、第 14 章 Upgrade 的”协商”思想?
墨叔:对。握手 = 建立连接 + 认证 + 协商能力。这是所有协议都要有的”开场”,只是实现各异。
16.9 完整协议一览 + 自我评审
墨叔:把上面所有决策汇总成一份”协议规范”:
【传感器上报协议 v1 规范摘要】
不变量(永不改变):
- 报文 = 8 字节定长头 + 变长负载
- 版本号在字节 0,字节序大端
- 边界靠头里的 Length 字段
报文头(8 字节,大端):
[0] Version [1] Type [2-3] Flags [4-5] Length [6-7] RequestID
报文类型:0x01 DATA / 0x02 COMMAND / 0x03 ACK / 0x04 ERROR
错误码(负载首字节):0x00 成功 / 0x01 客户端错(不重试)/ 0x02 服务端错(可重试)/ 0x03 版本不支持
可靠性:消息序号 + 云端去重 + 补发
扩展:Flags 位图 / 新类型 / TLV 字段(都配优雅降级)
安全:AES 加密 + HMAC 签名 + 时间戳/nonce 防重放
握手:设备ID + 凭证 + 能力位图 交换
墨叔:现在,用第 15 章的 10 条原则,反过来批判自己的设计:
| 原则 | 我们的设计做到了吗? |
|---|---|
| 版本号重武器 | ✅ 版本号只留不兼容变革 |
| 扩展点比功能重要 | ✅ 三个扩展点 |
| 降级比扩展重要 | ✅ 每个扩展点都配降级 |
| 不变量是地基 | ✅ 8 字节头定死 |
| 宽容分阶段 | ⚠️ 没明确,可补充 |
| 实现不一致是万恶之源 | ⚠️ 边界、字节序定死了吗?需强调 |
| 扩展点保持活性 | ⚠️ 没做 GREASE 类似机制 |
| 让中间设备透明 | ⚠️ 加密了,天然透明,✅ |
| 语义语法解耦 | ✅ 语义(上报/指令)和语法(字节)分离 |
| 能落地大于正确 | ✅ 简单、可调试 |
你:还有三个 ⚠️,说明设计还不够完善?
墨叔:对。设计协议,永远没有”完成”,只有”迭代”。 这三个 ⚠️ 就是下一次迭代的方向。一个真正的高手,不是设计出”完美的协议”,而是设计出”知道自己哪里还不完美”的协议。
16.10 Go 落地:一个编解码器骨架
墨叔:用 Go 写一个简化的编解码器,把协议落到代码:
type Header struct {
Version byte
Type byte
Flags uint16
Length uint16
RequestID uint16
}
// 编码:用大端序写入
func (h Header) Marshal() []byte {
buf := make([]byte, 8)
buf[0] = h.Version
buf[1] = h.Type
binary.BigEndian.PutUint16(buf[2:4], h.Flags)
binary.BigEndian.PutUint16(buf[4:6], h.Length)
binary.BigEndian.PutUint16(buf[6:8], h.RequestID)
return buf
}
// 解码
func UnmarshalHeader(b []byte) (Header, error) {
if len(b) < 8 {
return Header{}, fmt.Errorf("报文太短")
}
return Header{
Version: b[0],
Type: b[1],
Flags: binary.BigEndian.Uint16(b[2:4]),
Length: binary.BigEndian.Uint16(b[4:6]),
RequestID: binary.BigEndian.Uint16(b[6:8]),
}, nil
}你:binary.BigEndian 就是”字节序定死”的落地?
墨叔:对。协议里的每个约定(大端、8 字节头、版本号第一字节),都要在代码里精确对应。 协议和实现,是一体两面。
思考题
第 1 题(概念)
为什么设计协议的第一步是”搞清楚约束”,而不是”定报文格式”?请用本章的传感器场景,说明”算力、网络、生命周期”这三个约束分别如何影响了最终设计。
第 2 题(概念)
本章的报文头,为什么”版本号要放第一个字节”、“字节序要定死”、“长度字段位置要固定”?这三者有什么共同点?
第 3 题(设计)
在本章协议的基础上,增加一个需求:“云端要能批量下发固件更新(几 MB 大小),且设备要在弱网下可靠地完成更新。”
请你设计:固件更新报文怎么承载大块数据?怎么分片?怎么保证”分片传输的可靠性”?(提示:参考 HTTP 的 Range、TCP 的分段、以及本章的序号机制)
第 4 题(生活类比)
墨叔:设计协议,和”设计一门人类语言”,有什么惊人的相似之处?
提示:想想——
- 语言的”语法规则”对应协议的什么?
- 语言的”词汇可以不断增加新词”对应协议的什么?(提示:扩展点)
- 语言的”核心语法几百年不变”对应协议的什么?(提示:不变量)
- “方言”(不同地区说得不一样,但能大致听懂)对应协议的什么?(提示:宽容 vs 实现不一致)
第 5 题(更深一层的发问)
16.9 节的自我评审里,留下了三个 ⚠️(宽容分阶段、实现不一致、扩展点活性)。
请你深入思考其中一个——“扩展点保持活性”(GREASE 的思想):
- 为什么一个”预留了但长期不用”的扩展点会”失效”?(第 15 章讲过,请结合本章的传感器协议具体化)
- 在本章的传感器协议里,你如何设计一个”GREASE 式的机制”来保持扩展点活性?
- 这对”给不可升级设备设计协议”有什么特别的挑战?(提示:传感器卖出去就改不了,GREASE 需要”两端配合”,而老设备端改不了怎么办?)
参考答案
第 1 题答案
为什么要先搞清楚约束:因为协议没有”最优解”,只有”在给定约束下的最优解”。报文格式只是”结果”,约束才是”决定结果的原因”。先定格式,等于”先给答案再猜题目”。
三个约束如何影响设计:
| 约束 | 影响 |
|---|---|
| 算力弱(MCU) | 报文要简单、解析要轻 → 选二进制 + 定长头,避免 JSON 的复杂解析 |
| 网络弱(易断、按流量计费) | 要省流量 → 选预定义 schema + 二进制(不传字段名);要支持重连 → 设计序号 + 去重 + 补发 |
| 生命周期(不可升级) | 兼容性是生死线 → 不变量定死、扩展点预留、优雅降级,这是第 15 章的直接应用 |
一句话:约束是”因”,格式是”果”。 不懂约束就设计协议,就像不知道路况就设计车。
第 2 题答案
共同点:这三者都是**“最底层、最基础、最不能改”的东西**,是所有未来演进都依赖的地基(不变量)。
-
版本号第一个字节:让接收方能”先读版本号,再决定怎么解析”。未来任何版本升级,旧实现都能靠”第一个字节”识别版本并做相应处理(或优雅拒绝)。没有它,无法演进。
-
字节序定死(大端):多字节数字(长度、版本、ID)的解析方式,一旦双方不一致,读出来的数字完全不同,报文彻底错乱。这是二进制协议的第一号坑,必须第一天定死。
-
长度字段位置固定:边界判定永远靠它(第 2 章)。位置固定,接收方才能”不看版本号就能找到长度,从而切出完整报文”。
共同本质:它们都是”被所有实现、所有版本、所有中间设备依赖来做最基础决策”的东西——识别版本、解析数字、切分报文。 所以它们必须”永不变”。
第 3 题答案
设计固件更新(大块数据 + 弱网可靠传输):
1. 分片承载:几 MB 的固件,不能塞进一个报文(报文长度字段是 2 字节,最大 64KB,且弱网下大报文极易失败)。所以把固件切成一个个小片(比如每片 1KB),分片传输。
2. 分片报文设计:
新报文类型:0x05 = 固件分片(FIRMWARE_CHUNK)
负载结构:
[固件版本号][总片数][当前片序号][本片数据]
3. 可靠性保证(弱网):
- 每片带序号(当前片序号):接收方按序号拼接,缺片了能发现。
- 批量确认:不是每片都 ACK(太费流量),而是”收满一批(比如 32 片)后,统一 ACK”。
- 选择性重传:接收方 ACK 时告诉发送方”我缺第 3、7、15 片”,发送方只重传缺的片(而不是整个固件)。这就是”选择性重传”(类比 TCP 的 SACK)。
- 整包校验:所有片传完后,用一个总体的校验和(如 CRC32 或哈希)验证固件完整性。
这个设计复用了前面学过的:
- 分片 = HTTP 的 Range / TCP 的分段思想。
- 序号 + 去重 + 补发 = 本章 16.5 的可靠性机制。
- 选择性重传 = TCP SACK 的思想(只重传缺的,不重传全量)。
一句话:大块数据的可靠传输,本质是”分片 + 序号 + 选择性重传 + 完整性校验”。 这是所有可靠传输协议(TCP、QUIC、HTTP Range)的共同套路。
第 4 题答案
| 语言 | 协议 |
|---|---|
| 语法规则 | 报文格式(头结构、字段顺序) |
| 词汇可以不断增加新词 | 扩展点(新类型、新字段、TLV) |
| 核心语法几百年不变 | 不变量(版本号位置、字节序、边界) |
| 方言(大致能懂但不完全一致) | 宽容 vs 实现不一致 |
深刻的相似之处:
-
语法 = 报文格式:语言的语法规定”词怎么排成句”,协议的格式规定”字节怎么排成报文”。都是”结构规则”。
-
词汇增长 = 扩展点:语言能不断造新词(“互联网""区块链”)而不用改语法;协议能不断加新字段(新首部、新类型)而不用改格式。这都是”在不破坏结构的前提下,扩展内容”。
-
核心语法不变 = 不变量:汉语的”主谓宾”结构几千年没变;HTTP 的方法语义、状态码分类 30 年没变。“地基”之所以能几十年不动,是因为它是”被所有人依赖来做理解”的最底层结构。
-
方言 = 宽容 vs 不一致:方言之间”大致能懂但细节不同”,对应协议实现之间”大致互通但细节不一致”。
- 宽容(Postel 法则)就像”听懂带口音的普通话”——降低了沟通门槛(普及期有利)。
- 实现不一致就像”方言差异大到误会”——“我理解的”和”你说的”不一样,导致错误(成熟期有害,是安全漏洞的土壤)。
升华:语言和协议的共同本质是——它们都是”一群人为了沟通,达成的关于’符号如何表达意义’的约定”。 所以语言学的很多规律(语法稳定、词汇开放、方言演化),在协议设计里惊人地成立。设计协议,某种意义上就是”设计一门只有机器才说的小众语言”。
第 5 题答案
1. 为什么”预留了但长期不用”的扩展点会失效:
以本章传感器的 Flags 位图为例。假设我们预留了 16 个 flag bit,但十年都没用过其中一个 bit。那么:
- 云端和设备的实现者,会默认”这些 bit 永远没用”。
- 于是他们在写解析代码时,会写死:“遇到 flag bit 非零就报错”(而不是”忽略未知 bit”)——因为他们认为”非零就是异常”。
- 十年后,你想启用那个 bit 加新特性,发现沿途(或对端)的实现早就”不允许”它非零了——扩展点”僵死”。
本质:扩展点的活性,靠”持续被使用、被宽容对待”来维持。长期不用,实现者就会”关掉”对它的宽容,扩展点就失效了。 这和第 15 章、第 13 章 GREASE 讲的完全一致。
2. 如何设计 GREASE 式机制:
在传感器协议里,可以这样:
- 定期发送”无意义的未知值”:云端和设备的握手阶段,定期往 Flags 位图里随机置一个”无意义”的 bit,或者往 TLV 里塞一个”无意义的、未知的 Type”。
- 强制”必须忽略未知”:协议规范里明确规定——“接收方必须忽略任何未知的 flag bit、未知的 Type、未知的 TLV 类型,不得报错。”
- 一致性测试:设备的合规测试里,专门测”遇到未知值是否优雅忽略”,不合格的设备不予认证。
3. 对”不可升级设备”的特殊挑战:
这是本题最深刻的地方。GREASE 需要两端配合(发送端故意发、接收端正确忽略)。但传感器卖出去就改不了,老设备端:
- 无法更新成”会发 GREASE 随机值”的版本。
- 更糟:如果老设备当年实现时没有”忽略未知”的能力(写死了”未知就报错”),那么你未来发的任何 GREASE 值/新扩展,都会被老设备”报错拒绝”。
这意味着什么:
对于”不可升级”的设备,你必须在它出厂的那一天**,就把”宽容未知”的能力烧进去。因为一旦出厂,你就永远没有第二次机会去给它加”宽容”。**
这引出一个极其重要的结论:
“宽容未知”不是一个”以后可以补”的优化,而是”必须第一天就做对”的地基。 对可升级的软件,你还能事后打补丁;对不可升级的硬件,“第一天没做对”就等于”永远做不对”。
这就是为什么第 15 章反复强调”扩展点的精髓在降级”、“降级比扩展重要”——因为降级能力(宽容未知)一旦缺失,是补不回来的;而扩展(加新功能)反而可以慢慢来。
最终升华:设计”面向长期、面向不可升级设备”的协议时,最高优先级的,不是”预留多少扩展点”,而是”把’优雅降级/宽容未知’这个能力,做进每一个设备的第一版固件里”。 因为那是唯一的、不可逆的机会。
一页纸小结
设计协议的第一步是”搞清楚约束”:算力、网络、电量、生命周期、中间设备——约束是”因”,格式是”果”。
先定不变量:版本号放第一字节、字节序大端、长度字段位置固定、定长头。这些是”动不得的地基”。
边界靠长度前缀,长度是”承诺”,不匹配即错误(第 2 章)。
错误分类复用状态码智慧:第一位是”能不能重试”(第 4 章)。
可靠性靠”序号 + 去重 + 补发”(第 3、7、14 章的统一答案)。
扩展点 + 优雅降级:Flags 位图、新类型、TLV,每个都配”老设备降级”(第 15 章)。
自我评审:用 10 条原则反查设计,找到三个 ⚠️ 就是下一次迭代方向。
核心心法:协议没有最优解,只有”约束下的最优解”;设计协议,就是”在约束之间权衡”。
下一章:协议设计出来了,但光有”纸面协议”不够——你得知道它是怎么在你的 Go 代码里”活”起来的。我要带你从 Go 的 net/http 源码视角,看看那些你学过的概念(报文解析、隐式决策、连接管理、超时),是怎么被一行行代码实现的。以及那个经典的坑:为什么 net/http 有时候会”偷偷”做你不想要的事?