ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TEN-framework 内嵌 libwebsockets 实战:用 minimal-ws-proxy 理解 WebSocket 代理与环形缓冲广播

TEN-framework 内嵌 libwebsockets 实战:用 minimal-ws-proxy 理解 WebSocket 代理与环形缓冲广播 人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载导读本文以 TEN-framework 仓库内置的 libwebsockets 三方库中的minimal-ws-proxy示例为切入点讲解 libwebsocketslws如何在一个进程中同时扮演 HTTP 静态文件服务器、WebSocket 服务器和 WebSocket 客户端三种角色服务器主动与远程 wss 端点建立dumb-increment-protocol连接把收到的数据写入lws_ring环形缓冲再转发给所有通过浏览器连入的 WebSocket 客户端。读完本文你将掌握 lws 的 http mount 静态托管、协议回调protocol callback驱动的连接生命周期、客户端自动重连lws_sul调度、环形缓冲削峰以及浏览器端环状数组收尾展示的完整链路并能在 TEN-framework 的 libwebsockets 源码树中直接构建与运行该示例验证上述行为。示例定位一个客户端 服务器 静态托管三合一的 lws 程序TEN-framework 仓库在third_party/libwebsockets目录下完整托管了 libwebsockets 源码其中third_party/libwebsockets/minimal-examples/client-server/README.md用一张表格概括了该目录的核心示例namedemonstratesminimal-ws-proxyServes an index.html over http that connects back to the ws server, and maintains a ws client connection of its own at the same time to libwebsockets.org dumb-increment-protocol to feed a ringbuffer that is sent to all connected browsers.minimal-ws-proxy演示的能力可以拆解为三层HTTP 静态服务器通过 lws 的 http mount 机制把./mount-origin目录下的index.html托管在根路径/WebSocket 服务器浏览器打开页面后与本地服务建立lws-minimal-proxy协议的 ws 连接WebSocket 客户端服务进程自身同时以客户端身份向libwebsockets.org的dumb-increment-protocol发起 wss 连接把收到的文本推入环形缓冲并广播给所有在线浏览器。该示例完整源码位于 minimal-ws-proxy.c协议处理逻辑位于 protocol_lws_minimal.c前端页面与脚本位于 mount-origin 目录。构建与运行前提与构建命令示例自带 CMake 工程 CMakeLists.txt构建命令为$ cmake . make其 CMake 逻辑值得注意构建前会通过require_lws_config依次检查LWS_ROLE_WS、LWS_WITH_CLIENT、LWS_WITH_SERVER三个编译开关三者全部开启requirements为真才真正生成lws-minimal-ws-proxy可执行文件。这说明该示例是客户端角色 服务器角色 WebSocket 角色三依赖的典型用例——如果 libwebsockets 编译时未开启LWS_WITH_CLIENT或LWS_WITH_SERVER本示例会被 CMake 自动跳过不会产出二进制。运行与预期日志运行方式$ ./lws-minimal-ws-proxyREADME 中给出了参考运行日志时间为示例编写时期实际输出随运行时刻变化[2018/03/14 17:50:10:6938] USER: LWS minimal ws proxy | visit http://localhost:7681 [2018/03/14 17:50:10:6955] NOTICE: Creating Vhost default port 7681, 2 protocols, IPv6 off [2018/03/14 17:50:10:6955] NOTICE: Using non-SSL mode [2018/03/14 17:50:10:7035] NOTICE: created client ssl context for default [2018/03/14 17:50:11:7047] NOTICE: binding to lws-minimal-proxy [2018/03/14 17:50:11:7047] NOTICE: lws_client_connect_2: 0x872e60: address libwebsockets.org [2018/03/14 17:50:12:3282] NOTICE: lws_client_connect_2: 0x872e60: address libwebsockets.org [2018/03/14 17:50:13:8195] USER: callback_minimal: established日志解读要点visit http://localhost:7681静态页面服务端口固定为7681见源码中info.port 7681;2 protocolsvhost 注册了两个协议即http走lws_callback_http_dummy兜底与lws-minimal-proxycreated client ssl context由于需要外连 wss上下文以LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT初始化了全局 SSL两次lws_client_connect_2表明首次连接建立前有一次重连/重试callback_minimal: established表示到libwebsockets.org的客户端连接成功。多浏览器验证在多个浏览器窗口同时打开http://localhost:7681服务端在远程 wss 连接上收到的数据会被复制到所有已打开的浏览器窗口服务端环形缓冲最多暂存8 行文本浏览器端则只展示最近20 行收到的文本。服务端源码剖析从上下文初始化到事件循环上下文与协议表main()中先构造struct lws_context_creation_info info关键配置如下info.options LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE; info.port 7681; info.mounts mount; info.protocols protocols;LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT进程级初始化 SSL 栈供 wss 客户端连接使用LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE为 HTTP 响应自动附加安全头CSP 等最佳实践头对应前端页面中引入的strict-csp.svg徽标协议表同时声明了http与lws-minimal-proxy两个协议static struct lws_protocols protocols[] { { http, lws_callback_http_dummy, 0, 0, 0, NULL, 0}, LWS_PLUGIN_PROTOCOL_MINIMAL, LWS_PROTOCOL_LIST_TERM };协议宏定义在 protocol_lws_minimal.c 末尾#define LWS_PLUGIN_PROTOCOL_MINIMAL \ { \ lws-minimal-proxy, \ callback_minimal, \ sizeof(struct per_session_data__minimal), \ 128, \ 0, NULL, 0 \ }其中第三个字段sizeof(struct per_session_data__minimal)表示每个连接会话的私有数据大小第四个字段128为 rx 缓冲大小。静态文件托管http mountHTTP 静态服务通过struct lws_http_mount描述static const struct lws_http_mount mount { .mountpoint /, /* mountpoint URL */ .origin ./mount-origin, /* serve from dir */ .def index.html, /* default filename */ .origin_protocol LWSMPRO_FILE, /* files in a dir */ .mountpoint_len 1, /* char count */ };mountpoint /根路径全部交给该 mount 处理origin ./mount-origin静态文件取自程序启动目录下的mount-origin子目录README 明确指出it serves stuff in the subdirectory ./mount-origin of the directory it was started indef index.html访问/时默认返回index.htmlorigin_protocol LWSMPRO_FILE以文件系统目录方式提供内容。因此必须从包含mount-origin的目录启动程序否则页面无法被托管。事件循环signal(SIGINT, sigint_handler); while (n 0 !interrupted) n lws_service(context, 0);程序以lws_service(context, 0)驱动事件循环直到收到SIGINTinterrupted置位才退出并lws_context_destroy。日志级别可通过-d命令行参数覆盖lws_cmdline_option解析atoi转成日志位掩码默认级别为LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE如需LLL_INFO及更细粒度日志libwebsockets 必须以-DCMAKE_BUILD_TYPEDEBUG构建。环形缓冲广播协议回调的完整数据链路三类数据结构协议处理文件 protocol_lws_minimal.c 定义了三个层级的数据结构struct msg每条消息payload为 malloc 的缓冲区len为长度struct per_session_data__minimalpss每个浏览器连接一份含指向 vhost 链表pss_list的指针、wsi句柄以及该连接自己的环形缓冲消费游标tailstruct per_vhost_data__minimalvhd每个 vhost一份持有上下文、vhost、协议、sul 定时器、活跃 pss 链表头、lws_ring *ring环形缓冲以及客户端连接信息struct lws_client_connect_info i与client_wsi。协议初始化创建环形缓冲并发起客户端连接LWS_CALLBACK_PROTOCOL_INIT回调中vhd-ring lws_ring_create(sizeof(struct msg), 8, __minimal_destroy_message); if (!vhd-ring) return 1; sul_connect_attempt(vhd-sul);lws_ring_create创建容量为8 个元素的环形缓冲元素类型为struct msg并提供销毁回调__minimal_destroy_message释放payloadsul_connect_attempt被立即调度发起 wss 客户端连接参数如下vhd-i.port 443; vhd-i.address libwebsockets.org; vhd-i.path /; vhd-i.host vhd-i.address; vhd-i.origin vhd-i.address; vhd-i.ssl_connection 1; vhd-i.protocol dumb-increment-protocol; vhd-i.local_protocol_name lws-minimal-proxy; vhd-i.pwsi vhd-client_wsi; if (!lws_client_connect_via_info(vhd-i)) lws_sul_schedule(vhd-context, 0, vhd-sul, sul_connect_attempt, 10 * LWS_US_PER_SEC);address/port libwebsockets.org:443且ssl_connection 1即走wssprotocol dumb-increment-protocol远端服务端协议名local_protocol_name lws-minimal-proxy本地 vhost 上处理该客户端连接的回调协议即本文件注册的协议pwsi vhd-client_wsi连接建立后lws 会把客户端 wsi 写回该指针供后续回调查询若lws_client_connect_via_info返回 NULL连接立即失败则通过lws_sul_schedule在10 秒后重试。连接建立与断线自动重连LWS_CALLBACK_ESTABLISHED浏览器连入时把该 pss 插入 vhd 的活跃链表并把pss-tail初始化为环形缓冲最旧游标lws_ll_fwd_insert(pss, pss_list, vhd-pss_list); pss-tail lws_ring_get_oldest_tail(vhd-ring); pss-wsi wsi;LWS_CALLBACK_CLOSED时从链表移除。客户端侧对断线同样有自动重连策略case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: vhd-client_wsi NULL; lws_sul_schedule(vhd-context, 0, vhd-sul, sul_connect_attempt, LWS_US_PER_SEC); /* 1 秒后重试 */ case LWS_CALLBACK_CLIENT_CLOSED: vhd-client_wsi NULL; lws_sul_schedule(vhd-context, 0, vhd-sul, sul_connect_attempt, LWS_US_PER_SEC); /* 1 秒后重试 */初次失败重试间隔为 10 秒运行中断线后则改为1 秒重试体现了lws_sorted_usec_listsul定时器在连接保活中的典型用法。收包入环与广播LWS_CALLBACK_CLIENT_RECEIVE远端数据到达是整条链路的源头/* if no clients, just drop incoming */ if (!vhd-pss_list) break; if (!lws_ring_get_count_free_elements(vhd-ring)) { lwsl_user(dropping!\n); break; } amsg.len len; /* notice we over-allocate by LWS_PRE */ amsg.payload malloc(LWS_PRE len); ... memcpy((char *)amsg.payload LWS_PRE, in, len); if (!lws_ring_insert(vhd-ring, amsg, 1)) { __minimal_destroy_message(amsg); lwsl_user(dropping!\n); break; } /* let everybody know we want to write something on them */ lws_start_foreach_llp(struct per_session_data__minimal **, ppss, vhd-pss_list) { lws_callback_on_writable((*ppss)-wsi); } lws_end_foreach_llp(ppss, pss_list);三个关键细节无客户端时直接丢弃远端数据避免无谓缓冲环形缓冲满8 个元素时打印dropping!并丢弃新消息实现背压削峰分配payload时特意malloc(LWS_PRE len)并在LWS_PRE偏移处复制数据——这是 lws 写缓冲区的硬性要求发送前须在数据前预留LWS_PRE字节见下文写回逻辑。广播通过LWS_CALLBACK_SERVER_WRITEABLE完成pmsg lws_ring_get_element(vhd-ring, pss-tail); if (!pmsg) break; /* notice we allowed for LWS_PRE in the payload already */ m lws_write(wsi, ((unsigned char *)pmsg-payload) LWS_PRE, pmsg-len, LWS_WRITE_TEXT); if (m (int)pmsg-len) { lwsl_err(ERROR %d writing to ws socket\n, m); return -1; } lws_ring_consume_and_update_oldest_tail( vhd-ring, /* lws_ring object */ struct per_session_data__minimal, /* type of objects with tails */ pss-tail, /* tail of guy doing the consuming */ 1, /* number of payload objects being consumed */ vhd-pss_list, /* head of list of objects with tails */ tail, /* member name of tail in objects with tails */ pss_list /* member name of next object in objects with tails */ ); /* more to do? */ if (lws_ring_get_element(vhd-ring, pss-tail)) /* come back as soon as we can write more */ lws_callback_on_writable(pss-wsi);每个浏览器连接持有独立的tail游标lws_ring_get_element按游标取出属于自己的下一条消息写入时payload LWS_PRE正是为了满足lws_write对头部预留空间的要求写完一条后调用lws_ring_consume_and_update_oldest_tail以带 tail 的对象链表方式推进游标某条消息只有被所有连接消费后才会真正从环形缓冲释放__minimal_destroy_message释放payload若当前连接还有积压消息再次lws_callback_on_writable触发自己可写直到追平为止。浏览器端20 行的环形展示静态页 index.html 引入 example.js页面说明文字点明了整体语义The server makes a dumb-increment-protocol wss connection to libwebsockets.org. It proxies what it was sent to all browsers open on this page. The textarea shows the last 20 lines received.按页面协议自动选择 ws/wssfunction get_appropriate_ws_url(extra_url) { var pcol; var u document.URL; if (u.substring(0, 5) https) { pcol wss://; u u.substr(8); } else { pcol ws://; if (u.substring(0, 4) http) u u.substr(7); } u u.split(/); return pcol u[0] / extra_url; }页面以https://打开时自动使用wss://否则使用ws://协议名固定为lws-minimal-proxy与服务端协议表一致var ws new_ws(get_appropriate_ws_url(), lws-minimal-proxy);前端环形数组onmessage中用一个长度 20 的 JavaScript 环形数组模拟服务端行为ring[head] msg.data \n; head (head 1) % 20; if (tail head) tail (tail 1) % 20; n tail; do { s s ring[n]; n (n 1) % 20; } while (n ! head); document.getElementById(r).value s;head写入游标、tail读取游标二者相等时缓冲满tail跟随推进从而只保留最近 20 行输出到只读textarea idr cols40 rows20并自动scrollTop滚动到底部ws.onopen时启用输入框ws.onclose时禁用。在 TEN-framework 仓库中的工程定位libwebsockets 三方库本体位于 third_party/libwebsockets其 BUILD.gn 与output_libs.gni说明该库以 GN 工程方式参与 TEN-framework 整体构建本示例属于官方minimal-examples/client-server目录与同目录下其他 client/server 组合示例共同展示 lws 双角色能力核心协议处理 protocol_lws_minimal.c 中lws_ring、lws_sul、lws_callback_on_writable、lws_client_connect_via_info等 API是编写服务端主动外连并回灌本地客户端类代理/网关程序的通用模板。小结minimal-ws-proxy用约 230 行 C 代码完整展示了 lws 的核心编程模型能力实现要点源码位置HTTP 静态托管lws_http_mountLWSMPRO_FILEminimal-ws-proxy.cWS 服务器广播协议回调 LWS_CALLBACK_SERVER_WRITEABLEprotocol_lws_minimal.cWS 客户端外连lws_client_connect_via_info 重连调度protocol_lws_minimal.c削峰缓冲lws_ring容量 8按连接 tail 消费protocol_lws_minimal.c前端环形展示JS 环形数组保留最近 20 行example.js对 TEN-framework 的开发者而言这个示例是理解事件驱动回调 环形缓冲 定时器重连三种 lws 惯用法的捷径若需在真实扩展中实现上行拉取外部数据、下行推送给本地终端的代理型网关可以直接以此为蓝本改写。赞分享人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载相关推荐TEN-framework 内置 curl 的 bufq 缓冲队列模块深度解析设计、API 与内存管理TEN framework 内置 curl 的 bufq 缓冲队列模块深度解析设计、API 与内存管理 导读 bufq buffer queue缓冲队列人工智能AI Agent多模态语音AI 应用TEN Framework simple_http_server_cpp 扩展详解用 libwebsockets 把 HTTP 请求桥接为 TEN 命令TEN Framework simple_http_server_cpp 扩展详解用 libwebsockets 把 HTTP 请求桥接为 TEN 命令 本篇人工智能AI Agent多模态语音AI 应用免费开源的浏览器资源嗅探扩展猫抓cat-catch网页视频音频图片一键抓取完整指南免费开源的浏览器资源嗅探扩展猫抓cat catch网页视频音频图片一键抓取完整指南 深夜你刷到一个想反复看的网课视频右键菜单里却只有在新标签页中打开音视频上一篇如何用Roo Code实现物联网传感器数据的AI辅助处理完整指南下一篇QMK Firmware 中的 cloud_macro 云形五键宏键盘基于 Adafruit KB2040 的构建、烧录与底层配置解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进