本章导读
卷九收官章。前三章给了升级(93)、帧(94)、流(95);本章补上双向通道的最后两块工程件并把全链路串起来。关闭协议:xrtWsCloseWrite/CloseParse 的往返、关闭码的语义域(1000 正常/1001 离开/1002 协议错误……)、写侧强制(原因必须合法 UTF-8、总长不超限、保留码不上线)——优雅关闭是"双方都知道结束了"的握手。permessage-deflate:WebSocket 的压缩扩展——协商层(offer/response 的严格区分、window bits、context takeover)、xwsinflater/xwsdeflater 流式对象(分片推进不聚合、双重上限、RSV1 只在首帧)、Stream 层的压缩发送路径。最后用"一条消息的完整旅程"把卷九九章从 HTTP 解析到压缩分片帧收尾——第 98 章起进入 xhttp 的高层世界。
97.1引入
两个工程问题收尾卷九。其一:连接怎么"好好说再见"?直接断 TCP,对端无法区分"正常结束"与"网络故障/中间设备截断"——监控告警、重连策略全都失效。Close 控制帧带着状态码与原因,让关闭成为双方确认的协议事件:发起方发 Close、对端回 Close、各自进入终态——CloseInfo 快照告诉你对端"为什么走"。其二:JSON 消息重复率高(键名、结构),文本压缩能省 70-90% 带宽——但 HTTP 的 Content-Encoding 思路(第 93 章)在双向流上要重新设计:压缩按消息不按连接、字典跨消息复用(context takeover)、控制帧永不压缩——这就是 permessage-deflate,协商与流式处理都是协议层工程。
97.2概念
关闭握手与码语义
关闭码的语义域分三段。1000-1003:正常关闭族(1000 正常、1001 端点离开、1002 协议错误、1003 不接受数据类型);1008-1011:错误族(1008 策略违反、1009 消息过大、1010 缺扩展、1011 内部错误);3000-4999:应用自定义(库不解释);保留码(1005"无码"、1006"异常关闭"、1015"TLS 握手失败")只作 API 表达、禁止上线——它们描述"没有 Close 帧的情况",写侧强制拒绝。负载规则:码 2 字节 + 原因(合法 UTF-8、总长 ≤123——XWS_CLOSE_PAYLOAD_MAX)——写侧越界直接失败,读侧拒绝畸形负载。
permessage-deflate:协商层
扩展名 permessage-deflate,参数四个:server_max_window_bits/client_max_window_bits(压缩窗口 9-15——越小越省内存、压缩率略降)、server_no_context_takeover/client_no_context_takeover(消息边界重置字典——省内存、跨消息压缩率降)。协商规则由协商层严格强制:offer 与 response 分开校验、重复参数拒绝、非法 window bits 拒绝、response 不得引入 offer 未提的能力、context takeover 两端一致。应答原则是最小合规子集——只回必要参数(提议 server_max_window_bits=10,采纳就只回这个)。xrtWsDeflateDirection 把协商结果转成单方向运行参数——收发两向参数可以不同。
流式压缩对象
xwsinflater(解压)/xwsdeflater(压缩)是流式对象,与第 29 章解压/92 章解码同姿态:
- 分片推进:输入输出都按片段、不聚合整条消息——大消息内存恒定。
- 回调输出:解出的数据直进调用方缓冲或发送队列。
- 双重上限:解压总量受消息上限与 Inflate 上限双约束——压缩炸弹防御(3 字节输入声称解出 4 GB?上限先拦)。
- no-context-takeover:协商了就在消息边界重置字典。
- 容量规划:
xrtWsDeflaterBound写入前给出上界——SendLimit 预算的输入。 - 失败路径:回调失败/OOM/数据错误有明确 reset/abort/destroy 语义。
RSV1 规则:压缩标志 RSV1 只允许出现在压缩消息的首个数据帧——continuation 不重复、控制帧永不压缩。这是第 95 章"RSV 非零=扩展协商"的兑现。
一条消息的完整旅程(卷九全景)
九个章节的 API 在一条消息里各就各位——这就是"积木结构"的验收形态。
97.3示例
第一个完整程序:关闭负载往返
下面的程序来自 examples/websocket/close/main.c——码与原因的写出/解析往返:
#include <xrt.h> #include <stdio.h> */ /* 写出并解析一个带 UTF-8 原因的 Close 控制帧负载。 */ int main(void) { uint8 Payload[XWS_CLOSE_PAYLOAD_MAX]; xwsclose Close; xbytesview Input; size_t iSize; if ( !xrtWsCloseWrite( XWS_CLOSE_NORMAL, XRT_STR_LITERAL("shutdown"), Payload, sizeof(Payload), &iSize ) ) { return 1; } Input.Data = Payload; Input.Size = iSize; if ( !xrtWsCloseParse(Input, &Close) ) { return 2; } printf( "code=%u reason=%.*s\n", (unsigned)Close.Code, (int)Close.Reason.Size, Close.Reason.Data ); return 0; }
刚才发生了什么。 ① CloseWrite(1000, "shutdown") 产 9 字节负载(码 2 + 原因 8)——写侧三重强制在入口:原因 UTF-8 合法、总长 ≤123、码不在保留区间。② CloseParse 往返解析出 xwsclose{Code, Reason 视图}——码 1000 语义"正常关闭",原因给人看(日志/审计)。③ 保留码实验:试图写 1005/1006/1015 会直接失败——它们是 API 层的"表达性码"(Stream 的 CloseInfo 用 1006 表示"异常关闭没收到 Close"),不是可发送的值。这条"写侧拒绝"挡住了实现者最常犯的协议错误。④ Stream 层(第 96 章)的 Close 内部就是这对函数——层间关系:本章是负载层,95 章是连接层。
第二个完整程序:压缩协商应答
第二个程序来自 examples/websocket/deflate/main.c——offer 到 response 的最小合规应答:
#include <stdio.h> #include <xrt.h> */ /* 展示解析客户端 offer 并构造最小合规服务端响应。 */ int main(void) { xstrview Text = XRT_STR_LITERAL( "permessage-deflate; server_max_window_bits=10; " "client_max_window_bits" ); xwsextension Extension; xwsdeflate Offer; xwsdeflate Response; char Output[XWS_DEFLATE_MAX_SIZE + 1u]; size_t iOffset = 0; size_t iSize; if ( xrtWsExtensionNext( Text, &iOffset, &Extension ) != XHTTP_NEXT_ITEM ) { return 1; } if ( !xrtWsDeflateOfferParse( &Extension, &Offer ) || !xrtWsDeflateAccept( &Offer, &Response ) || !xrtWsDeflateResponseWrite( &Response, Output, XWS_DEFLATE_MAX_SIZE, &iSize ) ) { return 2; } Output[iSize] = '\0'; printf("response=%s\n", Output); return 0; }
刚才发生了什么。 ① offer 提议 server_max_window_bits=10(服务端压缩窗口 1KB)并通知客户端可带 client_max_window_bits。② 应答采纳 10 并按最小合规子集回写——只回必要参数:permessage-deflate; server_max_window_bits=10,不回显 client 侧的"能力通知"。为什么最小:response 回显的每个参数都是承诺——回了 client_max_window_bits 就承诺了窗口上限,多回=多承诺=少回旋。协商层的严格校验(重复参数/非法值/未提议能力)全部内建。③ 应答串放进 Sec-WebSocket-Extensions 响应字段——与第 94 章握手应答组合。配套的 deflater/inflater 示例演示流式对象的双向运转,extension_tour 是扩展层的完整巡检。
97.4契约
- 关闭负载:码 2 字节(网络序)+ 可选原因(UTF-8);总长 ≤
XWS_CLOSE_PAYLOAD_MAX(125);写侧强制三规则、读侧拒绝畸形。 - 码语义域:1000-1003 正常族 / 1008-1011 错误族 / 3000-4999 应用自定义 / 1005/1006/1015 保留(仅 API 表达,禁止上线)。
- 关闭握手:Close 唯一发送、对端自动应答、超时强制收尾、CloseInfo 终态快照(95 章连接层保证)。
- 压缩协商:offer/response 分别严格校验;重复/非法/未提议即拒绝;应答最小合规子集;
DeflateDirection转单方向参数。 - 压缩参数:window bits 9-15;context takeover 两端一致;no-takeover 在消息边界重置字典。
- 流式对象:分片推进不聚合;回调输出;解压受消息+Inflate 双重上限(炸弹防御);
DeflaterBound容量规划;明确 reset/abort/destroy。 - RSV1 规则:压缩标志只在首个数据帧;continuation 不重复;控制帧永不压缩。
- 发送路径:
TextCompressed族按需创建 Deflater(95 章);预算含压缩输出上界。 - 裁剪:
WEBSOCKET_CLOSE/_DEFLATE/_DEFLATER/_INFLATER/_EXTENSION独立宏——不压缩的连接零压缩代码。
常见错误
症状:CloseWrite(1006, ...) 直接失败;或塞了 200 字节的"详细说明"当原因——同样失败。
原因:1005/1006/1015 是"表达没有 Close 帧情况"的 API 值,协议禁止上线;负载上限 125 字节(码 2 + 原因 123)。原因超长该走应用层消息,Close 原因只放摘要。
xrtWsCloseWrite(1006, XRT_STR_LITERAL("abnormal"), ...); /* 保留码:拒绝 */ xrtWsCloseWrite(1000, LongParagraph, ...); /* 超限:拒绝 */
xrtWsCloseWrite(1000, XRT_STR_LITERAL("policy: quota"), ...); /* 码用语义域内的值;原因一句话;细节走应用消息或日志 */
症状:自写协商应答把 offer 的参数全部回显——客户端按"服务端承诺了 client_max_window_bits"初始化,实际服务端没实现对应窗口,压缩流错乱。
原因:response 的每个参数都是承诺。最小合规子集是协议的安全姿态——只回你真要执行的。
/* 把 offer 原样回回去 */ response = offer_string; /* 回显了未承诺的能力 */
/* 用协商层应答:内建最小合规子集与全部校验 */ xrtWsDeflateInit(&Offer); parse_offer(Fields, &Offer); accept = deflate_accept(&Offer); /* 只回必要参数 */
症状:恶意对端发 3 字节压缩片段声称解出 4 GB——解压器老老实实产出直到 OOM 被杀。
原因:压缩比是攻击面。消息上限(第 95 章配置)与 Inflate 上限双重约束是协议栈的标准防御——只设其一都有绕路(分片消息绕消息上限、多消息绕 Inflate 上限)。
xrtWsInflaterConfigInit(&Config); /* 不设上限直接建?危险 */ xrtWsInflaterCreate(&Config, NULL); feed_everything(&Infl, PeerData); /* 炸弹直达内存 */
/* 消息上限 + Inflate 上限都显式设置(量级按业务:如 1MB / 8MB) */ MessageConfig.MaxMessage = 1024 * 1024; InflateLimit = 8 * 1024 * 1024; /* 超限即协议错误:回 Close 1009(消息过大)断开 */
编程练习
对 CloseWrite/Parse 做五组实验:1000+短原因、1001+空原因、1005(保留)、原因含非法 UTF-8、130 字节原因——前三组验证写侧强制,后两组验证拒绝路径。验收标准:合法组往返一致;非法组错误信息能区分三类原因。
用 deflater/inflater 示例改造:发送侧把一条 10 KB JSON 压缩成 RSV1 帧(首帧 RSV1、分片、FIN),接收侧解压重组。统计压缩前后字节数与内存峰值(第 6 章)。验收标准:往返原文一致;分片边界在压缩数据中间也正确;内存峰值与消息大小无关(流式)。
在第 96 章回声服务上加齐:压缩协商(offer 最小应答)、压缩收发(RSV1 规则)、关闭协议(正常 Close 1000 与策略拒绝 1008 两路)、消息上限与 Inflate 双上限、Ping/Pong(含背压期间的心跳)。与浏览器 WebSocket 对打(开发者控制台)。验收标准:大消息(1 MB 文本)压缩往返正确;恶意输入(超限消息/畸形帧/保留码 Close)全部按协议拒绝且连接干净关闭;全链零整消息缓冲。
小结
| 知识点 | 速查 |
|---|---|
| 关闭负载 | 码(2B 网络序)+ UTF-8 原因;≤125 字节;写侧三强制、读侧拒畸形 |
| 码语义域 | 1000-1003 正常 / 1008-1011 错误 / 3000-4999 应用 / 1005·1006·1015 保留禁上线 |
| 关闭握手 | 唯一发送、自动应答、超时收尾、CloseInfo 快照 |
| 协商规则 | offer/response 分别严格校验;重复/非法/未提议即拒;最小合规子集应答 |
| 协商参数 | window bits 9-15;context takeover 一致;no-takeover 按消息重置 |
| 流式对象 | inflater/deflater 分片推进、回调输出、明确 reset/abort/destroy |
| 炸弹防御 | 消息上限 + Inflate 上限双重约束;超限回 1009 |
| RSV1 规则 | 压缩标志仅首数据帧;continuation 不重复;控制帧永不压缩 |
| 容量规划 | DeflaterBound 先算上界再进 SendLimit 预算 |
| 全景 | 一条消息的旅程串起 89→93→94→95→96 九章 API |
📖 本章涉及的 API 参考手册:websocket(86 个条目)websocket_stream(45 个条目)http(72 个条目)
点击查看完整签名、参数约束、返回值与错误说明