- 后端
- 网络
- 通信
【免费下载链接】libwebsockets
canonical libwebsockets.org networking library
导读
本文以 libwebsockets 仓库中的自测程序lws-api-test-http-compression(README.md)为主线,完整剖析 lws 服务端 HTTP 流式压缩(deflate与可选br)的协商、编码、传输与端到端校验全过程。读者将理解accept-encoding协商规则、lws 仅压缩文本类 MIME 的判定逻辑、zlib/brotli 底层实现细节,并掌握该测试程序的构建、运行与 ctest 集成方式。
测试程序定位:单进程端到端完整性验证
lws-api-test-http-compression是一个单进程自测程序,核心价值在于验证"HTTP 流式压缩能否完整无损地传输文件"。其设计思路(见 main.c 头部注释)非常清晰:
- 文件挂载(file mount)在本地生成一个多 MB 级文本文件,文件名就是文件内容的sha256 十六进制摘要,例如
/3a7bd3e2...c1f0.txt; - 客户端随后分别通过h1和h2c(prior knowledge,无 TLS)抓取该文件,并携带多种
accept-encoding请求头; - 客户端用zlib 或 brotli解码收到的响应体,再对比"解码后长度"与"sha256 摘要"是否与文件名一致;
- 只要压缩传输发生任何损坏、截断或分帧错误,都会表现为摘要不匹配,而不会"静默成功"。
正因为文件名与内容摘要强绑定,该程序把"服务端压缩是否正确"这一行为转化为可自动判定的布尔结果,是一种极其可靠的压缩正确性验证手段。
测试矩阵:accept-encoding 的协商预期
原文档给出了核心测试矩阵,程序实际执行的用例(main.c 中的cases[]表)比文档表格更完整,此处合并呈现:
| 请求(accept-encoding) | 期望响应(content-encoding) | 说明 |
|---|---|---|
| 不发送(h1/h2) | identity(无该头) | 不协商压缩 |
gzip | identity(lws 对普通文件不提供 gzip) | 见下文 gzip 说明 |
deflate | content-encoding: deflate(raw deflate 流) | zlib 格式(RFC 1950) |
deflate;q=0 | identity | q=0 表示"不要这种编码",且编码按完整 token 匹配 |
xdeflate | identity | 非标准名称不匹配 |
gzip, deflate;q=0.5 | content-encoding: deflate | 多编码列表按 q 值协商 |
br(仅LWS_WITH_HTTP_BROTLI构建时) | content-encoding: br | 只在编译期启用 brotli 时加入用例 |
gzip(h2) | identity | 同上,h2 路径重复验证 |
注意表格中的关键事实:lws 对普通文件不主动提供 gzip 编码。原因可从服务端源码验证:lws 的可用压缩方法表lcs_available[](stream.c)只包含lcs_brotli与lcs_deflate两项;而 gzip 仅在文件本身以.gz预压缩形式存储、可被直接转发时才会以content-encoding: gzip提供(见 server.c 的LWS_FOP_FLAG_COMPR_IS_GZIP分支)。这与测试矩阵中gzip期望 identity 的行为完全一致。
测试文件生成:sha256 命名与"有真实工作可做"的内容
程序启动时(generate_file())做如下事情:
- 在
--tmpdir下用mkdtemp创建临时目录; - 用xorshift32伪随机数(种子
0x12345678)生成十六进制数字流,并交错插入重复单词(lorem/ipsum/dolor/sit/amet/...等 12 个词,按随机值选取); - 边写文件边用
lws_genhash_update累积计算sha256; - 写完将文件重命名为
<sha256hex>.txt。
这种内容构造大有讲究:纯随机数据不可压缩,纯重复文本会被压成极小体量,而"伪随机 hex 流 + 重复词交错"既给了压缩器真实的工作量,又不会让文件塌缩,从而能真正检验压缩器在中等压缩比下的正确性。文件是text/plain,因为lws 只压缩"文本类" MIME(见下文)。
服务端与客户端结构:一个进程、三个 vhost
main()(main.c)创建了:
- 一个主 context,
port = CONTEXT_PORT_NO_LISTEN(不监听)、LWS_SERVER_OPTION_EXPLICIT_VHOSTS; - srv-h1 vhost:监听
-p指定的端口(默认 7681),挂载 file mount,协议仅lws_callback_http_dummy; - srv-h2 vhost:监听
--h2-port指定端口(默认 7682),设置LWS_SERVER_OPTION_H2_PRIOR_KNOWLEDGE(仅LWS_WITH_HTTP2构建时创建); - cli vhost:不监听,注册客户端协议
http-comp,负责发起所有抓取请求。
文件挂载mount(main.c)是经典的LWSMPRO_FILE类型,origin指向临时目录,mountpoint = "/"。客户端通过lws_client_connect_via_info逐 case 发起连接:h1 用例走port_h1,h2 用例走port_h2且ssl_connection = LCCSCF_H2_PRIOR_KNOWLEDGE(h2c 明文 prior knowledge)。
压缩协商的底层实现
accept-encoding 的解析与 accept mask
服务端在响应阶段调用lws_http_compression_validate()(stream.c)解析请求头WSI_TOKEN_HTTP_ACCEPT_ENCODING:用lws_tokenize按 RFC 7230 分隔符切分编码列表,逐个 token 与lcs_available[]中的编码名做整 token 不区分大小写匹配,并识别;q=0参数——q 值为 0 时将该编码标记为"拒绝"。结果写入wsi->http.comp_accept_mask位掩码,后续lws_http_compression_apply()只有在掩码对应位被置位(或本就是服务端主动解码)时才允许选用该压缩方法。这解释了测试矩阵中deflate;q=0期望 identity、xdeflate期望 identity 的行为。
压缩方法注册表:deflate 与 brotli
所有压缩方法通过统一的struct lws_compression_support接口(private-lib-roles-http-compression.h)暴露encoding_name、init_compression、process、destroy四个成员:
- deflate(deflate.c):实现采用zlib 格式(RFC 9110 8.4.1.2 / RFC 1950),即
windowBits 15,而非 raw deflate(-15)——源码注释明确指出 raw deflate 只有部分客户端能处理。压缩参数为deflateInit2(level=1, Z_DEFLATED, 15, memLevel=8, Z_DEFAULT_STRATEGY),级别取 1 优先保证吞吐;用Z_SYNC_FLUSH维持流式输出,最终帧用Z_FINISH收尾。测试客户端一侧则用inflateInit2(&zs, 15)与之一致地解码; - brotli(brotli.c):
BROTLI_PARAM_MODE设为BROTLI_MODE_TEXT(文本优化模式),BROTLI_PARAM_QUALITY设为BROTLI_MIN_QUALITY(最小质量档);编码时区分BROTLI_OPERATION_PROCESS与BROTLI_OPERATION_FINISH,解码时对BROTLI_DECODER_RESULT_ERROR记录错误码与错误字符串。
变换管线:buflist_comp 与帧边界管理
压缩变换与 h1/h2 传输层解耦(compression/README.md):它新增一类特殊 buflistwsi->buflist_comp,暂存"已交付但压缩器暂未消化"的输入。lws_http_compression_transform()(stream.c)的核心逻辑包括:
- 只在
LWS_WRITE_HTTP/LWS_WRITE_HTTP_FINAL写类型上做变换; - 收到 FINAL 输入后先降级为非 FINAL,等压缩器消化完缓冲区再用一次无输入调用触发真正收尾,并把最终输出帧升级回
LWS_WRITE_HTTP_FINAL; - 输入被压缩器部分消费时,剩余部分追加到
buflist_comp尾部,并触发lws_callback_on_writable继续处理; - 压缩器表示可能还有更多输出(
may_have_more)时同样回调可写,确保多帧输出不被截断。
什么内容才被压缩:文本 MIME 与 206 例外
服务端发响应时(server.c)只有满足全部条件才调用lws_http_compression_apply():
- 不是 CGI 解释输出(
!wsi->interpreting); - 不是 206 Partial Content(开启
LWS_WITH_RANGES时!ranges,因为 Content-Range/multipart part 头描述的是字节偏移,套上编码会使其变成"编码后字节"的偏移,违反 RFC 9110 14.4); content_type以text/开头,或等于application/javascript、image/svg+xml。
这正是文档所述"lws 只压缩文本类 MIME"的代码级依据——测试文件刻意使用text/plain就是为了命中该分支。压缩应用成功后,content-encoding头与响应头一起被写入(若头部空间不够则放弃压缩,主体按原样发送,保证协议自洽)。
客户端解码与校验:解码、计数、摘要三管齐下
每个测试用例由struct conn(main.c)承载状态,包含lws_genhash_ctx摘要上下文、z_stream或BrotliDecoderState、收发字节计数、content-encoding记录等。客户端回调callback_cli的关键路径:
LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:用lws_add_http_header_by_token附加WSI_TOKEN_HTTP_ACCEPT_ENCODING(若该用例配置了 accept 值);LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:记录状态码与响应content-encoding;LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:把收到的每个 body 分片送入conn_rx()解码;LWS_CALLBACK_COMPLETED_CLIENT_HTTP/LWS_CALLBACK_CLOSED_CLIENT_HTTP:收尾判定。
conn_rx()(main.c)按解码器类型分流:identity 直接进摘要;deflate 用inflate()(接受Z_OK/Z_STREAM_END/Z_BUF_ERROR,出现Z_STREAM_END置stream_ended);brotli 用BrotliDecoderDecompressStream循环驱动直到BROTLI_DECODER_RESULT_SUCCESS。所有解码输出都同步喂给 sha256 摘要。
conn_finish()(main.c)做最终判定,任何一项失败都计为 failure:
- 连接是否在完整结束前关闭;
- HTTP 状态是否 200;
content-encoding是否与用例预期一致(预期有则必须出现且相等,预期无则必须不出现);- 压缩流是否干净结束(
stream_ended); - 解码后字节数是否等于
file_size; - 解码流的 sha256 是否与文件名中的摘要一致。
每个用例结束打印PASS/FAIL及"线上字节数 vs 解码字节数"(例如h1 accept deflate: PASS (x bytes on the wire, y decoded)),通过对比可直观看到压缩节省的带宽。
构建要求与依赖
原文档的构建前提(README.md)与 CMakeLists.txt 完全对应:
LWS_WITH_HTTP_STREAM_COMPRESSION:必选,开启服务端流式压缩(顶层 CMakeLists.txt 默认 OFF,开启后强制启用 zlib 支持);- 同时具备 server 与 client 构建:
require_lws_config检查LWS_ROLE_H1、LWS_WITH_NETWORK、LWS_WITH_CLIENT且不允许LWS_WITHOUT_SERVER; LWS_WITH_GENCRYPTO:必选,用于lws_genhash_*计算 sha256 摘要;- 链接zlib(
find_package(ZLIB REQUIRED));当 lws 以LWS_WITH_HTTP_BROTLI构建时额外链接brotlidec(brotli/decode.h 同库族)。
文档注释点明了链接 zlib/brotlidec 的原因:客户端以"浏览器同款方式"解码服务端压缩流,因此测试程序直接复用了 lws 压缩时所用的同一批底层解码库。
构建与运行(在已构建好上述特性的 lws 环境下):
$ cmake . && make $ ./lws-api-test-http-compression -p 7681 --h2-port 7682 [--size bytes] [--tmpdir dir] [-d 1039]命令行参数与退出码
| 参数 | 默认值 | 说明 |
|---|---|---|
-p | 7681 | h1 服务端口 |
--h2-port | 7682 | h2c 服务端口(仅LWS_WITH_HTTP2时启用) |
--size | 3 MiB(3 × 1024 × 1024 字节) | 生成文件大小,可用atol指定字节数 |
--tmpdir | 当前目录 | 临时文件目录;ctest 会传入其构建目录 |
-d | 1039 | 日志级别(LLL_USER|LLL_ERR|LLL_WARN|LLL_NOTICE) |
生成文件位于--tmpdir下新建的临时子目录,进程退出时被清理(unlink文件 +rmdir目录)。退出码只有 0 表示全部用例通过,任一用例失败则返回 1;另有 90 秒看门狗定时器,超时也会判失败。此外还安装了 SIGINT 处理器支持手动中断。
ctest 集成:两个互补用例
CMakeLists.txt 注册了两个测试:
api-test-http-compression:默认 3 MiB 文件,覆盖全部协商矩阵;lws_get_free_port动态分配 h1/h2 端口,TIMEOUT 120;api-test-http-compression-empty:--size 0生成空文件——此时压缩器没有实质输入,但"流结束"仍必须正常终结 body(h1 上体现为最后一个 chunk)。该用例专门验证空 body 场景下压缩管线的收尾正确性。
两者共享WORKING_DIRECTORY为源码目录、--tmpdir指向构建目录,保证产物位置一致。
小结
lws-api-test-http-compression通过"sha256 命名文件 + 浏览器同款解码 + 摘要/长度双校验"的闭环设计,把 HTTP 流式压缩的正确性验证做到了可自动判定、零误报的程度。结合 stream.c、deflate.c、brotli.c 等实现源码,可以完整看到 lws 在编码协商(accept mask)、文本 MIME 过滤、206 例外、buflist 缓冲与帧收尾上的工程细节。无论你是要为自己的 lws 服务端启用LWS_WITH_HTTP_STREAM_COMPRESSION,还是要排查压缩传输的正确性,这个自测程序都是最直接的参考与验证工具。
- 后端
- 网络
- 通信
【免费下载链接】libwebsockets
canonical libwebsockets.org networking library
相关推荐
TEN-framework 内置 libwebsockets 的 HTTP 流式压缩模块:deflate 与 brotli 变换实现解析
TEN framework 内置 libwebsockets 的 HTTP 流式压缩模块:deflate 与 brotli 变换实现解析 本篇文章聚焦于 TEN
人工智能AI Agent多模态语音AI 应用深入解析 libwebsockets HTTP 流压缩:deflate/br 服务端变换、Accept-Encoding 协商与 buflist 背压机制
深入解析 libwebsockets HTTP 流压缩:deflate/br 服务端变换、Accept Encoding 协商与 buflist 背压机制 li
后端网络通信libwebsockets 自定义 HTTP 响应头实战:minimal-http-server-custom-headers 示例深度解析
libwebsockets 自定义 HTTP 响应头实战:minimal http server custom headers 示例深度解析 导读 本篇文章以
人工智能AI Agent多模态语音AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考