- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
libwebsockets(lws)为 HTTP/2 提供了一种名为 "immortal stream"(不朽流)的长轮询能力,让服务器可以向已半关闭的客户端流持续单向推送数据,且不受常规空闲超时约束。本文以 third_party/libwebsockets/READMEs/README.h2-long-poll.md 为核心,结合仓库内的源码实现与配套示例,完整讲解服务端如何开启 h2 long poll、客户端如何切换为只读长轮询模式,以及底层超时豁免机制的工作原理,读完即可在 lws 服务端/客户端中落地这一"单向实时推送"方案。
h2 long poll 与 immortal stream:核心概念
在标准 HTTP/2 语义中,流由客户端发起,任意一方发送带有END_STREAM标志的帧后,该方向即进入 half-closed 状态,最终流会关闭并释放资源。而 lws 的 h2 long poll 通过引入immortal stream概念,让部分流"超脱"常规超时管理:
- 服务端和客户端都可以承载 "immortal" 流,这些流不受常规超时约束,可以长时间存活;
- 这些流对客户端而言是只读的(read-only to the client),即数据方向固定为 server -> client,服务端可随时向流内写入数据,客户端只需持续接收;
- 只要某条网络连接上至少存在一条 immortal 流,整条连接本身也不受超时影响,直到它所承载的最后一条 immortal 流关闭为止。
需要注意的是,正因为连接与流都不会因空闲而被超时回收,文档特别建议:应当有另外的机制来确认客户端仍然存活(例如应用层心跳、客户端定时重连或业务层面的活动上报),否则可能出现服务端长期维持一条早已无人的僵尸连接。
这一机制与仓库中另一处面向 SSE 的lws_http_mark_sse()(见 lws-http.h)共享同一套底层 "immortal" 标记逻辑,可见 lws 将"连接上存在需要长期存活的流"统一抽象为不朽流来管理超时。
服务端开启 h2 long poll
允许客户端接入 immortal 流的服务端 vhost,必须在vhost 创建时设置选项标志LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL。该标志定义于 lws-context-vhost.h:
#define LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL (1ll << 32)在lws_context_creation_info的info.options中直接置位即可,例如仓库示例 minimal-http-server-h2-long-poll/minimal-http-server.c 的做法:
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL | LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;JSON 配置等价写法
如果通过 JSON 配置文件创建 vhost(lws 的 lejp-conf 解析路径),等价配置是在对应 vhost 上设置:
"h2-half-closed-long-poll": "1"该 JSON 键vhosts[].h2-half-closed-long-poll被注册在 lejp-conf.c 的配置路径表中,解析时经LEJPVP_FLAG_H2_HALF_CLOSED_LONG_POLL分支(lejp-conf.c)调用set_reset_flag置位LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL。因此对纯 JSON 配置驱动的部署,只需这一行即可,无需改动代码。
服务端底层行为
开启该选项后,当服务端 vhost 收到客户端发来的 HEADERS 帧时,h2 解析器会在 http2.c 中检查该选项:
if (lws_check_opt(h2n->swsi->a.vhost->options, LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL)) { /* * We don't directly timeout streams that enter the * half-closed remote state, allowing immortal long * poll */ lws_mux_mark_immortal(h2n->swsi); lwsl_info("%s: %s: h2 stream entering long poll\n", __func__, lws_wsi_tag(h2n->swsi)); } else { h2n->swsi->h2.END_STREAM = !!(h2n->flags & LWS_H2_FLAG_END_STREAM); ... }也就是说:开启选项后,凡是进入 half-closed remote 状态的 h2 流都会被直接标记为 immortal,不再按常规 END_STREAM 语义关闭与超时。这正是"服务端只需要设置一个选项,其余全部交给 lws"的根本原因。
客户端流切换为 long poll 模式
客户端侧通过一个专用 API 让已建立的 h2 客户端流转入 immortal 只读模式:
int lws_h2_client_stream_long_poll_rxonly(struct lws *wsi);该 API 的完整契约声明见 lws-http.h:它会向服务端发送一个带END_STREAM标志的零长度 DATA 帧,将本地流置为 half-closed (local)、对端置为 half-closed (remote),同时把客户端流标记为 immortal(不受超时约束),从而进入"可以无限期等待接收数据"的只读状态。返回 0 表示异步切换流程已启动;若当前 wsi 不是 h2 流则返回非零。
底层实现路径
调用入口在 http2.c:
int lws_h2_client_stream_long_poll_rxonly(struct lws *wsi) { if (!wsi->mux_substream) return 1; /* * Elect to send an empty DATA with END_STREAM, to force the stream * into HALF_CLOSED LOCAL */ wsi->h2.long_poll = 1; wsi->h2.send_END_STREAM = 1; lws_callback_on_writable(wsi); return 0; }它并不立即发送数据,而是设置h2.long_poll与h2.send_END_STREAM两个标志(字段定义见 private-lib-roles-h2.h)并请求可写回调。真正发送发生在 ops-h2.c 的 POLLOUT 处理中:
if (w->h2.send_END_STREAM && w->h2.long_poll) { uint8_t buf[LWS_PRE + 1]; enum lws_write_protocol wp = 0; if (!rops_write_role_protocol_h2(w, buf + LWS_PRE, 0, &wp)) { lwsl_info("%s: %s: entering ro long poll\n", __func__, lws_wsi_tag(w)); lws_mux_mark_immortal(w); } else lwsl_err("%s: %s: failed to set long poll\n", __func__, lws_wsi_tag(w)); goto next_child; }注意这里以零长度载荷调用 h2 写角色协议,实际产出即文档所述的"零长度 DATA + END_STREAM"帧,成功后随即调用lws_mux_mark_immortal()。此外,在 ops-h2.c 的写角色入口中,immortal 流(mux_stream_immortal为真)被豁免于普通写状态检查,保证半关闭后的流仍可继续写数据。
超时豁免的底层机制:lws_mux_mark_immortal
immortal 状态的核心实现在 wsi.c:
void lws_mux_mark_immortal(struct lws *wsi) { struct lws *nwsi; lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0); if (!wsi->mux_substream && !wsi->client_mux_substream) { lwsl_wsi_err(wsi, "not mux substream"); return; } if (wsi->mux_stream_immortal) /* only need to handle it once per child wsi */ return; nwsi = lws_get_network_wsi(wsi); if (!nwsi) return; wsi->mux_stream_immortal = 1; assert(nwsi->immortal_substream_count < 255); nwsi->immortal_substream_count++; if (nwsi->immortal_substream_count == 1) lws_set_timeout(nwsi, NO_PENDING_TIMEOUT, 0); }要点拆解:
- 流自身豁免:
lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0)直接取消子流上的待处理超时; - 连接级豁免:网络连接 wsi 上维护
immortal_substream_count计数(上限 255),标记某条流 immortal 时计数加一;当计数从 0 变为 1 时,连接本身的超时也被取消。因此"连接包含至少一条 immortal 流时不超时"这一规则由该计数精确实现; - 幂等保护:
mux_stream_immortal标志保证同一子流只处理一次,避免重复计数; - 常规超时恢复:在 http2.c 中,网络 wsi 的 keepalive 超时仅在
immortal_substream_count == 0时才会被设置:
if (!wsi->immortal_substream_count) lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE, wsi->a.vhost->keepalive_timeout ? wsi->a.vhost->keepalive_timeout : 31);即默认空闲连接约 31 秒(或 vhost 配置的keepalive_timeout)就会被关闭,但只要存在不朽流,连接就一直存活——示例程序中"每 60 秒推送一次时间戳、连接却永不超时"正是靠这条逻辑成立的。
实战验证:示例应用跑通 long poll
仓库内置了配套的完整示例,可直接验证整个流程。
服务端:minimal-http-server-h2-long-poll
示例位于 third_party/libwebsockets/minimal-examples/http-server/minimal-http-server-h2-long-poll,其构建与运行方式见该目录下的 README.md:
$ cmake . && make $ ./lws-minimal-http-server运行后服务端监听7681 端口(TLS 构建时使用目录内自带的localhost-100y.cert/localhost-100y.key自签名证书)。其核心逻辑在 minimal-http-server.c:
LWS_CALLBACK_HTTP中先输出 HTTP 200 响应头(LWS_ILLEGAL_HTTP_CONTENT_LEN,即不带 Content-Length 的流式响应),随后立即调度定时器;- 通过
lws_sul_schedule每60 秒(60 * LWS_US_PER_SEC,见第 49-50 行)触发一次sul_cb,置位pss->pending并调用lws_callback_on_writable,在LWS_CALLBACK_HTTP_WRITEABLE中把当前时间戳lws_now_usecs()写入流(第 85-95 行)。60 秒远大于默认的约 30 秒空闲回收阈值,因此连接能持续存活本身就是 immortal 生效的直接证据; - 示例同时提供了
-v参数(将默认的 5m/5m10s 有效性检查收紧为 5s/10s,见第 141-144 行与retry结构),用于更激进地验证超时豁免。
客户端:minimal-http-client
在另一个终端构建常规客户端示例并带参运行:
$ cmake . && make # 在 minimal-examples/http-client/minimal-http-client 目录 $ ./lws-minimal-http-client -l --long-poll参数含义(见 minimal-http-client.c):
-l:连接本地localhost:7681,并允许自签名证书(LCCSCF_ALLOW_SELFSIGNED,第 245-248 行);--long-poll:置位long_poll标志(第 238-241 行),要求使用 h2;- 连接建立时 ALPN 为
"h2,http/1.1"(第 261 行),并设置LCCSCF_H2_QUIRK_NGHTTP2_END_STREAM等 h2 兼容选项(第 257-259 行)。
客户端在LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP回调中执行切换(第 100-105 行):
if (long_poll) { lwsl_user("%s: Client entering long poll mode\n", __func__); lws_h2_client_stream_long_poll_rxonly(wsi); }随后在LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ中打印收到的长轮询数据(第 144-151 行,long poll rx: ...)。运行后即可观察到:客户端连上服务端、进入 immortal 模式,之后服务端每分钟推送一次时间戳,连接在远超常规超时时间的情况下持续保持在线。
验证要点
- 服务端日志出现
h2 stream entering long poll(http2.c); - 客户端日志出现
Client entering long poll mode与周期性的long poll rx: <长度> '<时间戳>'; - 等待超过默认的 30 秒空闲超时窗口(以及使用
-v收紧后的 5s/10s 窗口),连接仍不断开——这是 immortal 生效的最直接验证。
使用注意事项
- 活性确认:immortal 流与所在连接都不再受超时约束,必须自备应用层心跳或客户端活动上报,避免僵尸连接长期占用资源;
- 单向语义:long poll 流对客户端只读,客户端若要再发数据,需另行建立流或连接,不能复用该半关闭流;
- 服务端必须显式开启:只有 vhost 设置了
LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL(或 JSON 的"h2-half-closed-long-poll": "1"),客户端发起的半关闭流才会被服务端标记为 immortal;否则按标准 h2 语义处理,流可能很快关闭; - 非 h2 流保护:
lws_h2_client_stream_long_poll_rxonly()会检查mux_substream,对非 h2 子流直接返回非零,不会误操作; - 计数上限:单条网络连接的
immortal_substream_count上限为 255(wsi.c),设计长期轮询规模时应留意连接聚合策略。
综上,lws 的 h2 long poll 用"一个服务端选项 + 一个客户端 API"即完成了对 HTTP/2 半关闭流的超时豁免与单向推送改造,配合lws_mux_mark_immortal的流级/连接级两级计数管理,非常适合在 h2 之上实现类似"服务端每分钟(或按事件)推送一次状态、客户端长期挂机等待"的轻量实时场景。相关源码均可在本仓库的 third_party/libwebsockets/lib/roles/h2 与 third_party/libwebsockets/lib/core-net/wsi.c 中继续深入研读。
- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
相关推荐
JavaScript教程:深入理解长轮询(Long Polling)技术
JavaScript教程:深入理解长轮询 Long Polling 技术 什么是长轮询 长轮询 Long Polling 是一种简单而有效的服务器 客户端通信技
PicoClaw VK 渠道实战指南:基于 VK 社区 Bots Long Poll 的机器人接入与配置
PicoClaw VK 渠道实战指南:基于 VK 社区 Bots Long Poll 的机器人接入与配置 PicoClaw 的 VK 渠道通过 VK 的 Bot
人工智能AI 应用AI Agent交互助手工具调用MCP ClientsAgent 记忆libuv FS Poll 句柄(uv_fs_poll_t)完全指南:基于 stat 轮询的跨平台文件变更监控
libuv FS Poll 句柄(uv_fs_poll_t)完全指南:基于 stat 轮询的跨平台文件变更监控 uv_fs_poll_t 是 libuv 提供的
网络通信异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考