☰
libwebsockets minimal-raw-netcat 实战:用原始套接字实现 stdin 到远程服务器的双向转发
2026/10/7 9:35:46 网站建设 项目流程
  • 人工智能
  • AI Agent
  • 多模态
  • 语音
  • AI 应用

【免费下载链接】ten-framework

Open-source framework for conversational voice AI agents

项目地址:https://gitcode.com/TEN-framework/ten-framework
点击查看免费下载

本篇文章基于仓库中 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-httpHTTP(S) 服务回退到指定角色与协议
minimal-raw-file将文件描述符(设备节点、FIFO、普通文件等)收养进事件循环
minimal-raw-netcat把 stdin 写到远程服务器,并把返回内容打印到 stdout
minimal-raw-proxy / -fallback监听并代理到指定 IP/端口

可以看到,minimal-raw-netcat 的独特之处在于它同时收养了两类描述符:

  1. 一个由程序自己创建并 connect 好的"外来"TCP 套接字(对应远程服务器);
  2. 文件描述符 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.orgL193-L194
--port port目标端口(字符串形式)80L190-L191
-w msstdin 关闭后、关闭远端套接字前的等待毫秒数100(见下文说明)L196-L197
-d N设置 lws 日志级别(数字)LLL_USER \| LLL_ERR \| LLL_WARN \| LLL_NOTICEL152-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

这段输出本身就是一个"回调时序图",可以这样理解:

  1. Vhost 创建:日志显示Creating Vhost 'default' (no listener), 1 protocols——程序创建了不带监听端口的 vhost(见下文"上下文与 vhost"),并注册了 1 个协议raw-test;
  2. 外部连接建立:Starting connect...→Connected to libwebsockets.org:80...,对应源码中手工socket()+connect()的过程;
  3. 套接字被收养:LWS_CALLBACK_RAW_ADOPT,对应lws_adopt_descriptor_vhost(..., LWS_ADOPT_SOCKET, ...);
  4. 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,触发关闭回调;
  5. 远端响应回灌: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_ADOPT62套接字被收养进事件循环打印日志,并请求可写(lws_callback_on_writable)
LWS_CALLBACK_RAW_CLOSE60远端套接字关闭置interrupted = 1,lws_cancel_service()唤醒事件循环退出
LWS_CALLBACK_RAW_RX59套接字收到数据逐字节putchar并fflush(stdout)
LWS_CALLBACK_RAW_WRITEABLE61套接字可写恢复 stdin 流控,用lws_write(..., LWS_WRITE_RAW)写出waiting字节
LWS_CALLBACK_RAW_ADOPT_FILE63文件描述符被收养打印日志
LWS_CALLBACK_RAW_RX_FILE64文件描述符可读read(0, buf, sizeof(buf))读 stdin,请求远端可写,并暂停 stdin 流控
LWS_CALLBACK_RAW_CLOSE_FILE66文件描述符 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):

  1. getaddrinfo(server, port, ...)解析目标(AF_UNSPEC允许 IPv4/IPv6);
  2. 遍历结果用socket()创建SOCK_STREAM套接字;
  3. connect()到首个可用的地址;
  4. 成功后调用收养 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

项目地址:https://gitcode.com/TEN-framework/ten-framework
点击查看免费下载

相关推荐

上一篇:解放双手!中文字幕自动下载神器ChineseSubFinder深度解析 🎬
下一篇:fastbook机器人学:智能机器人控制算法终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询