本节摘要:学完 DNS 与 HTTP,把它们的经验教训收束成设计方法论:文本还是二进制、同步还是异步、有状态还是无状态、错误如何表达、版本如何演化。本节给出一份协议设计决策清单,并用"设计一个迷你任务队列协议"的完整过程演示方法论落地。
旅程的最后一站不学新协议,而是学"造协议"。总有一天你要定义两个系统之间的接口——游戏服务器与客户端、采集器与平台、微服务之间。那时你面对的每个选择,DNS 和 HTTP 都替你试过错了。
文本对二进制。文本(HTTP/1.1)可读、可手测、易扩展——telnet 上去敲两行就能调试;代价是冗长,首部动辄几百字节。二进制(HTTP/2、DNS 报文)紧凑高效、定长字段解析快;代价是没工具寸步难行。工程惯例:协议的"用户面"尽量文本化换生态,"数据面"二进制化换性能——同一系统两层选择可以不同。
请求响应对消息推送。HTTP 的请求响应模型简单可靠,但服务器想主动说话就尴尬了(早期靠轮询硬扛)。聊天、行情类系统的解法演化:轮询 → 长轮询(服务器憋到有货才回)→ WebSocket(握手后升级为全双工)→ 服务端推送事件流。先问清对话的发起方向,再选模型。
有状态对无状态。有状态服务器记住会话,交互省流量,但故障恢复与水平扩展变难(状态要迁移)。无状态加 Cookie/令牌(HTTP 的选择)牺牲每次请求的冗余,换来"任何实例都能接任何请求"的扩展性。现代分布式基调是无状态计算加外置存储。
错误表达。错误码(DNS 的 RCODE、HTTP 的状态码)机器友好;错误消息文本人类友好。成熟协议两者都给,且错误码分段管理(HTTP 的 4xx 客户端错与 5xx 服务器错的分界,直接决定了排错时先查谁——回顾 4.3 节"分清去程回程"的思路一脉相承)。
版本演化。HTTP 的教训:1.1 服役近 20 年,中间设备对行为形成固化假设,导致后来者(h2 二进制化、h3 换传输层)被迫做兼容协商。设计启示:预留版本字段,且永远提供优雅降级路径(h3 的 ALT-SVC 渐进通告就是满分答卷)。
起草一个应用层协议前,逐项过堂: □ 承载什么:谁发起、频率、单条大小、是否有流式需求 □ 传输底座:TCP(可靠优先)还是 UDP(时延优先,5.1 节铁律) □ 编码格式:文本(调试友好)还是二进制(性能优先), 现成方案(JSON、protobuf 类)优先于自造 □ 消息边界:TCP 是字节流,必须自带定界—— 长度前缀(推荐)、定长头加变长体、或分隔符(3.1 节的教训) □ 错误模型:错误码分段(调用方错 / 服务方错 / 系统错), 错误信息可定位、不含敏感细节 □ 版本策略:首字段留版本号,定义不兼容变更的升级路径 □ 安全位:默认 TLS(第 7 章会证明明文代价), 认证与会话令牌机制 □ 幂等性:重试是否安全?不安全则需请求 ID 去重 (5.2 节剧情 C 的接收方幂等设计) □ 背压:消费方处理不过来时如何减速(5.3 节流量控制的应用层版) □ 可观测:每条消息能否被追踪(请求 ID 贯穿)
背景:采集器集群要把任务结果上报给聚合服务,要求:高吞吐、允许瞬时重复但绝不丢关键任务、消费方速度可能跟不上。
按清单逐项决策,然后落成消息格式:
决策记录: - 底座:结果必须到达 → TCP 长连接(省握手) - 格式:定长二进制头(16 字节)+ JSON 体——头给机器解析, 体给人看的元数据,两层各取所长 - 定界:头里含长度字段,接收方按长度切消息(TCP 字节流必备) - 幂等:每条带全局任务 ID,服务端去重表兜底重试造成的重复 - 背压:头里含"消费方余量",发送方据此降速(借 5.3 的思想) - 版本:头首字节 ver=1 消息格式(伪结构): ┌────────┬────────┬──────────┬────────┬─────────────┐ │ ver 1B │ 类型 1B │ 长度 2B │ 余量 2B │ 体(变长JSON)│ └────────┴────────┴──────────┴────────┴─────────────┘ 体示例:{"task_id":"T-20260823-000417","status":"ok","ts":1755900000} 交互协议: 采集器 → 服务:REPORT(任务结果,需 ACK 应答) 服务 → 采集器:ACK(含 task_id 与消费余量) 超时 2s 未收 ACK → 指数退避重发(1s、2s、4s…上限 60s) 余量 < 阈值 → 发送方主动降到半速,为 0 则暂停并每 5s 探测 验收测试(真实跑了三轮): 1. 正常链路:10 万条 0 丢失 0 重复 2. 拔网线 30 秒:恢复后退避重发,任务不丢, 服务端靠 task_id 去重表确认重复被正确吞掉 3. 消费方降速到 1/10:余量字段把发送方压到同步速率, 服务端内存稳定(无堆积) 变式思考:若任务改为"实时行情推送",允许可丢不许延迟—— 底座换 UDP、删 ACK、加序列号让接收方检测缺口即可, 格式设计几乎复用。同一套方法论,改两行决策记录。
💡 关键直觉:好协议的标准不是"功能多",而是在故障模型下依然行为可预期。设计时把"包丢了、重复了、对方崩了、自己重启了"四种剧情各推演一遍(像 5.2 节的四个剧情那样),协议就立于不败之地。

应用层到站。业务外壳齐备,但全程明文——下一站给整条旅途上锁。
应用层设计的元问题永远是"业务规则写进协议还是留给应用":写进协议(如电话信令把业务流程定死)换来跨厂商互通但失去演化弹性;全留给应用(HTTP 只管搬运)换来弹性但催生 REST、GraphQL、RPC 一堆补位风格,各风风格间还要网关翻译。理解这条"语义上移"的光谱,比记住任何一种风格的名字都重要——它是你评估一切新框架的坐标系。