小智的 MQTT 已连接,为什么还不能说话?从音频通道看协议选择

小智的 MQTT 已连接,为什么还不能说话?从音频通道看协议选择

给小智接服务端时,有一种容易误判的状态:设备已连上 MQTT,控制消息也能往来,扬声器却没有声音。此时如果只盯着"设备在线",很容易跳到 ASR、TTS 或音频芯片上排查,漏掉尚未建立的音频通道。

这不是本文复现过的故障,而是公开客户端代码允许出现的情境。小智的 MQTT+UDP 方案把控制消息与音频分开传输:MQTT 在线,只能说明其中一条路径的连接状态。另一个更直观的证据是,会话结束时客户端可以关闭 UDP 音频通道,同时保留 MQTT 客户端。

因此,选择 WebSocket 还是 MQTT+UDP,先要看自己准备维护怎样的连接生命周期,以及怎样证明一次语音交换真的完成。本文锁定 78/xiaozhi-esp32 的提交 6240b777aaa2bc0cad43a4ce25b30de23f36ad00,核验日期为 2026-09-10;依据公开协议和客户端源码分析,没有烧录设备、搭建服务端或做弱网实测。

同一个"开始",两种协议做的事情不同

小智把两种实现放在同一个 Protocol 接口后面。应用可以调用 Start()OpenAudioChannel()SendAudio()CloseAudioChannel(),但相同函数名不代表相同网络动作。

WebSocket 实现中的 Start() 直接返回成功。实际创建连接发生在 OpenAudioChannel():读取地址与令牌、设置请求头、连接服务端,然后发送小智应用层的 hello 并等待响应。在这份代码中,等待服务器 hello 的上限是 10 秒。因此,Start() 返回成功甚至不能用作"WebSocket 已经连上"的证据;底层连接完成后,还要区分小智应用层协商是否完成。

WebSocket 客户端源码

MQTT 实现的 Start() 则调用 StartMqttClient(false),尝试连接消息代理,也就是 broker。只有需要开启音频时,OpenAudioChannel() 才通过 MQTT 发出 hello,声明 transportudp。服务端回复 UDP 地址、端口、会话信息及加密参数后,设备才继续创建 UDP 音频对象。这里同样有 10 秒的 hello 等待上限,它约束应用层响应等待,不代表完整联网过程最多只需 10 秒。

MQTT 客户端源码

把两条路径并排,就能看到"已连接"的含义差异。下面是根据上述函数整理的成功路径,省略错误分支,不是运行日志:

text 复制代码
WebSocket
Start 返回 → 需要音频 → WebSocket 连接 → 应用层 hello 往返
           → 同一连接承载 JSON 与二进制音频

MQTT+UDP
Start 连接 broker → 需要音频 → MQTT 上 hello 往返
                 → 取得 UDP 参数 → 创建 UDP 音频通道
                 → MQTT 传 JSON,UDP 传音频

在 MQTT+UDP 路径里,连接 broker、收到可用的 hello、创建 UDP 对象、实际收到音频,是可以分别成功或失败的环节。客户端的 on_audio_channel_opened_ 回调发生在 UDP Connect 返回成功并保存对象之后;这段路径没有等待"对端收到音频"的应用层确认。不能把这个回调当成双向音频已经经过网络的证明。

声音与控制走在一起,还是分别维护

WebSocket 方案将小智 JSON 消息放在文本帧中,将音频放在二进制帧中。在接收回调里,代码根据 binary 分流:音频交给 on_incoming_audio_,文本按 JSON 的 type 分派。Opus 是这里承载的音频编码格式;WebSocket 和 UDP 是传输方式,切换后者不等于更换前者。

接服务端时还需要对齐二进制格式。该提交中的 WebSocket 发送分支支持裸 Opus 载荷,以及带额外头部的版本 2、版本 3。版本 2 包含时间戳,版本 3 的头部更短;不能只做到"收到了二进制消息",就把整段字节直接送入同一个解码入口。应核对设备实际选择的版本,以及服务端对应的封包和解包路径。二进制结构定义

