4. MQTT直连设备实战

Azure IoT Hub 内置了 MQTT 3.1.1 协议端点,设备可以不借助任何 SDK,直接用标准 MQTT 客户端库接入。理解这条“裸协议”路径,是排查设备连不上、消息发不出、命令收不到等问题的关键——因为 SDK 只是把同样的报文封装了一层。本节以一台名为 dev-sensor-01 的温度采集设备为例,走完“建立连接 → 订阅云端下行主题 → 发布遥测 → 处理确认与错误”的完整链路。

一、连接三要素:地址、ClientId 与认证

IoT Hub 的 MQTT 端点固定为 {your-hub}.azure-devices.net:8883,使用 TLS 加密,因此客户端必须启用 TLS 并校验服务器证书。与普通 MQTT Broker 最大的不同在于身份标识:IoT Hub 用 ClientId 承载设备身份,格式为 {deviceId},即设备注册时使用的 Device ID。若 ClientId 与认证凭据中的设备不匹配,连接会被直接拒绝。

认证方式有两种。第一种是 SAS Token,把设备密钥对 {resourceUri}\n{expiry} 做 HMAC-SHA256 后 Base64 编码,拼成如下形式:

SharedAccessSignature sr=myhub.azure-devices.net%2Fdevices%2Fdev-sensor-01
  &sig={Base64(HMAC-SHA256(key, resourceUri + "\n" + expiry))}
  &se=1735689600

其中 se 是过期时间的 Unix 秒。第二种是 X.509 证书,客户端用设备证书做 TLS 双向认证,此时用户名可留空。教学示例中我们统一使用 SAS Token,并把它作为 MQTT 的 password,username 填 {hub}.azure-devices.net/{deviceId}/?api-version=2021-04-12。

SAS Token 的 se 建议设置为当前时间 + 1 小时,并在到期前主动重连刷新。过期后 IoT Hub 会以 CONNACK 返回 0x05(Not authorized),而不是直接断开 TCP。

二、连接确认:读懂 CONNACK 返回码

MQTT 客户端发出 CONNECT 后,Broker 回一个 CONNACK 报文,其中 return code 字段决定了后续行为。IoT Hub 的返回码语义与标准 MQTT 基本一致,但有几处需要特别注意:

返回码含义典型原因与处理
0x00连接已接受可立即订阅下行主题
0x01协议版本不支持未使用 MQTT 3.1.1,检查客户端版本
0x02ClientId 不合法ClientId 与设备 ID 不一致或含非法字符
0x04用户名或密码错误SAS Token 签名错误、username 格式不对
0x05未授权Token 过期、设备被禁用或已删除

收到 0x00 后,客户端应立刻发起订阅,而不是先发遥测。因为 IoT Hub 对下行消息采用“订阅后才投递”的机制,若设备未订阅 C2D 主题,云端下发的消息会被静默丢弃或返回错误。

MQTT 直连 IoT Hub 的报文交互与主题映射 设备 dev-sensor-01 MQTT 客户端库 CONNECT ClientId / SAS Token SUBSCRIBE devices/.../messages/devicebound/# PUBLISH devices/.../messages/events/ PUBACK / 错误 QoS 1 确认或拒绝 断线重连 + Token 刷新 Azure IoT Hub myhub.azure-devices.net:8883 CONNACK 0x00 校验 ClientId 与签名 SUBACK 登记下行订阅 路由到遥测管道 Event Hubs 兼容端点 PUBACK / 拒绝 配额、限流、格式错误 限流:每设备 8KB/s 上行 主题映射规则 上行遥测 devices/{id}/messages/ events/ 下行命令 devices/{id}/messages/ devicebound/# 双胞胎属性 $iothub/twin/PATCH/ properties/reported/ 直接方法 $iothub/methods/ POST/{methodName}

三、订阅下行主题:C2D、双胞胎与方法

IoT Hub 把三类下行能力映射到不同主题,设备需要按需订阅:

  • 云到设备消息(C2D):devices/{deviceId}/messages/devicebound/#。订阅后,云端通过服务端 API 发送的消息会以 PUBLISH 到达,QoS 1。
  • 双胞胎期望属性:$iothub/twin/PATCH/properties/desired/#,用于接收云端下发的配置变更。
  • 直接方法调用:$iothub/methods/POST/#,方法名在主题末段,响应需发布到 $iothub/methods/res/{status}/?$rid={rid}。

订阅时 QoS 建议用 1,SUBACK 返回的 granted QoS 若为 0x80,说明该主题不被允许,通常是主题拼写错误或设备无权限。C2D 消息处理完后,设备应主动发送 PUBACK;若使用 QoS 1 且未确认,IoT Hub 会在超时后重投,导致重复消费。

四、发布遥测:主题、QoS 与消息属性

上行遥测统一发布到 devices/{deviceId}/messages/events/。主题后面可以追加属性,IoT Hub 会把这些属性作为消息的系统属性透传给下游:

devices/dev-sensor-01/messages/events/
  .temperature=23.5
  .humidity=48
  .$.ct=application%2Fjson
  .$.ce=utf-8

其中 . 前缀表示自定义属性,$.ct 与 $.ce 分别映射到 Content-Type 和 Content-Encoding。Payload 建议用 JSON:

{"deviceId":"dev-sensor-01","temperature":23.5,"humidity":48,"ts":1735686000}

发布时 QoS 选 1,这样能拿到 PUBACK。若 IoT Hub 因限流或消息过大拒绝,客户端会收到断开或 PUBACK 缺失。单条消息上限为 256 KB,单设备上行吞吐约 8 KB/s,超出会触发限流,表现为连接被短暂断开后重连。

五、错误处理与重连策略

实战中最常见的错误有三类。第一类是 CONNACK 0x05,几乎都是 SAS Token 过期或设备被禁用,处理方式是重新生成 Token 并重连。第二类是 订阅后收不到 C2D,先确认订阅主题是否带 /# 通配,再检查设备是否处于启用状态。第三类是 发布后无 PUBACK,通常是 QoS 0 或网络中断,应改为 QoS 1 并实现超时重发。

不要在断线后立即高频重连。IoT Hub 对重连频率有限制,建议采用指数退避:首次 1 秒,之后 2、4、8 秒,上限 30 秒,并在每次重连成功后重新订阅所有下行主题。

一个可复用的连接状态机可以这样组织:DISCONNECTED → CONNECTING → CONNECTED → SUBSCRIBING → READY,任一环节失败回到 DISCONNECTED 并触发退避。只有进入 READY 后才允许发布遥测,这样能避免在未订阅状态下丢失下行命令。

六、最小可运行验证清单

完成上述配置后,用下面的检查表逐项确认,即可判断设备是否真正接入成功:

检查项期望结果不通过时的排查方向
TCP 8883 可达TLS 握手成功防火墙、代理、DNS
CONNACK 返回码0x00ClientId、SAS Token、设备状态
SUBACK granted QoS1主题拼写、权限
发布后收到 PUBACK1 秒内QoS 设置、限流、消息大小
云端收到遥测Event Hubs 端点可见主题路径、属性格式
C2D 消息到达设备端回调触发订阅是否生效、设备是否启用

把这张表跑通,MQTT 直连的链路就算真正打通了。后续无论是换用 SDK 还是接入 IoT Edge,底层交互逻辑都与本节一致,只是封装层次不同。