- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
本篇文章基于仓库中 libwebsockets 的 minimal-raw-netcat 示例 展开,完整讲解如何借助 libwebsockets(lws)的 raw 模式,把一个"外来"的 TCP 客户端套接字和 stdin 文件描述符同时收养进 lws 事件循环,实现"stdin → 远程服务器 → stdout"的双向数据搬运。读完本文,你不仅能独立编译、运行和调参这个"netcat 变体",还能从源码层面理解LWS_CALLBACK_RAW_*回调族、lws_adopt_descriptor_vhost()收养机制以及定时器在连接收尾阶段的作用,为自行编写基于 lws raw API 的转发器、代理或串口工具打下基础。
示例定位:libwebsockets raw 家族中的"网络猫"
minimal-raw-netcat 是 lws 官方 minimal-examples 中 raw 系列示例 的一员。raw 模式是 libwebsockets 区别于纯 WebSocket 能力的重要扩展:它允许 lws 事件循环管理非 HTTP/WS 语义的描述符,包括普通 TCP/UDP 套接字、文件、设备节点、FIFO 甚至串口。
在 raw 示例总览表 中,各示例的分工如下:
| 示例 | 演示内容 |
|---|---|
| minimal-raw-adopt-tcp | 收养别人已连接的 TCP 套接字 |
| minimal-raw-adopt-udp | 创建 UDP 套接字并读写 |
| minimal-raw-fallback-http | HTTP(S) 服务回退到指定角色与协议 |
| minimal-raw-file | 将文件描述符(设备节点、FIFO、普通文件等)收养进事件循环 |
| minimal-raw-netcat | 把 stdin 写到远程服务器,并把返回内容打印到 stdout |
| minimal-raw-proxy / -fallback | 监听并代理到指定 IP/端口 |
可以看到,minimal-raw-netcat 的独特之处在于它同时收养了两类描述符:
- 一个由程序自己创建并 connect 好的"外来"TCP 套接字(对应远程服务器);
- 文件描述符 0,即 stdin(标准输入)。
这正是LWS_CALLBACK_RAW_*回调族中"文件描述符"与"套接字描述符"两套回调在同一次运行中同时被触发的经典样例。
编译:CMake 构建与前置条件
示例的构建方式与其 README 描述一致:
cmake . && make工程配置见 CMakeLists.txt,其中有几个值得注意的约束:
find_package(libwebsockets CONFIG REQUIRED):要求系统已安装并导出了 libwebsockets 的 CMake 配置;require_lws_config(LWS_WITH_SERVER 1 requirements):示例编译前会强制校验 lws 编译时开启了LWS_WITH_SERVER宏。虽然本例并没有监听端口,但它依赖服务端相关的 vhost / adoption 基础设施;- 链接阶段根据
websockets_shared变量选择共享库websockets_shared或静态库websockets。
在本仓库中,libwebsockets 以 third_party/libwebsockets 的形式完整内置(含BUILD.gn与output_libs.gni),因此也可以把它视为 TEN-framework 构建体系中的一个第三方依赖来交叉编译、复用其产物。
用法与命令行参数
程序的命令行接口由源码通过lws_cmdline_option()解析(见 minimal-raw-netcat.c),启动时打印的提示信息为:
LWS minimal raw netcat [--server ip] [--port port] [-w ms]支持的参数及语义如下:
| 参数 | 作用 | 默认值 | 源码位置 |
|---|---|---|---|
--server ip | 目标服务器地址(IP 或域名) | libwebsockets.org | L193-L194 |
--port port | 目标端口(字符串形式) | 80 | L190-L191 |
-w ms | stdin 关闭后、关闭远端套接字前的等待毫秒数 | 100(见下文说明) | L196-L197 |
-d N | 设置 lws 日志级别(数字) | LLL_USER \| LLL_ERR \| LLL_WARN \| LLL_NOTICE | L152-L153 |
关于-w有一个值得注意的细节:README 文字描述为"stdin 关闭后等待 1 秒再输出在途结果",但源码 L38 中us_wait_after_input_close的默认值是LWS_USEC_PER_SEC / 10,即100 毫秒(LWS_USEC_PER_SEC为 1 秒的微秒数 1000000,除以 10 得 100000 微秒)。传入-w时则按1000 * atoi(p)微秒计算,即单位为毫秒。也就是说,实际默认等待时间以源码为准,可通过-w 1000显式调整为 README 描述的 1 秒。
日志全部输出到stderr,因此可以用2>log之类的方式把日志与业务输出(stdout 上的远端响应)分离。
运行演示与输出逐行解读
README 给出的典型调用是:用echo拼一个 HTTP/1.1 请求喂给 stdin,观察程序把请求转发给远程服务器并打印响应:
echo -e -n "GET / http/1.1\r\n\r\n" | ./lws-minimal-raw-netcat运行输出(节选自原 README,时间戳为历史数据):
[2018/05/02 08:53:53:2665] USER: LWS minimal raw netcat [--server ip] [--port port] [2018/05/02 08:53:53:2667] NOTICE: Creating Vhost 'default' (no listener), 1 protocols, IPv6 off [2018/05/02 08:53:53:2703] USER: Starting connect... [2018/05/02 08:53:53:5644] USER: Connected to libwebsockets.org:80... [2018/05/02 08:53:53:5645] USER: LWS_CALLBACK_RAW_ADOPT [2018/05/02 08:53:53:5645] USER: LWS_CALLBACK_RAW_ADOPT_FILE [2018/05/02 08:53:53:5646] USER: LWS_CALLBACK_RAW_RX_FILE [2018/05/02 08:53:53:5646] USER: LWS_CALLBACK_RAW_CLOSE_FILE [2018/05/02 08:53:53:8600] USER: LWS_CALLBACK_RAW_RX (186) HTTP/1.1 301 Redirect server: lwsws Strict-Transport-Security: max-age=15768000 ; includeSubDomains location: https://libwebsockets.org content-type: text/html content-length: 0这段输出本身就是一个"回调时序图",可以这样理解:
- Vhost 创建:日志显示
Creating Vhost 'default' (no listener), 1 protocols——程序创建了不带监听端口的 vhost(见下文"上下文与 vhost"),并注册了 1 个协议raw-test; - 外部连接建立:
Starting connect...→Connected to libwebsockets.org:80...,对应源码中手工socket()+connect()的过程; - 套接字被收养:
LWS_CALLBACK_RAW_ADOPT,对应lws_adopt_descriptor_vhost(..., LWS_ADOPT_SOCKET, ...); - stdin 被收养并读到 EOF:
LWS_CALLBACK_RAW_ADOPT_FILE→LWS_CALLBACK_RAW_RX_FILE→LWS_CALLBACK_RAW_CLOSE_FILE,对应lws_adopt_descriptor_vhost(..., LWS_ADOPT_RAW_FILE_DESC, ...)收养 fd 0,随后管道因echo写完而 EOF,触发关闭回调; - 远端响应回灌:
LWS_CALLBACK_RAW_RX (186)表示收到 186 字节,随后逐字节putchar到 stdout,即上文的 HTTP 响应头。
README 同时提醒:示例"什么都自己做"。演示中远端服务器在空闲约 5 秒后主动关闭连接,触发LWS_CALLBACK_RAW_CLOSE,此后程序会继续运行直到你按下^C(SIGINT)。
源码级原理拆解
全局状态与协议注册
源码 L34-L38 定义了核心全局状态:
static struct lws *raw_wsi, *stdin_wsi; /* 远端套接字与 stdin 各自的 wsi */ static uint8_t buf[LWS_PRE + 4096]; /* 转发缓冲区,预留 LWS_PRE 头空间 */ static int waiting, interrupted; static struct lws_context *context; static int us_wait_after_input_close = LWS_USEC_PER_SEC / 10;buf预留LWS_PRE字节是 lws 的安全要求:lws_write()需要协议头部空间,直接复用缓冲区时必须在数据前留白;waiting记录本次从 stdin 读到的字节数,供写回调使用;- 回调函数统一挂在协议
raw-test上(L130-L133),并通过LWS_PROTOCOL_LIST_TERM终止协议表。
回调族:两类描述符、两套生命周期
回调函数callback_raw_test()(L40-L128)按lws_callback_reasons分发。对应的事件常量定义在 lws-callbacks.h:
| 回调 | 枚举值 | 触发时机 | 本例处理 |
|---|---|---|---|
LWS_CALLBACK_RAW_ADOPT | 62 | 套接字被收养进事件循环 | 打印日志,并请求可写(lws_callback_on_writable) |
LWS_CALLBACK_RAW_CLOSE | 60 | 远端套接字关闭 | 置interrupted = 1,lws_cancel_service()唤醒事件循环退出 |
LWS_CALLBACK_RAW_RX | 59 | 套接字收到数据 | 逐字节putchar并fflush(stdout) |
LWS_CALLBACK_RAW_WRITEABLE | 61 | 套接字可写 | 恢复 stdin 流控,用lws_write(..., LWS_WRITE_RAW)写出waiting字节 |
LWS_CALLBACK_RAW_ADOPT_FILE | 63 | 文件描述符被收养 | 打印日志 |
LWS_CALLBACK_RAW_RX_FILE | 64 | 文件描述符可读 | read(0, buf, sizeof(buf))读 stdin,请求远端可写,并暂停 stdin 流控 |
LWS_CALLBACK_RAW_CLOSE_FILE | 66 | 文件描述符 EOF/关闭 | 置空stdin_wsi,若远端仍在则用lws_set_timer_usecs()启动收尾定时器 |
LWS_CALLBACK_TIMER | — | 定时器到期 | 置interrupted,lws_cancel_service()结束程序 |
其中LWS_CALLBACK_RAW_RX_FILE中lws_rx_flow_control(wsi, 0)与LWS_CALLBACK_RAW_WRITEABLE中lws_rx_flow_control(stdin_wsi, 1)构成一对背压控制:stdin 数据被读走但远端尚未写完成时,暂停继续读取,避免缓冲区堆积。
上下文与 vhost:不监听也能运行
主函数中先以LWS_SERVER_OPTION_EXPLICIT_VHOSTS创建 context,再单独创建 vhost(L158-L174):
info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS; context = lws_create_context(&info); info.port = CONTEXT_PORT_NO_LISTEN_SERVER; /* 明确表示:不创建监听套接字 */ info.protocols = protocols; vhost = lws_create_vhost(context, &info);CONTEXT_PORT_NO_LISTEN_SERVER让 vhost 只承载协议与回调分发、不监听任何端口,这与 raw 模式"只做描述符管理"的定位完全吻合。
外部套接字:手工 connect 再交给 lws
与通常的lws_client_connect_via_info()不同,本示例刻意手工完成域名解析与连接,以演示"外来套接字"的收养路径(L185-L228):
getaddrinfo(server, port, ...)解析目标(AF_UNSPEC允许 IPv4/IPv6);- 遍历结果用
socket()创建SOCK_STREAM套接字; connect()到首个可用的地址;- 成功后调用收养 API:
raw_wsi = lws_adopt_descriptor_vhost(vhost, LWS_ADOPT_SOCKET, sock, protocols[0].name, NULL);lws_adopt_descriptor_vhost()的原型与语义见 lws-adopt.h:成功时返回绑定该描述符的新 wsi,失败时关闭描述符并返回 NULL。收养类型枚举(lws-adopt.h L66-L75)中:
LWS_ADOPT_RAW_FILE_DESC = 0:按原始文件描述符收养;LWS_ADOPT_SOCKET = 2:按套接字收养(缺省即文件);- 另有
LWS_ADOPT_ALLOW_SSL、LWS_ADOPT_FLAG_UDP、LWS_ADOPT_RAW_SOCKET_UDP等组合。
stdin 的收养与事件循环
随后用同样的 API、以LWS_ADOPT_RAW_FILE_DESC把 fd 0(stdin)也收养进来(L239-L245):
sock.filefd = 0; stdin_wsi = lws_adopt_descriptor_vhost(vhost, LWS_ADOPT_RAW_FILE_DESC, sock, protocols[0].name, NULL);此后两个描述符完全纳入 lws 的poll事件循环:
while (n >= 0 && !interrupted) n = lws_service(context, 0);lws_service()驱动回调分发;interrupted可由 SIGINT(sigint_handler)、远端关闭(LWS_CALLBACK_RAW_CLOSE)或收尾定时器(LWS_CALLBACK_TIMER)置位,从而优雅退出。lws_cancel_service()用于从回调内部唤醒阻塞中的事件循环。
收尾时序:为什么它比真 netcat 更"耐心"
这是示例 README 着重强调的优势:stdin 关闭后不立即断开,而是等待在途数据返回。
- stdin EOF 触发
LWS_CALLBACK_RAW_CLOSE_FILE; - 若远端 wsi 仍存活,调用
lws_set_timer_usecs(raw_wsi, us_wait_after_input_close)注册一次性定时器; - 定时器到期触发
LWS_CALLBACK_TIMER,置interrupted并取消服务,进程退出。
因此请求发出后即使 stdin 已经关闭,远端尚未到达的响应仍能在等待窗口内通过LWS_CALLBACK_RAW_RX打印到 stdout——这正是演示输出中LWS_CALLBACK_RAW_CLOSE_FILE之后仍能看到LWS_CALLBACK_RAW_RX (186)的原因。等待时长由-w毫秒参数控制。
扩展思路:把示例改造成通用双向管道
结合源码结构,minimal-raw-netcat 可以直接演化为多种实用工具,改动点都很小:
- 本机 HTTP 探测:
./lws-minimal-raw-netcat --server 127.0.0.1 --port 8080,配合echo或脚本向本地服务发送原始请求,绕过curl的规范化; - 回显/延时测量:连接任意 echo 服务,观察往返数据;
- 串口/FIFO 适配:参照同目录的 minimal-raw-file 与 minimal-raw-serial 示例,把 stdin 换成设备节点,即可把"文件描述符收养"推广到串口数据搬运;
- 双向代理雏形:若要同时转发远端到本地的数据(而不只是打印),只需在
LWS_CALLBACK_RAW_RX中把收到的字节写回 stdin 对应的 wsi(LWS_WRITE_RAW),即向 minimal-raw-proxy 看齐。
从 raw/README.md 总览 可见,adopt-tcp、adopt-udp、proxy 等示例分别覆盖了"收养既有连接""UDP 读写""监听代理"等相邻场景,可与本示例对照阅读,形成对 lws raw 能力面的完整认识。
注意事项与限制
- 外部依赖与网络可达性:示例默认连接
libwebsockets.org:80,属于互联网外部站点,实际复现时可能因网络策略不可达;建议先用--server 127.0.0.1 --port <本地端口>指向本地服务验证; - 构建前提:需要系统预装带 CMake 配置导出的 libwebsockets,且库编译时开启
LWS_WITH_SERVER;本仓库内置的 third_party/libwebsockets 可作为替代构建来源; - 默认等待时长:README 描述"等待 1 秒",源码默认实为 100 ms(
LWS_USEC_PER_SEC / 10),需要 1 秒请显式传-w 1000; - 单连接模型:示例面向单条连接,且 stdin 关闭即进入收尾流程,不适合需要持续交互或并发多路转发的场景;
- 缓冲区大小:转发缓冲区为
LWS_PRE + 4096字节,超过 4 KB 的单次读入会分多次写出,符合流式处理预期,但高吞吐场景可相应调大。
总体而言,minimal-raw-netcat 是理解 libwebsockets raw 模式最直接的入门样例:它把"外来套接字收养""文件描述符收养""raw 回调族""定时器收尾"四条主线压缩在约 260 行 C 代码中,既有完整的端到端可运行性,又为后续基于 lws 编写代理、串口工具和自定义协议转发器提供了可复用的骨架。
- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
相关推荐
Monolog SocketHandler 实战指南:用 PHP 套接字把日志实时送往远端日志服务器
Monolog SocketHandler 实战指南:用 PHP 套接字把日志实时送往远端日志服务器 导读 Monolog 是 Laravel 等 PHP 框架
示例工程数据库教程后端基于 libwebsockets 的最小化 HTTPS 服务器:minimal-http-server-tls 实战与源码解析
基于 libwebsockets 的最小化 HTTPS 服务器:minimal http server tls 实战与源码解析 本指南以 TEN framewo
人工智能AI Agent多模态语音AI 应用OpenChamber Dev Server Tunnel 深度解析:原始字节级远程开发服务器隧道协议与实现
OpenChamber Dev Server Tunnel 深度解析:原始字节级远程开发服务器隧道协议与实现 本篇技术指南聚焦 OpenChamber 桌面端的
AI Agent人工智能代码智能体交互助手
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考