MQTT+UDP 则需要维护两类接入。JSON 经 MQTT 的发布通道发送;音频发送函数单独检查 UDP 对象,把音频载荷加密后交给 UDP。hello 中的 udp 字段不是可有可无的提示:客户端会检查服务器、端口、key 和 nonce,进行十六进制解析,要求解析后的 key 与 nonce 都是 16 字节,然后导入 AES-128 CTR 所需的密钥。关键检查失败时,不会置位服务器 hello 完成事件。

这些字段说明协议兼容的工作量,也说明排障应该在哪里继续。MQTT 已连上但 hello 等待超时,下一步应核对应用层回复和字段解析;已经走到 UDP 通道创建、仍无音频,才继续检查 UDP 双向可达性和音频收发。不能因为同一域名上的 MQTT 可达,就跳过 UDP 端口、网络策略及映射状态的验证。加密参数属于敏感会话材料,排查时记录校验结果,不把原值放进文章或共享日志。MQTT+UDP 协议说明

两种方案也会改变服务端的维护边界。WebSocket 可以在同一条连接上关联控制与音频;MQTT+UDP 则需要让两路数据对应同一语音会话,并分别观察它们的状态。这是由客户端协议结构得出的接入要求,不是在断言小智官方在线服务的内部部署方式,也不意味着所有第三方服务端都实现了两种方案。

丢了一段声音,代码会等、跳过,还是补回来

WebSocket 基于 TCP。TCP 向上提供可靠、有序的字节流:某方向的较早字节缺失时,即使后续字节已经到达,也不能越过缺口按原样交付应用。由此可以推导,同一方向上后发的音频或 JSON 可能一起等待。这种等待常被称为队头阻塞;它不等于连接的另一个方向也必然同时停止。RFC 6455RFC 9293

UDP 本身不提供同样的可靠、有序交付保证。把音频放到 UDP,可以避开音频路径上的 TCP 重传等待,但缺失、迟到或乱序的音频仍需由接收与播放策略处理。传输协议不会自动决定哪些音频已经过时,更不会保证扬声器连续发声。RFC 768

小智的 UDP 接收分支提供了一个比"UDP 更实时"更具体的观察:它先检查 16 字节头部、消息类型和载荷长度,再检查序号。序号小于或等于已经接收的序号时,直接返回;序号向前跳跃时,先记告警,随后仍会继续尝试解密和提交音频。这段分支没有把缺号的数据补齐,也没有把后到的旧包重新排回去。

用一组假设序号可以读懂它的后果:若已接受 100,接着到达 102,且包格式与解密均通过,102 会被接受并更新接收序号;此时 101 再到达,就属于旧序号而被丢弃。这个例子是按代码推演的结果,不是丢包实验数据。它说明当前接收策略倾向继续向前处理,而不是等待缺号恢复。实际听感还取决于后续解码、丢帧处理和播放缓冲,本文没有据此评测音质。MQTT 客户端的 UDP 接收分支

因此,"网络比较差"还不足以决定换协议。如果已经测到 TCP 重传等待影响音频时效,UDP 是值得验证的方案;如果只知道声音断续,而尚未区分编码、队列、模型生成与网络阶段,换成 UDP 可能只是把等待变成丢失,原来的瓶颈仍然存在。

关闭会话,是最容易看清差别的地方

WebSocket 的 CloseAudioChannel() 重置 websocket_,在这条实现中不发送小智应用层的 goodbye 消息。这里说的是应用层 JSON 消息,不是断言底层库没有 WebSocket 关闭握手。

MQTT 的同名函数则销毁 UDP 对象;若由客户端主动关闭,还通过 MQTT 发出带 session_idgoodbye。如果是收到服务端针对当前会话的 goodbye,回调安排关闭 UDP,但不再回发一次,避免双方反复告别。该函数没有销毁 MQTT 客户端。

这正好解释了开篇情境中的另一种可能:设备在线、音频关闭,也可以是一次正常结束后的状态。MQTT 连接描述设备与消息代理的联系,音频通道描述本次语音交换所用的资源。两者既可能在故障中分开,也可能按设计在正常流程中分开。

