- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
导读
lws-minimal-http-server-eventlib-custom是 libwebsockets(lws)官方 minimal-examples 中演示自定义事件库(custom event library)集成的 HTTP 服务器示例,位于本仓库third_party/libwebsockets/minimal-examples/http-server/minimal-http-server-eventlib-custom/。该示例展示了如何让 lws 事件驱动核心直接寄生在应用自己已有的 poll() 循环之上,而不是使用 lws 内置的默认循环或 libuv、libevent 等外部事件库。读完本文,你将掌握自定义事件循环的完整接入流程:poll 描述符表的管理、struct lws_event_loop_ops接口层的实现、foreign_loops与event_lib_custom两个上下文创建参数的配合使用,以及 lws 静态文件挂载与客户端连接在同一循环上的共存方式。
示例定位:为什么需要"自定义事件循环"
libwebsockets 的默认运行方式是自建内部事件循环并接管进程控制流;当需要与 libuv、libevent、libev 等事件库集成时,可通过上下文创建选项切换。但现实中的很多应用早已拥有自研的 poll()/select() 主循环(例如网关、媒体服务器、既有网络框架),此时既不想再引入一个事件库依赖,又希望 lws 的 WebSocket/HTTP 能力融入现有循环。lws 为此提供了"自定义事件库(custom event lib)"机制:
- 它把事件循环抽象为一组可插拔的操作接口(
struct lws_event_loop_ops),定义在 lws-eventlib-exports.h; - 应用只需实现其中少量关键回调,lws 内部的连接建立、读写调度、超时处理就全部经由这些回调驱动,而真正的 poll() 调用权始终留在应用手里。
本示例(minimal-http-server.c)正是这一机制的完整最小实现:仅用约 460 行 C 代码,同时承载了静态文件 HTTP 服务、安全响应头、404 页面,以及一个可选的 HTTPS 客户端连接,全部运行在同一个自定义 poll 循环上。
构建与运行
该示例的构建方式与仓库内其余 lws minimal-examples 一致,其 CMake 配置见 CMakeLists.txt:
$ cmake . && makeCMake 通过find_package(libwebsockets CONFIG REQUIRED)查找 lws 安装,并借助LwsCheckRequirements校验三项配置开关——LWS_ROLE_H1、LWS_WITH_SERVER、LWS_WITH_CLIENT——三者必须全部开启,否则跳过编译(这正是本示例同时包含 server 与 client 逻辑的原因)。非 Windows 平台链接产物为websockets(或共享库websockets_shared)。
编译成功后直接运行:
$ ./lws-minimal-http-server [2018/03/04 09:30:02:7986] USER: LWS minimal http server | visit http://localhost:7681 [2018/03/04 09:30:02:7986] NOTICE: Creating Vhost 'default' port 7681, 1 protocols, IPv6 on然后访问 http://localhost:7681。服务默认从启动目录下的./mount-origin子目录(mount-origin)提供静态文件,其中 index.html 为默认首页;若访问不存在的路径,lws 会依据error_document_404配置返回 404.html。另外,进程启动后还会在同一循环上向warmcat.com:443发起一个 HTTPS GET 客户端连接,用于演示同一自定义循环上 server/client 并存的能力。
程序通过-d <loglevel>命令行选项控制日志冗余度(lws_cmdline_option解析,见 minimal-http-server.c),例如./lws-minimal-http-server -d15可看到更详细的 poll 调度日志。按Ctrl+C(SIGINT)会触发sigint_handler置位interrupted标志,使自定义循环正常退出并销毁 lws 上下文。
核心概念一:把 poll 描述符表交给应用管理
要让 lws 寄生在自定义循环上,应用必须提供一张"外来事件循环"可维护的 fd 表。示例定义了custom_poll_ctx_t(minimal-http-server.c):
#define MAX_CUSTOM_POLLFDS 64 typedef struct custom_poll_ctx { struct lws_pollfd pollfds[MAX_CUSTOM_POLLFDS]; int count_pollfds; } custom_poll_ctx_t;它本质上就是struct pollfd的定长数组(上限 64 个描述符)加一个有效计数,与 POSIXpoll()的参数形式一一对应。全局实例a_cpcx会在创建 lws 上下文时作为外来循环对象传入。围绕这张表,示例实现了四个基础操作,它们与 poll() 语义完全对应:
custom_poll_find_fd():按 fd 在表中线性查找,返回struct lws_pollfd *,是其余操作的内置工具;custom_poll_add_fd():注册新 fd 及其关注事件(events),重复注册或表满(达到MAX_CUSTOM_POLLFDS)时报错返回 1;custom_poll_del_fd():删除 fd,采用"用最后一个元素回填被删槽位"的压缩策略,删后count_pollfds--;custom_poll_change_fd():按"增加/移除"两个事件掩码增量调整 fd 的events:(events & ~events_remove) | events_add。
注意,struct lws_pollfd在LWS_POLLFD平台上与原生struct pollfd布局一致,因此这张表可以直接作为poll()的系统调用参数使用,无需转换。
核心概念二:自定义循环如何驱动 lws
custom_poll_run()(minimal-http-server.c)是本示例真正的"主循环",它以while (!interrupted)持续运行,直到收到 SIGINT。其调度逻辑是理解 lws 事件驱动模型的关键:
n = lws_service_adjust_timeout(context, 5000, 0); /* 让 lws 压缩 poll 超时 */ n = poll(cpcx->pollfds, (nfds_t)cpcx->count_pollfds, n); ... for (n = 0; n < cpcx->count_pollfds; n++) { if (!cpcx->pollfds[n].revents) continue; m = lws_service_fd(context, &cpcx->pollfds[n]); ... }每一步都有明确的职责划分:
- 超时协商:循环不是固定
poll()5000ms,而是先调用lws_service_adjust_timeout(context, 5000, 0)。lws 内部可能注册了比 5 秒更早的定时任务(如 HTTP keep-alive 超时、客户端连接重试),它会把这个参数压缩为"最早待触发事件"的剩余时间,返回给调用方作为本次 poll 的超时上限。这正是 lws 定时器能在外来循环上正常工作的前提——注释明确强调"现有的循环必须与 lws 协商最大等待超时"。 - 事件分发:
poll()返回后,逐个检查每个 fd 的revents,把置位的事件交给lws_service_fd()处理。lws 内部会依据该 fd 上 wsi 的状态(监听 socket 的 accept、HTTP 请求的读写、WebSocket 帧收发等)完成相应推进。 - 槽位回退:
lws_service_fd()的返回值m非零表示该连接已关闭,而关闭时custom_poll_del_fd()会用表尾元素回填当前槽位,因此代码里用if (m && cpcx->pollfds[n].fd != fd) n--;在同一个索引上重试新换入的描述符。m < 0表示 lws 认为发生了异常,示例选择忽略并继续,体现"外层应用可能并不关心"的容错态度。
这一"协商超时 → poll → 逐 fd 分发"的节奏,就是 lws 在任意外来事件循环上工作的标准范式。
核心概念三:lws 事件库操作接口层
自定义 poll 表本身只是"原材料",要让 lws 认识并驱动它,还需要实现事件库抽象层。lws 在 lws-eventlib-exports.h 中定义了struct lws_event_loop_ops,字段包括init_pt、init_vhost_listen_wsi、sock_accept、io、wsi_logical_close、run_pt等,并配套evlib_size_pt等结构体尺寸字段。示例实现了其中 5 个回调(minimal-http-server.c):
static const struct lws_event_loop_ops event_loop_ops_custom = { .name = "custom", .init_pt = init_pt_custom, .init_vhost_listen_wsi = sock_accept_custom, .sock_accept = sock_accept_custom, .io = io_custom, .wsi_logical_close = wsi_logical_close_custom, .evlib_size_pt = sizeof(struct pt_eventlibs_custom) };四个回调各自对应一个生命周期节点:
init_pt_custom(L196-L206):lws 上下文创建期间,对每个 service thread(pt)调用一次,把创建时传入的外来循环指针(_loop)暂存进"每个 pt 的私有区域"。私有区域通过公开辅助函数lws_evlib_tsi_to_evlib_pt()获取,示例中即为struct pt_eventlibs_custom { custom_poll_ctx_t *io_loop; },其大小正是上面填写的evlib_size_pt;sock_accept_custom(L209-L215):监听 socket 就绪、accept 出新连接时调用,把新 fd 加入 poll 表并关注POLLIN。lws_get_socket_fd(wsi)取出 lws 管理的原生 fd;io_custom(L218-L240):lws 需要增减某个 wsi 的关注事件时调用,把flags从 lws 的事件语义翻译成 poll 语义——LWS_EV_READ对应POLLIN,LWS_EV_WRITE对应POLLOUT,LWS_EV_START/LWS_EV_STOP决定是"增加"还是"移除"(LWS_EV_*标志位定义见 lws-eventlib-exports.h);wsi_logical_close_custom(L243-L248):wsi 逻辑关闭时调用,把对应 fd 从 poll 表删除。
此外示例还把整个接口包进一个事件库插件描述符lws_plugin_evlib_t evlib_custom(L262-L271),头部hdr填上类名"lws_evlib_plugin"、LWS_BUILD_HASH与LWS_PLUGIN_API_MAGIC。这个类型定义于 lws-protocols-plugins.h,含义是"任何符合该协议的事件库实现都能被 lws 以同等地位接纳"——也就是说,这套自定义接入方式并不仅限于本示例,完全可以独立实现为一个可复用的事件库插件。
核心概念四:上下文创建时完成绑定
自定义事件库的一切准备工作最终在main()中通过struct lws_context_creation_info完成(minimal-http-server.c):
info.port = 7681; info.mounts = &mount; info.error_document_404 = "/404.html"; info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE; info.event_lib_custom = &evlib_custom; /* 绑定自定义事件库实现 */ foreign_loops[0] = &a_cpcx; /* 传入应用自己的 poll 循环对象 */ info.foreign_loops = foreign_loops; info.protocols = protocols; /* 可选的客户端协议回调 */ context = lws_create_context(&info);这里用到两个关键的上下文创建参数,其语义在 lws-context-vhost.h 中有明确注释:
event_lib_custom(L878-L886):非 NULL 时覆盖事件库选择,改用这份自定义实现代替 lws 内置默认循环,同时要求不再设置LWS_SERVER_OPTION_LIBUV等其他事件库标志;该字段专为"已有自研手写事件循环、想让 lws 原生地把它当作一个事件库来用"的集成场景设计;foreign_loops(L694-L707):指向一组外部事件循环对象的数组,按 service thread 依次使用;默认单线程情况下只放一个即可。当它非 NULL 时,lws 不再自建循环、也不负责关闭这些循环,循环的生命周期完全由应用掌控。
两个参数缺一不可:event_lib_custom告诉 lws"用什么操作接口",foreign_loops[0]告诉 lws"把哪个对象当作循环本体"——后者会经由init_pt_custom存入每个 pt 的私有区域。info.protocols注册的"httptest"协议(L354-L357)仅用于示例自连客户端,静态文件服务本身并不依赖它。
静态文件挂载与安全响应头
HTTP 服务本体由struct lws_http_mount mount(L278-L296)定义,是一个典型的"目录挂载"配置:
mountpoint为/,即根路径挂载点,mountpoint_len为 1;origin为"./mount-origin",表示从该目录提供文件,注释明确说明"修改mount.origin即可改为从其他目录服务";def为"index.html",即访问目录时默认返回的文件名;origin_protocol为LWSMPRO_FILE,声明 origin 是文件系统目录而非 CGI 等协议;- 其余字段(
cache_max_age、auth_mask、cgienv等)置零,表示关闭相关能力。
与之配合的是创建信息里的error_document_404 = "/404.html"和LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE选项——后者让 lws 自动为响应附加 CSP 等安全响应头(挂载目录中的 strict-csp.svg 即用于验证严格 CSP 是否生效)。也就是说,在自定义事件循环上搭建的服务,与 lws 标准 HTTP 服务的全部能力(挂载、默认页、404、安全头、mimetype 识别)完全一致,事件循环的替换对上层 HTTP 语义零侵入。
同一循环上的可选客户端连接
为证明自定义循环不只能服务监听 socket,示例还演示了一个 HTTPS 客户端连接(L360-L392):通过lws_client_connect_via_info()向warmcat.com:443发起 GET/,启用 SSL(LCCSCF_USE_SSL)并附带两个 HTTP/2 quirk 标志,其响应由callback_http中的LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP、LWS_CALLBACK_RECEIVE_CLIENT_HTTP等回调处理并打印状态码与内容 hexdump。这段逻辑是可选的(代码注释明确标注 "It's optional..."),它的价值在于验证:lws 会把客户端连接的 socket 也通过io_custom/sock_accept_custom注册进同一张 poll 表,与监听 socket、服务器连接并存于一个循环。
客户端连接的协议回调callback_http最终以lws_callback_http_dummy()收尾(L351),保证未处理事件走 lws 内置的 HTTP 默认行为。
运行验证与常见问题
- 验证流程:启动后依次观察——
Creating Vhost 'default' port 7681日志说明监听已就绪;浏览器访问http://localhost:7681应看到./mount-origin/index.html渲染的页面;访问不存在的路径应返回 404 页面;终端应出现来自warmcat.com客户端连接的响应日志(如LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: resp 200),证明自定义循环同时驱动了服务端与客户端。 - 常见问题:
- fd 表溢出:
MAX_CUSTOM_POLLFDS为 64,若并发连接超过该值,custom_poll_add_fd会返回错误并在日志中打印no room left,需要按实际负载调大该常量; - 编译被跳过:CMake 要求 lws 以
LWS_WITH_SERVER、LWS_WITH_CLIENT编译(LWS_ROLE_H1为 HTTP/1 角色开关),若本地 lws 缺任一配置,make将不会生成可执行文件,需检查 lws 构建配置; - 目录不存在:程序从启动目录解析
./mount-origin,在错误工作目录下启动会导致静态文件 404 或无法找到默认页,此时应确认相对路径正确,或按源码注释修改mount.origin。
- fd 表溢出:
小结
lws-minimal-http-server-eventlib-custom用一份紧凑的示例代码讲清了 lws 自定义事件循环集成的完整闭环:应用自己的 poll 表 + 四个表操作构成循环本体;struct lws_event_loop_ops接口层把 lws 的事件请求翻译成 poll 事件;event_lib_custom与foreign_loops在上下文创建时完成绑定;lws_service_adjust_timeout与lws_service_fd完成超时协商与事件分发。对于需要把 lws 的 HTTP/WebSocket 能力融进既有自研循环的集成场景,这套模式可以直接照搬,或进一步封装为独立的lws_plugin_evlib插件复用。
- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
相关推荐
libwebsockets 最小化 HTTP 服务器示例详解:eventlib + SMP 多线程事件循环(minimal-http-server-eventlib-smp)
libwebsockets 最小化 HTTP 服务器示例详解:eventlib + SMP 多线程事件循环(minimal http server eventl
人工智能AI Agent多模态语音AI 应用doccano 快速上手指南:从安装部署到标注工作流的完整实践
doccano 快速上手指南:从安装部署到标注工作流的完整实践 doccano 是一个面向机器学习从业者的开源数据标注工具,支持多种任务类型与多种数据格式,并可
人工智能AI Agent多模态语音AI 应用LeetCode 995. K 连续位的最小翻转次数:从 O(n·k) 暴力到差分数组、双端队列与原地 O(1) 空间的完整题解
LeetCode 995. K 连续位的最小翻转次数:从 O n·k 暴力到差分数组、双端队列与原地 O 1 空间的完整题解 导读 本篇基于本仓库 proble
人工智能AI Agent多模态语音AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考