第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 的安全边界
为什么协议边界不能来自 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