Skip to content

MQTT 服务设计

后端 MQTT 客户端的模块设计、消息路由机制和处理管道。MQTT 协议定义见 06.MQTT通信协议,EMQX 配置见 09.EMQX配置与安全


1. MQTT Client 初始化

使用 mqtt.js 库,在 MqttModuleonModuleInit 生命周期中创建客户端实例。

连接参数

参数说明
brokerUrlmqtt://emqx:1883(开发)/ mqtts://emqx:8883(生产)EMQX 地址
clientIdbackend-{hostname}-{pid}后端服务标识,用于 EMQX 区分不同实例
usernamebackend_admin后端管理账号,EMQX 侧需配置 PostgreSQL 认证
password环境变量注入后端 MQTT 连接凭证
cleantrue不保留离线消息(后端不依赖持久化会话)
keepalive60 秒心跳间隔
reconnectPeriod1s → 2s → 4s → 最大 30s断线重连指数退避

TLS 配置(生产环境)

typescript
{
  ca: fs.readFileSync('/etc/emqx/certs/ca.pem'),
  rejectUnauthorized: true,
}

2. 装饰器式 Topic 路由

2.1 装饰器定义

三个核心装饰器:

装饰器作用域说明
@MqttController(prefix)声明 Topic 前缀,如 sys
@SubscribeTopic(pattern)方法声明 Topic 模式,支持通配符 +#
@MqttContext() / @Payload()参数注入 MQTT 上下文对象 / 解析后的消息体

2.2 路由注册流程

  1. 应用启动 → MqttModule.onModuleInit() 初始化 MQTT Client
  2. MqttExplorer 扫描所有带 @MqttController 的类
  3. 提取每个 @SubscribeTopic 方法 → 拼接完整 Topic:{prefix}/{pattern}
  4. 订阅时追加共享订阅前缀:$share/group1/{prefix}/{pattern}
  5. MQTT Client 收到消息 → 匹配 Topic → 调用对应方法 → 传入解析后的 Payload 和 Context

共享订阅确保多 NestJS 实例负载均衡,详见 09.EMQX配置与安全 第 4 节。设备认证由 EMQX PostgreSQL 认证插件直连数据库完成,后端无需实现 /api/v1/devices/auth/api/v1/devices/acl 端点。

2.3 路由表

订阅 Topic(共享订阅前缀省略)处理方法说明
sys/+/+/thing/event/property/posthandlePropertyPost属性上报
sys/+/+/thing/service/+/set_replyhandleSetReply控制指令回复
sys/+/+/thing/config/set_replyhandleConfigReply配置下发回复
sys/+/+/thing/ota/progresshandleOtaProgressOTA 进度上报
sys/+/+/thing/event/+/posthandleEventPost设备事件(告警等)

2.4 Context 解析

MqttContext 提供以下信息:

方法返回值说明
getTopicParams(){ productId, deviceId }从 Topic 中提取 +/+ 位置的变量值
getTopic()string完整 Topic 字符串
getClientId()stringMQTT ClientId(仅在需要物理 ID 时使用)

productIddeviceId 从 Topic 中按 +/+ 占位顺序提取:sys/{productId}/{deviceId}/thing/...


3. 消息处理管道

从 MQTT 消息到业务逻辑的完整处理链:

MQTT 消息到达

  ├─→ Topic 匹配 → 路由到 @SubscribeTopic 方法
  │     │
  │     ├─→ Payload 校验
  │     │     ├─ JSON.parse
  │     │     ├─ 验证必填字段(msgId、params)
  │     │     └─ 格式不符合 → 记录日志,丢弃(不回发 MQTT 错误)
  │     │
  │     ├─→ 设备身份校验
  │     │     ├─ 从 Topic 提取 deviceId
  │     │     └─ 查 PostgreSQL 确认设备存在且状态正常
  │     │
  │     └─→ 业务处理
  │           ├─ 属性上报 → DeviceService.updateShadow()
  │           ├─ set_reply → DeviceService.confirmCommand()
  │           ├─ config_reply → DeviceService.confirmConfig()
  │           └─ ota_progress → OtaService.updateProgress()

  └─→ 异常 → 记录日志 + 错误计数器 + 告警(连续失败超过阈值)

校验失败不回发 MQTT 错误消息——设备端不会订阅错误 Topic,回发只会造成额外的消息风暴。


4. 指令下发

后端主动向设备发布消息,通过 MqttService.publish() 方法封装。

下发 Topic 对照

Topic场景触发时机
sys/{pid}/{did}/thing/service/property/set用户控制设备小程序/Web 下发控制指令
sys/{pid}/{did}/thing/config/set注册后下发配置设备首次注册激活时
sys/{pid}/{did}/thing/ota/upgradeOTA 升级通知运维触发固件升级

发布参数

参数默认值说明
topic必填完整 Topic 路径
payload必填JSON 对象,自动序列化
qos1至少一次送达
retainfalse指令不需要保留消息

5. 断线重连与容错

策略配置
重连间隔指数退避:1s → 2s → 4s → 8s → 最大 30s,无限重试
重连后操作自动重新订阅所有 @SubscribeTopic 规则
EMQX 宕机MQTT Client 持续重连,HTTP API 正常响应不受影响
消息积压clean: true 不保留离线消息;设备端应缓存关键消息并在 Broker 恢复后补发

6. 与设备状态管理的集成

MQTT 消息到达后与设备状态的联动(引用 02.后台服务设计 3.2 节已修复的内容):

属性上报路径:

handlePropertyPost → DeviceService.updateShadow
  ├── Redis: HSET device:shadow:{id} (热缓存,WebSocket 推送用)
  ├── PostgreSQL: UPDATE shadow JSONB (权威数据)
  └── InfluxDB: 异步写入时序数据

指令回复路径:

handleSetReply → DeviceService.confirmCommand
  ├── 根据 msgId 查找指令记录
  ├── 更新指令状态为完成
  └── WebSocket 推送执行结果至前端

完整的两级存储设计原因和同步策略见 12.缓存策略 第 3 节。


7. 目录结构

src/gateways/mqtt/
├── mqtt.module.ts          # 动态模块,onModuleInit 初始化客户端
├── mqtt.service.ts         # 发布消息、获取客户端实例
├── mqtt.decorators.ts      # @SubscribeTopic, @MqttController, @Payload, @MqttContext
├── mqtt.explorer.ts        # 路由扫描器,发现并注册订阅
├── mqtt.interface.ts       # MqttContext 接口定义
└── handlers/               # 按业务拆分的消息处理器
    ├── property.handler.ts  # 属性上报
    ├── command.handler.ts   # 控制回复
    ├── config.handler.ts    # 配置回复
    └── ota.handler.ts       # OTA 进度