- 后端
- 网络
- 通信
【免费下载链接】libwebsockets
canonical libwebsockets.org networking library
本文基于 minimal-examples-lowlevel/http-client/minimal-http-client-hugeurl/README.md 及其配套源码,讲解 libwebsockets 中lws-minimal-http-client-hugeurl示例的构建与运行方式、超长 URL 的组织技巧、HTTP 客户端回调链(LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP→LWS_CALLBACK_RECEIVE_CLIENT_HTTP→LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ→LWS_CALLBACK_COMPLETED_CLIENT_HTTP)的工作机理,以及上下文参数(pt_serv_buf_size、fd_limit_per_thread、超时等)的配置依据,帮助读者完整复现这个"用 > 2.5KB 的 URL 访问服务器并接收整页响应"的最小化 HTTP/HTTPS 客户端。
示例定位:它验证什么
minimal-http-client-hugeurl位于 lowlevel(低层 API)风格的 http-client 示例目录下,在 http-client 目录索引 中的官方描述是:
minimal-http-client-hugeurl — Sends a > 2.5KB URL to warmcat.com
与目录里"连接并读取 warmcat.com 首页"的 minimal-http-client 不同,这个示例的核心差异点只有一个:把 URL 的查询串撑大到 2KB 以上(/?fakeparam=<2KB>),验证 libwebsockets 客户端在发送一个远超常规长度的请求行(request line)时,能否正确建立 TLS 连接、把完整 URL 无截断地发出去,并稳定收完整个页面响应。README 中给出的预期行为正是如此:
The application goes to https://warmcat.com/?fakeparam=<2KB> and receives the page data.源码解析:2KB 的 URL 是怎么写进 C 代码的
核心文件 minimal-http-client-hugeurl.c 首先用一个字符串字面量拼接常量构造了这条超长 URI:
static const char * const uri = "/?fakeparam=" "00000000000000000000000000000000000000000000000000" "0000000000000000000000000000000000000000000000000000" ... "3333333333333333333333333333333333333333333333333333" /* 2000 */ ;利用 C 语言"相邻字符串字面量自动拼接"的特性,把 2000 个字符分块写成每行约 50 个字符的形式,并在 500/1000/1500/2000 处标注了位置注释,既保证源码可读,又直观证明查询参数总长度达到 2KB。这个uri随后被原样赋给lws_client_connect_info的path字段:
i.path = uri; i.host = i.address; i.origin = i.address; i.method = "GET"; i.protocol = protocols[0].name; /* "http" */ i.pwsi = &client_wsi; lws_client_connect_via_info(&i);值得注意的是host与origin都指向i.address,而address默认是libwebsockets.org(加-l本地模式时为localhost),即 README 里历史输出中的warmcat.com已被更新为当前域名,示例逻辑不变。
构建与运行
构建
README 给出的构建方式(在示例目录下执行,前提是系统已安装 libwebsockets 开发包):
$ cmake . && make其 CMakeLists.txt 通过find_package(libwebsockets CONFIG REQUIRED)探测已安装的库,并用require_lws_config声明了三个硬性构建前提:
require_lws_config(LWS_ROLE_H1 1 requirements) require_lws_config(LWS_WITH_CLIENT 1 requirements) require_lws_config(LWS_WITH_TLS 1 requirements)即:libwebsockets 必须启用 HTTP/1.x 角色(LWS_ROLE_H1)、客户端支持(LWS_WITH_CLIENT)与 TLS(LWS_WITH_TLS),否则不会生成可执行文件。也可以直接在仓库根目录用cmake . && make编译,该示例会由 lowlevel 示例的 CMake 体系统一纳入构建。
命令行参数
源码定义了四个开关(lws_switches数组 +lws_cmdline_option解析):
| 选项 | 含义 |
|---|---|
-l | 本地模式:连接localhost:7681,并追加LCCSCF_ALLOW_SELFSIGNED允许自签证书 |
--server <addr> | 指定要连接的服务器地址(覆盖默认libwebsockets.org/localhost) |
-p <port> | 指定端口,源码中显式校验必须在 16 位范围(1..65535),越界直接报错退出 |
--help | 打印帮助 |
此外,lws_cmdline_option_handle_builtin还会处理 libwebsockets 内建全局选项(如日志详细度),LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT保证全局 SSL 上下文完成初始化。
运行与预期输出
README 中记录的一次典型运行日志(连接建立 → 收到 26520 字节响应体 → 按块读出 → 事务完成):
$ ./lws-minimal-http-client [2018/03/04 14:43:20:8562] USER: LWS minimal http client hugeurl [2018/03/04 14:43:20:8571] NOTICE: Creating Vhost 'default' port -1, 1 protocols, IPv6 on [2018/03/04 14:43:20:8616] NOTICE: created client ssl context for default [2018/03/04 14:43:20:8617] NOTICE: lws_client_connect_2: 0x1814dc0: address warmcat.com [2018/03/04 14:43:21:1496] NOTICE: lws_client_connect_2: 0x1814dc0: address warmcat.com [2018/03/04 14:43:22:0154] NOTICE: lws_client_interpret_server_handshake: incoming content length 26520 [2018/03/04 14:43:22:0154] NOTICE: lws_client_interpret_server_handshake: client connection up [2018/03/04 14:43:22:0169] USER: RECEIVE_CLIENT_HTTP_READ: read 1024 ...(按 ~1KB 的块持续读出,最后一块 read 974) [2018/03/04 14:43:22:3022] NOTICE: lws_http_client_read: transaction completed says -1 [2018/03/04 14:43:23:3042] USER: Completed几个可以对照日志理解的要点:
port -1表示CONTEXT_PORT_NO_LISTEN——纯客户端进程,不监听任何端口;- 响应以约 1024 字节的粒度多次触发
RECEIVE_CLIENT_HTTP_READ(块大小由下文pt_serv_buf_size与 TLS 记录边界共同决定),最后一次读出 974 字节后,lws_http_client_read返回完成信号(日志中transaction completed says -1,见 client-http.c 中lws_http_transaction_completed_client的判定路径); - 程序退出码由
status != 200决定:bad变量在COMPLETED/CLOSED回调里按最终 HTTP 状态码赋值,main返回它,因此该示例天然带"是否 200"的成功判定。
回调链:HTTP 客户端必须"手动抽干"接收数据
示例的协议回调callback_http处理了 HTTP 客户端的四个关键 reason,这也是 lws-callbacks.h 文档化的标准用法:
1. 连接建立:捕获响应状态码
case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: status = (int)lws_http_client_http_response(wsi); lwsl_user("Connected with server response: %d\n", status); break;lws-client.h 特别强调:lws_http_client_http_response()必须在LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP中捕获状态码,因为"之后再存储相关头部的内存会被释放,这个值就会丢失"。
2. 收到未解释数据:必须调用 lws_http_client_read
/* uninterpreted http content */ case LWS_CALLBACK_RECEIVE_CLIENT_HTTP: { char buffer[1024 + LWS_PRE]; char *px = buffer + LWS_PRE; int lenx = sizeof(buffer) - LWS_PRE; if (lws_http_client_read(wsi, &px, &lenx) < 0) return -1; } return 0; /* don't passthru */LWS_CALLBACK_RECEIVE_CLIENT_HTTP(= 46)的语义是"通知你有数据到了,但并不会替你读走它"。头文件注释明确警告:如果你在 HTTP 客户端中不处理它、不调用lws_http_client_read(),事件循环会反复被告知"同一批数据可读"而陷入空转。buffer预留了LWS_PRE头部空间(lws 可能向前写入的预区),长度 1024 与日志中每次read 1024正好对应。
3. 解帧后的载荷:RECEIVE_CLIENT_HTTP_READ
/* chunks of chunked content, with header removed */ case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: lwsl_user("RECEIVE_CLIENT_HTTP_READ: read %d\n", (int)len); #if 0 /* enable to dump the html */ ... #endif return 0; /* don't passthru */lws_http_client_read()的契约(见 lws-client.h)是:
- 非 chunked 连接:把最多
len字节的正文直接填入你的缓冲,返回前把len改写为实际填充量; - chunked 连接:线性缓冲里混着 chunk 头,无法整块交出,因此该函数会回调
LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ,in指向 chunk 起点、len为 chunk 长度,有多少 chunk(或部分 chunk)就回调多少次——这正是"chunked 帧无法一步处理完"的原因,也是本例日志里大量read 1024行的来源。
从源码结构看,这个抽干过程实现在 client-http.c:每次读走n字节后,若rx_chunked且chunk_remaining归零则推进 chunk 状态机;若已知Content-Length,则递减rx_content_remain,剩余量耗尽且content_length_given为真时走到completed:标签调用lws_http_transaction_completed_client(wsi),产生日志里的transaction completed。源码中还保留了#if 0注释的"逐字符 dump 页面 HTML"代码块(可打印 ASCII、非 ASCII 打印为.),把注释改回即可看到完整页面内容——这是 README 里"receives the page data"说法对应的可验证手段。
4. 事务收尾:退出事件循环
case LWS_CALLBACK_COMPLETED_CLIENT_HTTP: client_wsi = NULL; bad = status != 200; lws_cancel_service(lws_get_context(wsi)); /* abort poll wait */ break; case LWS_CALLBACK_CLOSED_CLIENT_HTTP: client_wsi = NULL; bad = status != 200; lws_cancel_service(lws_get_context(wsi)); /* abort poll wait */ break;LWS_CALLBACK_COMPLETED_CLIENT_HTTP(= 47)表示客户端事务完成;当前版本下它与"关闭"等价,因为客户端侧尚不支持事务管线化(头文件注释原话)。两个回调都置空client_wsi并调用lws_cancel_service()打断阻塞中的lws_service()等待——main的循环条件while (n >= 0 && client_wsi)随即退出,程序进入lws_context_destroy()收尾并打印Completed: OK/failed。
上下文与连接参数:为什么这么配
main()里的上下文配置是本示例最值得学习的部分:
lws_context_info_defaults(&info, NULL); lws_cmdline_option_handle_builtin(argc, argv, &info); info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */ info.protocols = protocols; info.pt_serv_buf_size = 8192; info.timeout_secs = 10; info.connect_timeout_secs = 30; info.fd_limit_per_thread = LWS_FD_LIMIT_PER_THREAD_MIN + 1;逐项含义:
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT:全局一次性初始化 TLS 库,后续客户端连接复用该全局状态;CONTEXT_PORT_NO_LISTEN:纯客户端,日志中port -1即来源于此;pt_serv_buf_size = 8192:每个 service 循环的接收缓冲尺寸,直接决定单次lws_http_client_read最多能抽走的数据量(示例实际取 1024 的子缓冲);timeout_secs = 10/connect_timeout_secs = 30:分别为连接建立后与建连阶段的超时控制,避免异常网络下进程永久挂起;fd_limit_per_thread = LWS_FD_LIMIT_PER_THREAD_MIN + 1:这是针对"单客户端连接"进程的关键调优。lws-context-vhost.h 解释:默认 fd 表要按ulimit -n规模分配,而本例永远只有一个客户端 wsi/fd,告诉 lws 按LWS_FD_LIMIT_PER_THREAD_MIN(内部保留 + 8 的连接开销)再加 1 个连接来分配即可,显著降低内存占用;- mbedTLS/WolfSSL 特例:OpenSSL 走系统信任库,而 mbedTLS 必须显式告知信任哪个 CA,因此源码在
LWS_WITH_MBEDTLS或USE_WOLFSSL下设置info.client_ssl_ca_filepath = "./warmcat.com.cer",对应随附的 warmcat.com.cer 文件。
连接信息侧则只需标记i.ssl_connection = LCCSCF_USE_SSL(本地-l模式再或上LCCSCF_ALLOW_SELFSIGNED),协议名取protocols[0].name(即"http"),pwsi回传client_wsi供主循环判断生命周期。
测试体系:默认不访问公网
示例的 CMakeLists.txt 同时把该二进制接入了 ctest 回归:它会先用ctest-background.sh后台拉起仓库自带的test-server(固定随机端口,FIXTURES_SETUP hc_huge_srv),再运行两条用例:
add_test(NAME http-client-hugeurl-warmcat COMMAND lws-minimal-http-client-hugeurl -l --server ${LWS_CTEST_SERVER_RESOLVE} -p ${PORT_HC_HUGE_SRV}) add_test(NAME http-client-hugeurl-warmcat-h1 COMMAND lws-minimal-http-client-hugeurl -l --server ${LWS_CTEST_SERVER_RESOLVE} -p ${PORT_HC_HUGE_SRV} --h1)即:-l走本地自签服务验证 2KB+ URL 的收发正确性,--h1用例则强制 HTTP/1.x 角色再验证一次;测试用例工作目录设为示例自身所在目录(保证能找到warmcat.com.cer),超时 60 秒,并在并行 CI(SAI_OVN)下通过sai_resource申请连接资源租约。从这条测试命令还能确认--h1是lws_cmdline_option_handle_builtin提供的内建选项之一。
小结
minimal-http-client-hugeurl用一个"超长 URL"的压力点,展示了 libwebsockets HTTP 客户端的完整最小闭环:
- 用字符串字面量拼接出 2KB 级
path,交给lws_client_connect_via_info()发起 TLS GET; - 在
ESTABLISHED_CLIENT_HTTP捕获响应码、在RECEIVE_CLIENT_HTTP里用lws_http_client_read()持续抽干数据、在RECEIVE_CLIENT_HTTP_READ里拿到已剥除 chunk 帧的载荷、在COMPLETED/CLOSED_CLIENT_HTTP里判定成败并用lws_cancel_service()结束服务循环; - 上下文按单连接客户端调优(
CONTEXT_PORT_NO_LISTEN、pt_serv_buf_size、fd_limit_per_thread = LWS_FD_LIMIT_PER_THREAD_MIN + 1、双超时),TLS 信任链按所选 TLS 库区分处理(系统信任库 vs 显式client_ssl_ca_filepath); - 构建上要求
LWS_ROLE_H1+LWS_WITH_CLIENT+LWS_WITH_TLS,ctest 用例默认通过本地test-server后台服务完成验证,不依赖公网。
掌握这个示例后,把它当作模板替换uri、address/port、回调中的载荷处理,即可快速搭建任意 HTTPS 拉取型客户端。
- 后端
- 网络
- 通信
【免费下载链接】libwebsockets
canonical libwebsockets.org networking library
相关推荐
TEN-framework 仓库中的 libwebsockets HTTP 客户端实践:深入解析 minimal-http-client-hugeurl 示例
TEN framework 仓库中的 libwebsockets HTTP 客户端实践:深入解析 minimal http client hugeurl 示例
人工智能AI Agent多模态语音AI 应用libwebsockets minimal-http-client-multi 深度解析:8 路并发 HTTP 客户端与连接复用实战
libwebsockets minimal http client multi 深度解析:8 路并发 HTTP 客户端与连接复用实战 导读 minimal ht
人工智能AI Agent多模态语音AI 应用libwebsockets HTTP/2 客户端接收流控实战:minimal-http-client-h2-rxflow 示例全解析
libwebsockets HTTP/2 客户端接收流控实战:minimal http client h2 rxflow 示例全解析 本篇基于 libwebso
后端网络通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考