第 21 章 / 共 51 章
卷三 · 协议开发
WebSocket 基础:六回调写双向通信
RFC 6455 的握手、掩码、分片、控制帧、压缩全部在 XServer 与 xrt 层完成——你的脚本是这条流水线末端的六个函数。
本章导读
站位:WS 的"基础"章已经能写出可用的实时服务——因为协议复杂度被宿主吃掉了。进阶章(ch22)处理广播、限额与子协议。
21.1 引入
手写 WebSocket 的痛苦清单:Upgrade 握手校验(Sec-WebSocket-Key 的 SHA-1+Base64)、帧头解析(掩码位、扩展长度)、分片重组、控制帧穿插、close 状态机、permessage-deflate 扩展协商。XServer 的分工是:宿主做握手与校验(失败 400),xrtWsStreamAttach 做帧层——回调拿到的是"整条消息",发送的是一个函数调用。
21.2 握手与回调
HTTP 请求 → Upgrade 校验(xrtWsUpgradeRequestCheck)
失败 → 400 成功 → 101 切换 → xrtWsStreamAttach
│
┌──────────────────────────────────┴───────────────┐
▼ ▼ ▼ ▼ ▼
WsOpen WsText WsBinary WsPing WsClose
(连接就绪)(整条文本)(整条二进制)(观察用)(iCode+原因)
- 握手按 vhost 路由(Host/SNI 同 HTTP);路由失败 404
- host 未导出 WsText/WsBinary → 503(服务存在但没写业务)
- Pong 由 xrt 层自动回应——WsPing/WsPong 回调只做观察(心跳统计)
- 握手期线路硬边界:防止读满 ReadLimit 后永久等待的慢速攻击
21.3 发送族与 echo
echo 服务(契约原文风格)
static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
(void)xrtWsStreamText(pWs, xrtStrView("connected"));
}
static void WsText(XS_HostInfo* pHost, xwsstream* pWs, xstrview tText)
{
(void)xrtWsStreamText(pWs, tText); /* echo */
}
static void WsBinary(XS_HostInfo* pHost, xwsstream* pWs, xbytesview tData)
{
(void)xrtWsStreamBinary(pWs, tData);
}
刚才发生了什么:发送族 xrtWsStreamText / Binary(拷贝发送)为主;另有 ...Ref(借用发送,函数返回前缓冲不可动)与 ...Take(转交所有权,零拷贝大消息)变体——按消息来源选。
21.4 连接上下文
WS 业务几乎都需要"每个连接自己的状态"(用户身份、订阅列表)。推荐做法:连接注册表(自建 map:连接指针 → 上下文),WsOpen 建、WsClose 删:
连接注册表骨架
static xmutex G_WsLock;
static xmap* G_WsConns; /* xwsstream* → ConnCtx*(跨回调借用需自管)*/
typedef struct ConnCtx {
char sUser[64];
uint64 iLastSeen;
} ConnCtx;
static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
ConnCtx* pCtx = xrtCalloc(1, sizeof(ConnCtx));
/* 入表(锁内)... */
}
static void WsClose(XS_HostInfo* pHost, xwsstream* pWs,
uint16 iCode, xstrview tReason)
{
/* 出表并释放(锁内);WsClose 保证是终态回调 */
}
注意:连接生命周期宿主管,你的上下文生命周期你管——注册表模式让两者精确同步。这个表也是 ch22 广播的基础设施。
21.5 契约
- 回调线:六回调在 worker 线程可并发;消息视图(tText/tData)回调内有效。
- 终态线:WsClose 是每连接的终态回调——注册表清理放这里最可靠。
- 发送线:Text/Binary 拷贝、Ref 借用、Take 转交——按缓冲所有权选变体。
常见错误
把消息视图存进注册表
- 症状
- 后续读到错乱数据。
- 原因
- tText/tData 是回调内借用视图——存表前先拷贝(注册表存你自己的 ConnCtx,不存视图)。
Ref 发送后立刻改缓冲
- 症状
- 偶发消息内容错乱。
- 原因
- xrtWsStreamTextRef 借用你的缓冲——函数返回 ≠ 发送完成(背压下数据还在缓冲里)。要立刻复用缓冲就用拷贝版或 Take 版。
编程练习
基础echo 三连测
跑通 echo 后用任意 WS 客户端发文本/二进制/大消息(1MB)。
验收:三类消息回显一致;1MB 消息被 1009 拒收(默认上限,ch22 调整)。
进阶带身份的会话
首条消息约定 "login:名字",注册表记身份,此后 echo 带 "[名字] " 前缀。
验收:两个并发会话互不串扰;断线后注册表无残留。
挑战心跳看门狗
用 WsPing 观察统计 RTT;60 秒无任何消息的连接主动关闭。
验收:拔网线的僵死连接在 60 秒级被清出注册表。
小结
| 要点 | 结论 |
|---|---|
| 宿主负责 | 握手校验(400/404/503)+ Attach 帧层 |
| 六回调 | Open/Text/Binary/Ping/Pong/Close;Pong 自动回 |
| 发送族 | Text/Binary(拷贝)、Ref(借用)、Take(转交) |
| 上下文 | 自建注册表:Open 建、Close 删(终态保证) |
本章涉及的 API 参考
xrt 延伸阅读
- xrt 参考 · websocket_stream — 流式发送与引用计数语义
- xrt 参考 · websocket_upgrade — 握手校验的库层细节