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。
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,检查客户端版本 |
| 0x02 | ClientId 不合法 | ClientId 与设备 ID 不一致或含非法字符 |
| 0x04 | 用户名或密码错误 | SAS Token 签名错误、username 格式不对 |
| 0x05 | 未授权 | Token 过期、设备被禁用或已删除 |
收到 0x00 后,客户端应立刻发起订阅,而不是先发遥测。因为 IoT Hub 对下行消息采用“订阅后才投递”的机制,若设备未订阅 C2D 主题,云端下发的消息会被静默丢弃或返回错误。
三、订阅下行主题: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 并实现超时重发。
一个可复用的连接状态机可以这样组织:DISCONNECTED → CONNECTING → CONNECTED → SUBSCRIBING → READY,任一环节失败回到 DISCONNECTED 并触发退避。只有进入 READY 后才允许发布遥测,这样能避免在未订阅状态下丢失下行命令。
六、最小可运行验证清单
完成上述配置后,用下面的检查表逐项确认,即可判断设备是否真正接入成功:
| 检查项 | 期望结果 | 不通过时的排查方向 |
|---|---|---|
| TCP 8883 可达 | TLS 握手成功 | 防火墙、代理、DNS |
| CONNACK 返回码 | 0x00 | ClientId、SAS Token、设备状态 |
| SUBACK granted QoS | 1 | 主题拼写、权限 |
| 发布后收到 PUBACK | 1 秒内 | QoS 设置、限流、消息大小 |
| 云端收到遥测 | Event Hubs 端点可见 | 主题路径、属性格式 |
| C2D 消息到达 | 设备端回调触发 | 订阅是否生效、设备是否启用 |
把这张表跑通,MQTT 直连的链路就算真正打通了。后续无论是换用 SDK 还是接入 IoT Edge,底层交互逻辑都与本节一致,只是封装层次不同。