第6章:网络通信协议设计
对齐原书第 6 章:粘包与解包、struct/TLV、整数压缩、版本与分片、XML/JSON、自定义协议、HTTP、邮件协议和 WebSocket。
学习目标
- 能解释 TCP byte stream 上拆包、合包与增量解码,设计 length-prefix、delimiter 或 self-describing frame 并限制长度、分片和资源
- 能比较 raw struct、fixed fields、TLV、varint、XML 与 JSON,设计固定字段宽度、network byte order、version negotiation 和 unknown-field policy
- 能分析 HTTP、SMTP、POP3 与 WebSocket 的 framing/state machine,写出 chunk、长连接、handshake、mask、compression 和 close 的安全边界
机制总览
协议 framing、版本与校验边界
- 1
编码 frame
固定头明确 magic、version、type、length 和必要校验。
- 2
增量解码
decoder 先等完整头,再按受限长度等待 body,可保留半包。
- 3
版本演进
未知可选字段可跳过,破坏性变更通过显式版本协商。
章级决策实验
协议 framing、版本与校验边界
沿一条消息从编码到解码,验证长度、版本、错误处理和兼容策略。
选择推理阶段
当前阶段 · 编码 frame
固定头明确 magic、version、type、length 和必要校验。
可核验证据
golden bytes、跨语言编码测试与 schema。
协议设计的目标不是字段最少,而是在任意分片、异常输入和版本组合下仍能确定边界并安全失败。
失效—证据矩阵
协议 framing、版本与校验边界
编码 frame
典型失效
依赖分隔符却没有转义,或 length 未定义字节序。
核验证据
golden bytes、跨语言编码测试与 schema。
增量解码
典型失效
一次 recv 被当成完整消息,恶意长度导致无限分配。
核验证据
随机分片测试、长度上限与模糊测试。
版本演进
典型失效
复用旧字段改变语义,新旧节点静默误解。
核验证据
兼容矩阵、回放旧流量与灰度指标。
为什么协议边界不能来自 recv 次数
TCP 保证有序 byte stream,不保存 sender 的 send 调用边界。一个 frame 可被多次 recv 拆开,多个 frame 也可合并到一次 recv;所谓“粘包”不是 TCP 故障,而是应用没有定义或正确实现 framing。
协议设计必须同时回答:怎样找边界、字段怎样编码、版本怎样演化、恶意长度怎样拒绝、失败后是否可重新同步、日志如何脱敏,以及 client/server 如何证明互操作。
6.1 理解 TCP:可靠的是 bytes,不是消息
TCP sequence number、retransmission 与 receive buffer 让 bytes 按序到达;它不理解你的 C++ object、JSON document 或 business packet。Nagle、MSS、congestion control、sender buffering 和 receiver read size 都会改变交付形态。
先预测 4-byte length header 被拆成两次 recv 时 decoder 能否读取 length,以及一次 recv 合并两帧时应 dispatch 几次;再操作下图观察 accumulator。
切换拆分与合并交付
6.2 如何解决粘包:选择 framing contract
常见方案:fixed-size 适合严格定长记录;delimiter 适合文本但需 escaping/maximum line;length-prefix 高效通用但必须验证 length;TLV/self-describing 可跳过未知字段但解析更复杂。协议可以外层 length-prefix、内层 TLV。
↡frame header 显式携带 payload byte count,decoder 先读固定 header,再等待指定长度 body 的 framing 方法。enum class DecodeResult { NeedMore, FrameReady, ProtocolError };
DecodeResult Decoder::next(Buffer& input, Frame& output) {
if (input.size() < 4) return DecodeResult::NeedMore;
const auto length = input.peek_u32_be();
if (length == 0 || length > kMaxFrameBytes) return DecodeResult::ProtocolError;
if (input.size() < 4u + length) return DecodeResult::NeedMore;
input.consume(4);
output = input.take(length);
return DecodeResult::FrameReady;
}6.3 解包与处理:parser 和 business handler 分层
decoder 只验证 wire syntax 并产生 typed message;dispatcher 校验 message type/version/auth state;business handler 执行业务。把业务调用放进 recv parsing loop 会让慢 handler 阻塞所有连接,并让 parser state 难以 fuzz。
↡可接收任意长度 byte chunks、保留未完成状态,并在数据足够时产出零到多个完整消息的 parser。void Connection::on_bytes(std::span<const std::byte> bytes) {
input_.append(bytes);
for (;;) {
Frame frame;
switch (decoder_.next(input_, frame)) {
case DecodeResult::NeedMore: return;
case DecodeResult::ProtocolError: return close_bad_frame();
case DecodeResult::FrameReady: dispatch(std::move(frame)); break;
}
}
}decoder 测试要随机切分同一 wire input、随机合并多帧、截断每个 byte boundary、注入超长/unknown type;所有 chunking 方式必须产出相同 messages 或明确 error。
6.4 从 struct 到 TLV:wire format 不是 ABI dump
直接 send(&object, sizeof object) 泄漏 compiler padding、alignment、native endian、pointer、enum width 与 ABI。显式 fixed fields 可解决 width/endian,但 positional layout 的插入/删除仍易破坏旧 reader。TLV 用 tag/type/length/value 让 reader 识别字段并跳过 unknown optional fields。
比较 raw struct、fixed fields 与 TLV
6.5 整型压缩:varint 的收益与边界
varint 用每 byte 的 continuation bit 表示更多 group,小正整数占更少 bytes;signed value 可先 ZigZag,使绝对值小的负数也紧凑。代价是 branch、变长读取和恶意超长序列。
void write_varuint(std::uint64_t value, Buffer& out) {
while (value >= 0x80) {
out.push(static_cast<std::uint8_t>(value) | 0x80);
value >>= 7;
}
out.push(static_cast<std::uint8_t>(value));
}decoder 最多读取与目标 width 对应的 bytes,并检查最后一组溢出;不要无限循环等待 terminating byte。随机 ID 接近 64-bit full range 时,fixed 8 bytes 可能更简单且更快。
6.6 协议设计注意事项:宽度、精度、端序、升级
wire schema 显式写 uint16/uint32/int64,不要写“int”;network byte order 或规定 endian 必须一致。浮点跨语言还要规定 IEEE-754、NaN/Infinity、rounding 与业务精度,货币通常用 fixed-point integer。
对应原书的四条检查项是:不要依赖 C++ struct 的字节对齐,要显式指定整型字段长度,统一处理大小端,并用兼容矩阵验证协议自动升级而不是猜测部署先后。
↡发送方与接收方在连接或消息中声明支持版本/feature,并选择双方交集或明确拒绝的升级机制。magic:u32 | protocol_version:u16 | flags:u16 | frame_length:u32 | message_type:u16version 不应只靠部署顺序猜测。握手或 header 声明 version/capabilities;rolling upgrade 同时验证 old↔new。新增 optional field 通常兼容,改变既有 tag 含义、默认值或单位通常不兼容。
6.7 包分片:重组必须有资源预算
应用 message 超过单 frame、transport 或业务限制时可分片,header 至少含 message id、fragment index/count 或 offset、total length 和 integrity check。receiver 需要 per-peer/global memory limit、fragment count limit、timeout、duplicate/overlap policy。
↡把一个逻辑消息拆成多个有身份与顺序信息的 frames,并在限额与超时内重组的应用层机制。message_id:u64 | fragment_index:u16 | fragment_count:u16 | total_length:u32 | payload若底层协议已可靠传输,不要无原因重复实现 packet-level reliability;应用分片主要服务于 message size、streaming、并行处理或中间层限制。
6.8 XML 与 JSON:文本可读性不等于 schema
XML 提供 element/attribute/namespace,JSON 提供 object/array/string/number/bool/null。两者都需要 schema-level constraints:required fields、type/range、unknown policy、duplicate keys、encoding、depth 与 total size。JSON number 跨语言可能丢失 64-bit integer precision。
{
"version": 2,
"requestId": "18446744073709551615",
"items": [{ "id": 7, "quantity": 2 }]
}XML parser 禁用不必要的 external entity/network resolution,限制 entity expansion;JSON 限制 nesting/string/array size。human-readable payload 也不能把 token、password 或个人数据直接写日志。
6.9 一个自定义协议:先写状态和失败模型
自定义 frame 可采用 magic/version/flags/length/type/request-id/header checksum/payload。magic 帮助检测错流,不等于 parser 可无限扫描 resync;length 与 compression 都需解压后上限。request-id 用于关联 response,不应承担 authorization。
struct DecodedHeader {
std::uint16_t version;
std::uint16_t flags;
std::uint32_t payload_length;
std::uint16_t message_type;
std::uint64_t request_id;
};protocol specification 要列出 byte layout、state machine、timeouts、error codes、close behavior、limits、security、examples 和 compatibility matrix。只有 encoder 代码而没有独立规范,很难跨语言互操作。
6.10 HTTP:行、header 与 body framing
HTTP/1.1 message 由 start-line、headers、空行与可选 body 构成。GET 通常检索资源,POST 提交 representation,但安全性、幂等性与缓存由 method semantics/endpoint contract 定义,不能只看是否有 body。
↡HTTP/1.1 通过十六进制 chunk size、chunk data 与终止 0 chunk 逐块传输未知总长度 body 的编码。HTTP/1.1 200 OK\r\n
Transfer-Encoding: chunked\r\n
\r\n
5\r\nHELLO\r\n
0\r\n\r\nparser 必须按协议规则处理 Content-Length、chunked 与 close-delimited body,拒绝冲突/歧义,避免 request smuggling。long-lived connection 上每个 request/response 都有独立 framing;keep-alive 不是“永不关闭”,要有 idle timeout、request count 与 drain policy。
HTTP client 应使用成熟库如 libcurl,配置 DNS/connect/overall timeout、TLS verification、redirect、proxy、body limit 和 cancellation;server 也应使用经过验证的 parser。RESTful interface 是资源/representation/HTTP semantics 的设计约束,不是“返回 JSON”同义词。
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);6.11 SMTP、POP3 与邮件客户端
SMTP 是 command/reply state machine:server greeting、EHLO capabilities、MAIL FROM、RCPT TO、DATA、message terminator、QUIT;多行 reply 与 status class 要完整解析。DATA 阶段用 dot-stuffing,不能简单搜索第一个点号。
POP3 通常经历 authorization、transaction、update:USER/PASS(或更安全认证)、STAT/LIST/RETR/DELE、QUIT。邮件客户端还要解析 message headers/body/MIME,并通过 TLS、certificate validation 与 credential protection 建立安全边界。
↡SMTP/POP3 中由 server reply code 和当前会话阶段共同决定下一条合法 command 的文本协议执行模型。6.12 WebSocket:HTTP Upgrade 后切换 frame parser
client 发送 HTTP Upgrade 与随机 Sec-WebSocket-Key;server 返回 101、Upgrade/Connection headers,并按 key + GUID 计算 accept。验证通过后连接不再解析 HTTP messages,而解析 WebSocket frames。
按原书目录,这条链依次包含 WebSocket 握手、WebSocket 协议格式、WebSocket 压缩以及 frame 的装包与解包;四者共享连接状态,却必须分开验证。
切换 HTTP、WebSocket 与邮件协议阶段
byte 0: FIN RSV1 RSV2 RSV3 opcode
byte 1: MASK payload_len(7 bits)
optional: extended length | masking key
payload: masked client data or server datatext/binary message 可跨 continuation frames;ping/pong/close 是 control frames。close handshake 要解析 code/reason 并停止新业务发送,不能直接把 TCP EOF 当作所有 WebSocket 正常关闭。
本章回顾:协议是边界、状态与兼容性的集合
- TCP recv chunk 不是消息;incremental decoder 按 framing 累积、验证、循环提取。
- raw struct 不是 wire schema;字段宽度、endian、length、required/optional 和 unknown policy 必须显式。
- version negotiation、golden vectors、old/new 双向测试比“先升级服务端”可靠。
- HTTP、SMTP、POP3 和 WebSocket 都是状态机,各自用 length/chunk/terminator/frame 定义边界。
- 任意 length、fragment、nesting、compression 和 parser recovery 都必须有资源预算与失败策略。
练习
问题 1:length-prefix decoder 一次收到半个 header,下一次收到剩余 header、完整 body 和下一帧。正确循环是什么?
问题 2:v2 在 raw struct 中间插入字段,v1 reader 为什么会错?怎样用 TLV 与 compatibility policy 修复?
问题 3:WebSocket client frame 未 mask、声明 20 MiB compressed payload,且 opcode 为 continuation 但没有已开始 message,server 应怎样处理?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- message framing
- length-prefixed frame
- incremental decoder
- TLV encoding
- varint encoding
- version negotiation
- application fragmentation
- HTTP chunked coding
- mail protocol state machine
- WebSocket frame