所以监控页面如果只显示一个"在线"圆点,选用 MQTT+UDP 后尤其需要说明它指向哪件事。至少要能分辨控制连接、当前音频通道,以及最近一次真实媒体交换;同时,WebSocket 连接尚在也不能证明 Opus 解码、服务端生成或扬声器播放成功。这里建议补充的是观测含义,不是声称客户端已经提供了一套完整监控系统。

用一次完整语音交换决定接入方案

到这里,选择标准已经可以落到具体接入工作上。先确认计划使用的服务端真正支持什么协议,再比较自己愿意维护哪些状态。

如果当前目标是把一个支持小智 WebSocket 协议的服务端接通,且没有证据表明 TCP 等待已经成为主要问题,WebSocket 可以作为起点:一条连接关联文本和音频,沿连接、hello、二进制格式、解码与播放逐段排查。这个建议基于接入路径较集中,不是吞吐、功耗或弱网性能的测试结论。

如果需要让控制连接与语音会话独立存在,服务端支持 MQTT+UDP,也有能力维护 UDP 入口及其媒体观测,就可以将它纳入验证。收益是两路生命周期与传输策略可以分别处理;代价是 broker 可用不再足以判断音频路径,音频丢失与乱序的影响也需要单独验收。

下面这张接入验收表,可以直接作为测试记录的骨架。填入真实观察后再判断,而不是把函数名或日志中的"connected"直接写成"语音可用"。

一次交换中的观察 WebSocket 要确认什么 MQTT+UDP 要确认什么
开始语音前 真正建立了连接,不能只看 Start() 返回 broker 连接可用;这一步尚不代表 UDP 音频可用
应用层协商 收到匹配 transport 的 hello,核对实际音频与封包参数 MQTT 回复中的 UDP 地址、端口和加密参数通过解析
上行语音 服务端收到并能解码对应二进制载荷 服务端实际收到并能处理 UDP 音频;仅本地发送返回不够
下行播报 二进制音频经过接收、解码并实际播放 UDP 接收序号、解密、解码与播放都有对应观察
会话结束 连接关闭与应用状态收敛 UDP 关闭、goodbye 与会话对应;MQTT 可继续在线
再次开口 新一轮连接与协商成功 控制连接仍可用或已恢复,并重新完成音频通道协商

若要比较两种方案的体验,使用同一设备、音频输入、编解码参数、服务端模型及目标网络条件,分别记录建连与协商耗时、上行到达、首段下行音频到达、实际播放起点和中断恢复。两端没有可靠时钟同步时,不直接相减时间戳冒充单向网络时延;可以先用同一端的单调时钟测量本端事件间隔。

开篇的"MQTT 已连接但没有声音",应当沿这张表找到最后一个有证据的环节。缺 hello 就查协商,缺媒体到达就查音频路径,已经到达而无声再查解码与播放。完成这次定位后,才知道应该补齐当前方案,还是有理由承担另一种传输组合的维护成本。

相关推荐
智塑未来1 小时前
FPGA开发板选型:研发交付能力怎么看
人工智能·fpga开发
朴实赋能1 小时前
AI拒答机制实战:心理咨询危机识别Agent三层防护、五级分层与六类智能体拆解
大数据·人工智能·多agent协同·心理咨询ai·心理危机识别·ai拒答机制·隐私匿名化
阿里云云原生1 小时前
从钉群提问到任务交付:团队级 Agent 基础设施全链路实践
agent
史一试1 小时前
Agent开发第6步:实现 SSE 编解码
人工智能
熊猫钓鱼>_>2 小时前
【SenseNova U1.5 Lite实战】鸿蒙校园工具开发者适配原生统一多模态大模型全记录
人工智能·华为·ai·harmonyos·媒体·sensenova
龙亘川2 小时前
智慧景区建设实践|文旅热度持续走高,景区如何把流量稳稳转化为口碑?
大数据·人工智能·科技·信息可视化·智慧城市
天远API2 小时前
零信任架构实战:基于天远行驶OCR证识别构建自动化智能停车网关
运维·人工智能·架构·自动化
冗量2 小时前
Pi Agent Chord 架构深度剖析
架构·agent·pi agent
实名上网宋凯宣2 小时前
【Codex-智能体】
人工智能·codex·skills