第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。

分步1 / 3

切换拆分与合并交付

6.2 如何解决粘包:选择 framing contract

常见方案:fixed-size 适合严格定长记录;delimiter 适合文本但需 escaping/maximum line;length-prefix 高效通用但必须验证 length;TLV/self-describing 可跳过未知字段但解析更复杂。协议可以外层 length-prefix、内层 TLV。

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。

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。

分步1 / 3

比较 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 的字节对齐,要显式指定整型字段长度,统一处理大小端,并用兼容矩阵验证协议自动升级而不是猜测部署先后。

magic:u32 | protocol_version:u16 | flags:u16 | frame_length:u32 | message_type:u16

version 不应只靠部署顺序猜测。握手或 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。

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 200 OK\r\n
Transfer-Encoding: chunked\r\n
\r\n
5\r\nHELLO\r\n
0\r\n\r\n

parser 必须按协议规则处理 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 建立安全边界。

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 的装包与解包;四者共享连接状态,却必须分开验证。

分步1 / 3

切换 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 data

text/binary message 可跨 continuation frames;ping/pong/close 是 control frames。close handshake 要解析 code/reason 并停止新业务发送,不能直接把 TCP EOF 当作所有 WebSocket 正常关闭。

本章回顾:协议是边界、状态与兼容性的集合

  1. TCP recv chunk 不是消息;incremental decoder 按 framing 累积、验证、循环提取。
  2. raw struct 不是 wire schema;字段宽度、endian、length、required/optional 和 unknown policy 必须显式。
  3. version negotiation、golden vectors、old/new 双向测试比“先升级服务端”可靠。
  4. HTTP、SMTP、POP3 和 WebSocket 都是状态机,各自用 length/chunk/terminator/frame 定义边界。
  5. 任意 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

资料与写作方式声明

本章以C++服务器开发精髓,第6章 网络通信协议设计权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…