4. MQTT协议与设备影子通信
设备影子(Device Shadow)本质上是云端为每台设备维护的一份 JSON 文档,用来保存设备“期望状态(desired)”与“上报状态(reported)”之间的差异。设备离线时,应用仍可写入 desired;设备上线后拉取影子、执行变更、再上报 reported,影子随之收敛。MQTT 是承载这套机制最常用的协议,而主题设计、QoS、保留消息与遗嘱消息,决定了影子更新是否可靠、可追溯、可恢复。
一、主题设计:把影子通道拆成三条语义清晰的路由
影子通信不建议把所有消息塞进一个主题,而应按“请求—增量—结果”拆分为三条通道。以产品 envmon、设备 dev001 为例,可采用如下主题结构:
# 应用/服务端 → 设备:下发期望状态
$shadow/envmon/dev001/desired/update
# 设备 → 云端:上报当前状态
$shadow/envmon/dev001/reported/update
# 云端 → 订阅方:影子文档变更后的增量通知
$shadow/envmon/dev001/delta/notify
# 设备 → 云端:请求拉取完整影子文档
$shadow/envmon/dev001/get
# 云端 → 设备:返回完整影子文档
$shadow/envmon/dev001/get/accepted
主题层级遵循“固定前缀 / 产品 / 设备 / 语义 / 动作”的顺序,便于用通配符做权限隔离。例如运维平台只订阅 $shadow/envmon/+/delta/notify 即可监听全部设备的影子变化;而设备端只被授权发布到自己的 reported/update,订阅自己的 desired/update 与 get/accepted,避免越权写入他人影子。主题中不要使用设备自增 ID 或含空格、中文的字段,保持大小写一致,否则在 ACL 与规则引擎中容易匹配失败。
二、QoS 选择:按消息语义而非“越可靠越好”
QoS 决定投递保证等级,但并非越高越好。QoS 2 需要四次握手,会显著增加设备功耗与云端连接数压力。对影子通信,可按下列原则选择:
| 消息类型 | 建议 QoS | 理由 |
|---|---|---|
| desired/update(下发期望) | QoS 1 | 允许重复,设备端按版本号幂等处理即可 |
| reported/update(状态上报) | QoS 1 | 保证至少到达一次,重复上报可被版本覆盖 |
| delta/notify(增量通知) | QoS 0 或 1 | 通知可丢,设备可主动 get 补齐 |
| get / get/accepted(拉取影子) | QoS 1 | 请求与响应需成对,避免设备空等 |
| 遗嘱消息(LWT) | QoS 1 | 离线告警必须送达,重复可接受 |
关键点是:影子本身带有版本号(version),设备端处理 desired 时应比较版本,旧版本直接丢弃。因此 QoS 1 的重复投递不会造成状态错乱,反而比 QoS 2 更省资源。若某条消息丢失,设备重连后通过 get 拉取完整影子即可自愈,这就是“QoS 1 + 版本幂等 + 主动拉取”的组合价值。
三、保留消息与遗嘱消息:解决“上线即知”和“掉线即知”
保留消息(Retained Message)让 broker 为某个主题保存最后一条消息,新订阅者一订阅就立即收到。它非常适合影子场景中的“设备上线即拿到最新期望状态”。例如应用向 desired/update 发布一条 retained 消息,设备重连订阅后无需等待下一次下发,立刻收到当前期望值。但要注意:retained 消息只保留最后一条,且不应滥用在高频上报主题上,否则 broker 存储与订阅风暴会带来额外开销。
遗嘱消息(Last Will and Testament, LWT)在设备异常断开时由 broker 代为发布。设备连接时声明:
{
"topic": "$shadow/envmon/dev001/status",
"qos": 1,
"retain": true,
"payload": "{\"state\":\"offline\",\"reason\":\"unexpected\"}"
}
这样设备掉线后,订阅 status 的应用会立即收到离线通知,并可将影子中的 reported 标记为不可信。注意 LWT 只在非正常断开时触发;设备主动发送 DISCONNECT 不会触发遗嘱,因此正常关机应由设备自己发布一条 online=false 的状态消息。
四、影子更新消息流:一次完整的期望—执行—上报闭环
下面用一张图展示应用下发期望、设备执行并上报、影子收敛的完整消息流。图中同时标出 QoS、retained 与 LWT 的作用位置。
图中可见,一次完整闭环包含:应用发布 desired(QoS 1,retained)→ Broker 更新影子并计算 delta → 设备收到 delta 后执行变更 → 设备上报 reported(QoS 1)→ Broker 收敛影子并通知应用。若设备在任意环节异常断开,LWT 会立即发布离线状态,应用据此判断影子中的 reported 是否可信。
五、实践要点与常见问题
第一,设备端必须实现版本比较。收到 desired 时,若消息中的 version 小于本地已处理版本,直接丢弃,避免 QoS 1 重复投递导致重复执行。第二,retained 消息只用于“最新期望”这类低频、状态型主题,不要用于高频遥测。第三,LWT 的 retain 建议设为 true,这样新订阅者也能立即知道设备离线。第四,设备重连后应先订阅 get/accepted,再发布 get,最后才订阅 desired/update,防止在拉取过程中漏掉新的期望变更。第五,主题 ACL 要按设备粒度隔离,禁止设备订阅 $shadow/+/+/desired/update 这类过宽通配符。