- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
SMD(System Message Distribution)是 libwebsockets(lws)提供的一套跨平台、线程安全的系统内消息发布/订阅机制:同一进程内任意已注册的参与者可以向其他参与者广播短消息,进程之间还可借助 Secure Streams 代理互通。本文以 TEN-framework 仓库内嵌的 libwebsockets 源码(SMD 说明文档 与 实现 smd.c)为对象,梳理消息格式、类过滤、投递保证、跨进程桥接等核心机制,并给出可直接参考的 API 用法与三类预定义消息 Schema。
SMD 是什么:系统内部轻量消息总线
在嵌入式与物联网场景中,系统内的多个独立模块(网络栈、按键处理、状态机、业务应用等)经常需要快速感知彼此的事件与状态变化。libwebsockets 的 SMD 为这种"系统内部、短消息、低占空比"的通信提供了统一的 API:
- 跨平台一致:各操作系统与框架通常有各自碎片化的消息传递 API,而
lws_smd的接口在 Windows、RTOS 等任何平台上行为一致,跨平台代码只需编写一次。 - 消息短小:单条消息负载不超过 384 字节(
LWS_SMD_MAX_PAYLOAD,定义于 lws-smd.h),低于管道或 UDS 数据报的原子性系统限制,也适合小内存系统的堆使用;但同时足以承载有实际意义的 JSON。 - 触发来源内外皆可:消息本身只在系统内部传递,但其触发事件可能来自外部,例如按键(keypress)或网络状态变化(networking state changes)。
从实现上看,SMD 是挂在struct lws_context上的一个子系统(ctx->smd),参与者(peer)注册后形成一条双向链表(owner_peers),待分发消息挂在另一条链表(owner_messages)上,消息在堆上分配,由 smd.c 统一调度。
三种部署形态:单进程广播到跨进程代理
SMD 支持从单进程到多进程的渐进式扩展,文档中给出了三种形态:
单进程形态:所有参与者注册在同一个
lws_context上,消息由 lws 服务线程上下文以回调方式投递给参与者。参与者注册与发送消息的 API 都是线程安全的,因此非服务线程也可安全发布消息。跨进程形态:不同进程、不同
lws_context的参与者之间,借助现有 Secure Streams 代理(SS Proxy)互连——用户可复用_lws_smd内建 streamtype 建立 Secure Stream,即可接收远端进程的 SMD 消息,也可反向把本进程消息发给远端。事件循环集成:消息在任何注册参与者发送后,最早也要等到事件循环的下一次轮转才会被分发,从而在单次事件循环 trip 内批量处理多个排队事件,保证消息处理非递归、栈占用温和。
消息类(Message Class)与过滤
消息类(lws_smd_class_t,即uint32_t位图)用于标识消息的通用类型,例如网络状态或 UI 按键事件。参与者注册时通过_class_filter位掩码声明自己关心哪些类:
- 过滤器位为 0 的类永远不会投递给该参与者;
- 一条消息通常只标记单个类,但也允许同时置多个类位、按"任一匹配"投递——此时必须保证负载能被所有可能类的读者解析,例如统一使用 JSON。
仓库预定义了几个 well-known 高层类(见 lws-smd.h):
| 常量 | 位值 | 含义 |
|---|---|---|
LWSSMDCL_INTERACTION | 1 << 0 | 用户与设备交互(按键、触摸屏、拿起设备等) |
LWSSMDCL_SYSTEM_STATE | 1 << 1 | lws_system 状态机变化(如进入 OPERATIONAL) |
LWSSMDCL_NETWORK | 1 << 2 | 网络事件(link-up、DHCP、captive portal 状态更新) |
LWSSMDCL_METRICS | 1 << 3 | SS 客户端进程向代理上报指标(特殊:代理不转播此类) |
LWSSMDCL_USER_BASE_BITNUM | 24 | 用户自定义类起始位 |
用户可用LWSSMDCL_USER_BASE_BITNUM定义最多 8 个私有类,类位取(1 << LWSSMDCL_USER_BASE_BITNUM)至(1 << (LWSSMDCL_USER_BASE_BITNUM + 7))。
分配期拒绝机制:lws_smd维护所有参与者类掩码的全局并集(smd->_class_filter,见_lws_smd_class_mask_union())。若某个类当前没有任何参与者监听,则分配该类的消息会被在分配期而非分发期直接拒绝(lws_smd_msg_alloc()返回 NULL),从而不浪费堆与 CPU;一旦出现感兴趣的参与者,该类消息随即恢复产生。因此调用方在lws_smd_msg_alloc()返回 NULL 时应无错误地跳过本次事件生成动作(对应源码 smd.c#L38-L49 的rejecting class ... no participant wants分支)。
消息投递保证与线程语义
文档与实现共同明确了以下投递语义(smd.c):
- 投递范围:已发送消息会投递给所有类掩码匹配的已注册参与者,包括发送者自己;发送 API 线程安全。
- 回调上下文:本地投递的回调发生在 lws 事件循环线程 0(默认
LWS_MAX_SMP = 1时的唯一线程);跨进程参与者则在各自 UDS 网络线程的上下文收到回调。 - 负载生命周期:从回调返回那一刻起,消息负载就可能被立即销毁,不可保存引用或期待之后仍可用。
- 时间戳与顺序:消息携带全系统单调时钟(monotonic)时间戳;同事件循环上的参与者按序收到;跨线程时顺序取决于平台锁竞争;经 UDS 连接的跨进程参与者可能乱序——在意顺序的接收方必须依据消息创建时间戳自行排序。
队列深度与 TTL
从 context.c#L669-L683 可见,SMD 的队列行为可通过lws_context_creation_info配置:
smd_ttl_us:排队消息存活时间,默认非 FreeRTOS 平台 2,000,000 µs(2s),FreeRTOS 平台 5,000,000 µs(5s);超时消息会被强制移除并推进相关 peer 的 tail(lws_smd_message_pending()中的超时清理逻辑)。smd_queue_depth:最大排队消息数,默认非 FreeRTOS 平台 40、FreeRTOS 平台 20;超过深度的新消息在_lws_smd_msg_send()中被拒绝。
消息引用计数(Refcounting)
为免为每条消息维护一份"参与者数量长度"的列表,SMD 在消息到达时按当时表示愿意接收该类的活跃参与者数计算一个 refcount(_lws_smd_msg_assess_peers_interested())。每个参与者把消息作为自己的"tail"逐条推进,每成功投递一次就递减 refcount,归零即销毁(_lws_smd_msg_destroy())。
由于对端可能异步 detach / 关闭链路,分发器(distributor)侧的逻辑 peer 对象会推迟自我销毁,直到不可能再收到其活跃期内时间戳的消息为止;默认 2 秒宽限期(grace period)用于保证离开的 peer 在销毁前正确结算消息引用计数。
消息创建:alloc/send 与 printf 一站式
根据负载类型,lws_smd提供两套消息创建路径:
- 已知长度:
lws_smd_msg_alloc(ctx, _class, len)返回不透明消息对象关联的负载缓冲,调用方直接就地准备内容(免拷贝),随后用lws_smd_msg_send(ctx, payload)入队;若生成内容过程中出错,可用lws_smd_msg_free(&payload)放弃(否则 lws 会自行接管销毁)。 - 格式化文本(推荐 JSON):
lws_smd_msg_printf(ctx, _class, format, ...)一步完成"尺寸探测 → 分配 → 格式化 → 入队"(内部先vsnprintf(NULL, 0, ...)计算长度再调用lws_smd_msg_alloc与lws_smd_msg_send),同样线程安全;若当前无任何参与者关注该类,直接返回 0、不做任何分配。
int lws_smd_msg_printf(struct lws_context *ctx, lws_smd_class_t _class, const char *format, ...);由于 SMD 消息短小、低占空比而内容开放,官方明确推荐使用 JSON:可维护、可扩展、可调试、自描述,且避免跨团队共享头文件版本这类脆弱依赖。
Secure Streams_lws_smdstreamtype 跨进程互通
当以LWS_WITH_SECURE_STREAMS构建时,lws_smd暴露内建 streamtype_lws_smd(LWS_SMD_STREAMTYPENAME),用户 Secure Stream 可借助 SS payload 语义与 SMD 互通:
- 创建 Secure Stream 时,用户提供的 SS info 结构成员
manual_initial_tx_credit被复用为该 SMD 连接的RX 类掩码。 - RX 与 TX 负载前均有16 字节二进制头(
LWS_SMD_SS_RX_HEADER_LEN):MSB-first 的 64 位类位域(当前仅用低 32 位)+ MSB-first 的 64 位微秒级时间戳。TX 时只需设置前 64 位类位域,时间戳由 lws 自动补齐(对应 smd.c#L331-L359 中lws_ser_wu64be写类、时间戳位留 0 的实现)。
在 SS 的tx()回调中可用助手lws_smd_ss_msg_printf(tag, buf, &len, _class, format, ...)一步完成格式化与写头:
int lws_smd_ss_msg_printf(const char *tag, uint8_t *buf, size_t *len, lws_smd_class_t _class, const char *format, ...);对应地,在_lws_smdstreamtype 的rx()回调中,把收到的载荷直接透传给lws_smd_ss_rx_forward(ss_user, buf, len)(代理侧变体lws_smd_sspc_rx_forward()),即可把序列化的 SMD 消息反序列化并转发给本 context 内所有关注该类的本地参与者(转发时保留原始源时间戳而非转发时刻,见 smd.c#L404-L409)。
Well-known 消息 Schema:三类典型业务事件
SMD 针对常见嵌入式业务预定义了三种高层类与对应 JSON Schema,参与者可按类订阅并解析。
用户交互按键事件(LWSSMDCL_INTERACTION)
由lws_button在用户与已定义按键交互时产生;点击类事件与按下/抬起类事件会同时发布,参与者按交互语义选择关注哪一类。两类事件发布前都经过精细过滤(细节见 按键驱动说明)。
Schema:
{ "type": "button", "src": "<controller-name>/<button-name>", "event": "<event-name>" }示例:{"type":"button","src":"bc/user","event":"doubleclick"}
| 事件名 | 含义 |
|---|---|
down | 按键通过"按下"过滤,适合基于时长的响应 |
up | 按键已抬起,适合基于时长的响应 |
click | 按键活动被归类为单击 |
longclick | 按键活动被归类为长按 |
doubleclick | 按键活动被归类为双击 |
路由表变化(LWSSMDCL_NETWORK)
若能订阅 OS 路由表变化(例如 Linux 上通过 rtnetlink 支持),lws 会用 SMD 通告变化;若启用了 Captive Portal Detect(CPD)且能看到路由表变化,则自动触发新一轮 CPD,结果同样经 SMD 广播。
Schema:
{ "rt": "add|del", "add" if being added }实现细节:context/pt 创建时,在 Linux 下 lws 会尝试获取路由表(需要 root 权限),该动作发生在协议初始化后、权限被 drop 之前;lws 在每个 pt 维护路由表缓存,变化时重估既有连接的对端是否仍可达,不可达则关闭连接;若网关路由变化,还会额外发布{"trigger":"cpdcheck","src":"gw-change"}。
Captive Portal 检测(LWSSMDCL_NETWORK)
主动探测网络能否直连互联网或是否被强制门户拦截。检测步骤通过 Secure Streams Policy 中 streamtypecaptive_portal_detect编程,示例配置:
"captive_portal_detect": { "endpoint": "connectivitycheck.android.com", "http_url": "generate_204", "port": 80, "protocol": "h1", "http_method": "GET", "opportunistic": true, "http_expect": 204, "http_fail_redirect": true }- 检测结果:Schema 为
{"type": "cpd", "result":"<result>"},结果取值OK(可直达互联网)、Captive(处于强制门户后)、No internet(无连接)。 - 请求重测:Schema 为
{"trigger": "cpdcheck"},任何参与者可借此主动请求重新检测。
lws_system 状态推进(LWSSMDCL_SYSTEM_STATE)
lws_system 状态变化被转成 SMD 消息,让不在事件循环上的参与者也能感知系统进度。注意区分两套机制:在主事件循环上注册 lws_system notifier 回调可以同步否决状态变更并 hook 拟议状态;而 SMD 事件是异步的、只在状态变更被决定之后通知,但可覆盖整个系统。
在 lws 负责引导启动的前提下,系统必须获得日期(date)并取得非强制门户连接的 IP 之后才能建立经过验证的 TLS 连接,因此用户代码通常依赖系统到达OPERATIONAL状态再行动。
Schema:{"state":"<state>"},状态枚举及含义:
| 状态 | 含义 |
|---|---|
CONTEXT_CREATED | 正在创建 lws_context |
INITIALIZED | 初始 vhost 与协议已初始化 |
IFACE_COLDPLUG | 已发现网络接口 |
DHCP | 已获得 DHCP |
CPD_PRE_TIME | 尚未拥有系统时间时的 captive portal 检测钩子 |
TIME_VALID | Ntpclient 已运行 |
CPD_POST_TIME | 拥有系统时间后(基于 TLS)的 captive portal 检测钩子 |
POLICY_VALID | 系统策略已获取并解析 |
REGISTERED | 设备已向权威机构注册 |
AUTH1 | 已凭注册信息从权威机构取得 auth1 |
AUTH2 | 已凭注册信息从权威机构取得 auth2 |
OPERATIONAL | 系统已激活,可建立经过认证的 TLS 连接 |
POLICY_INVALID | 策略正在变更 |
参与者注册与回调签名
要成为 SMD 参与者,调用lws_smd_register()并给出类过滤掩码与回调;注册同样线程安全:
struct lws_smd_peer * lws_smd_register(struct lws_context *ctx, void *opaque, int flags, lws_smd_class_t _class_filter, lws_smd_notification_cb_t cb); typedef int (*lws_smd_notification_cb_t)(void *opaque, lws_smd_class_t _class, lws_usec_t timestamp, void *buf, size_t len);- 回调收到:注册时传入的
opaque指针、消息类、单调时间戳、负载缓冲与长度。 - 注册时可用
LWSSMDREG_FLAG_PROXIED_SS标记"实际是代理 SS 连接在注册"(此时 opaque 为 ss handle)。 - 注销使用
lws_smd_unregister(pr);若注册打算与lws_context同生命周期则无需显式注销,lws_context_destroy会统一清理(对应 smd.c#L767-L804 的_lws_smd_destroy())。
注册新参与者时,实现会同步更新全局类掩码并为队列中所有该参与者感兴趣的既有消息递增 refcount,保证新订阅者不会错过已在途的消息(见 smd.c#L634-L653);反之,注销时会为其本应收到的所有排队消息递减 refcount,归零即销毁。
小结与源码阅读建议
SMD 为 TEN-framework 内嵌 libwebsockets 的嵌入式系统提供了"单进程广播 + 跨进程代理桥接 + 事件循环集成"的统一消息总线,其设计要点可归纳为:384 字节短负载、类位图过滤、分配期拒绝无听众的类、引用计数管理生命周期、单调时间戳保证顺序可判、以及JSON 负载的自描述 Schema。
建议按以下路径深入源码:
- 接口定义与常量:include/libwebsockets/lws-smd.h
- 核心实现(分配/入队/分发/注册/超时/销毁):lib/system/smd/smd.c
- 队列深度与 TTL 的默认值及配置:
smd_queue_depth/smd_ttl_us见 lib/core/context.c#L669-L683 - 按键事件过滤的细节说明:lib/drivers/button/README.md
- 架构图(消息格式、单进程分发、SS 代理桥接):doc-assets/smd-message.png、doc-assets/smd-single-process.png、doc-assets/smd-proxy.png
- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
相关推荐
TEN-framework 集成下的 libwebsockets 发布策略:stable 分支、backport 与 soname 机制详解
TEN framework 集成下的 libwebsockets 发布策略:stable 分支、backport 与 soname 机制详解 libwebsoc
人工智能AI Agent多模态语音AI 应用TEN-framework 内置 libwebsockets meta-drivers:用统一驱动抽象解锁跨平台嵌入式开发
TEN framework 内置 libwebsockets meta drivers:用统一驱动抽象解锁跨平台嵌入式开发 导读 本文围绕 TEN framew
人工智能AI Agent多模态语音AI 应用libwebsockets 客户端 HTTP Cookie 存储、缓存与自动应用机制详解(TEN-framework 内置组件)
libwebsockets 客户端 HTTP Cookie 存储、缓存与自动应用机制详解(TEN framework 内置组件) 本指南围绕 TEN frame
人工智能AI Agent多模态语音AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考