第 33 章 / 共 51 章
卷五 · 扩展、运维与实战

XTP2 扩展:在 TCP 上说二进制

TCP 给你字节流,XTP2 给你消息边界。本章把帧库(ch23 的伏笔)装进 EventData,得到一个能防粘包、防恶意帧的协议服务。

本章导读

站位:xtp2 是"编解码原语"(API 参考),本章是"组装工艺"——会话状态放哪、粘包怎么留、坏帧怎么断。完整成品在 实战二。

33.1 引入

二进制协议的痛点从来不是"设计帧格式"——是实现纪律:边界处理(粘包/半包)、长度校验(恶意超大帧)、类型安全(视图生命周期)。xtp2 把这三样做成了库:32 字节定长头 + 四型消息(REQUEST/RESPONSE/PUSH/EVENT)+ 流式解析器;你只需要给它喂字节、处理产出的消息。前置:build.bat xtp 变体。

33.2 帧与会话缓冲

组装的第一件事是会话结构:每连接一个处理器 + 半包残留缓冲(ch21 注册表模式的 TCP 版):

会话结构(注册表 + 处理器)
typedef struct XtpSession {
    xtp2_processor* pRx;      /* 每连接一个解析器 */
    xbuffer*        pPending; /* 半包残留(feed 消化不完的尾巴)*/
} XtpSession;
/* Open 建(入注册表)· Close 删(销毁处理器与缓冲)*/

为什么需要 pPending:xtp2_feed 一次至多产出一条消息——TCP 一次送达可能含"一条半"甚至多条;消化到 XTP2_MORE 时剩余字节要留到下一次 EventData 继续。

33.3 EventData 组装协议

协议组装核心(官方示例的会话化版本)签名出处 lib/xtp2.h
static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
    XtpSession* pS = SessionFind(pConn);      /* 注册表取会话 */
    xbuffer* pIn = pS->pPending;
    BufferAppendNetBuf(pIn, pBuffer);          /* 缓冲链 → 连续缓冲 */

    for ( ;; ) {
        size_t iUsed = 0;
        xtp2_message tMsg;
        xtp2_result r = xtp2_feed(pS->pRx,
            BufferData(pIn), BufferSize(pIn), &iUsed, &tMsg);
        BufferConsume(pIn, iUsed);             /* 吃掉已消费部分 */
        if ( r < 0 ) {                     /* 坏帧:粘性错误 */
            SessionDrop(pConn);                 /* 断开:见 33.4 决策 */
            return;
        }
        if ( r == XTP2_MESSAGE ) {
            DispatchMessage(pConn, &tMsg);     /* 用毕即弃的视图 */
            continue;                      /* 缓冲里可能还有整条 */
        }
        break;  /* XTP2_MORE:留尾巴等下一轮 */
    }
}

刚才发生了什么——组装工艺的四个要点都在里面:残留缓冲衔接轮次、consumed 精确消费、消息视图当轮用毕、错误即断(粘性错误没有"再试试")。响应方向用 xtp2_measure + encode 打包后 xrtNetStreamSend——REQUEST 的 id 原样放进 RESPONSE 即可实现关联。

33.4 limits 与安全

  • 长度防线:max_packet(默认 1MiB)在头解析期就生效——恶意超大帧在读 body 之前被拒,内存不会被"声明 4GB"的包头骗走
  • 结构防线:参数表长度一致性复核、头字段越界检查、status 的无符号转换避开实现定义行为
  • 断连策略:协议粘性错误意味着流已不可信——fuzz 验证的回环不变量保证"能 decode 的都是合法帧",反之坏帧后继续喂只会继续错。断开 + 日志是对二进制协议最诚实的处理
  • 时限防线:TCP idle_timeout 兜住"连接建立后只发半个头"的慢速攻击

33.5 契约

  • 会话线:每连接一个处理器;残留缓冲跨轮衔接;Close 终态统一销毁。
  • 消息线:feed 产出的视图当轮用毕;需要留存先拷贝。
  • 错误线:粘性错误 = 断开;limits 收紧到业务实际需要。

常见错误

忽略 consumed,整轮重喂
症状
消息重复处理或解析错位。
原因
feed 的 consumed 告诉你本轮吃了几字节——不消费掉它们,下一轮会从头重解。缓冲操作必须严格按 consumed 前进。
坏帧后 reset 重试
症状
用 reset 清错误后继续喂同一条流,消息内容错乱。
原因
reset 只重置解析器状态,不修复已经错位的字节流——半条坏消息的后半截会被当新消息头。reset 的正确用途是"复用处理器对象"(新连接/新流)。

编程练习

基础echo 帧

实现 PING REQUEST → PONG RESPONSE 的最小服务(客户端可用 xtp2_encode 造帧)。

验收:id 正确回带;一次发 3 条帧全部响应(粘包)。
进阶分片轰炸

客户端把一帧拆成 1 字节一片发送,验证半包缓冲的正确性。

验收:逐字节发完一条消息后服务端正确产出一次。
挑战恶意帧测试

构造超限长度头、截断帧、非法 magic 三类攻击,确认服务端内存平稳且断连干净。

验收:三类全部一次断连;RSS 全程平稳;正常客户端不受影响。

小结

要点结论
定位编解码原语:帧格式+解析纪律做成了库
组装会话=处理器+残留缓冲;按 consumed 精确消费
消息feed 视图当轮用毕;REQUEST id 回带关联
安全limits 头期生效;坏帧即断;idle 兜底
本章涉及的 API 参考
xrt 延伸阅读