适用对象 :初中级物联网开发工程师、嵌入式开发者、后端开发人员、IoT平台接入工程师
协议重点 :MQTT 3.1.1,兼顾 MQTT 5.0
实践重点:Broker 部署、设备接入、Topic 设计、QoS、心跳、LWT、Retain、会话、安全与故障排查
1. 前言与概述
1.1 MQTT 是什么?
MQTT(Message Queuing Telemetry Transport) 是一种轻量级的、基于发布/订阅模型的消息传输协议。
它最初由 IBM 的 Andy Stanford-Clark 和 Arlen Nipper 等人推动,用于资源受限、网络质量不稳定的设备之间进行消息通信。
MQTT 的核心思想可以概括成一句话:
设备不直接找设备通信,而是把消息交给 Broker,由 Broker 按 Topic 将消息转发给订阅者。
典型结构:
MQTT Broker
┌───────────────┐
│ │
发布 │ │ 订阅
设备 A ──────────►│ Broker │──────────► 后端服务器
│ │
发布 │ │ 订阅
设备 B ──────────►│ │──────────► 手机APP
│ │
└───────────────┘
▲
│
订阅/发布
│
网关设备
MQTT 运行在 TCP/IP 等能够提供有序、可靠双向字节流的传输之上,并通过发布/订阅模型实现消息解耦。MQTT 3.1.1 是 MQTT 的第一个 OASIS 标准版本,同时标准化为 ISO/IEC 20922:2016;MQTT 5.0 于 2019 年成为 OASIS 标准。
1.2 MQTT 的发展历史
可以简单理解为:
1990s
│
│ IBM 等推动 MQTT
▼
MQTT 3.1
│
▼
MQTT 3.1.1
│
│ 2014 OASIS 标准
▼
MQTT 5.0
│
│ 2019 OASIS 标准
▼
现代 IoT / 工业互联网 / 车联网
MQTT 3.1.1
MQTT 3.1.1 是目前大量传统 IoT 系统仍在使用的版本。
特点:
-
协议简单
-
客户端实现成本低
-
Broker 支持广泛
-
QoS 0/1/2
-
Retain
-
LWT
-
Clean Session
-
Topic 通配符
-
用户名/密码
-
TLS
1.3 MQTT 5.0 相比 3.1.1 增加了什么?
MQTT 5.0 不是重新设计了一套 MQTT,而是在 MQTT 3.1.1 核心模型基础上进行了大量增强。
| 能力 | MQTT 3.1.1 | MQTT 5.0 |
|---|---|---|
| 发布/订阅 | ✅ | ✅ |
| QoS 0/1/2 | ✅ | ✅ |
| Retain | ✅ | ✅ |
| LWT | ✅ | ✅ |
| Clean Session | ✅ | 改进 |
| Clean Start | ❌ | ✅ |
| Session Expiry | ❌ | ✅ |
| Reason Code | 基础 | 更丰富 |
| Properties | ❌ | ✅ |
| Message Expiry | ❌ | ✅ |
| Topic Alias | ❌ | ✅ |
| Receive Maximum | ❌ | ✅ |
| Maximum Packet Size | ❌ | ✅ |
| Server Disconnect Reason | 较弱 | ✅ |
| Request/Response 模式支持 | 需自行设计 | 更完善 |
| User Properties | ❌ | ✅ |
MQTT 5.0 的设计目标之一就是增强大型系统扩展性、错误报告、请求/响应模式、扩展机制以及小型客户端支持。
实际开发建议
如果你正在开发一个新 IoT 系统:
Broker 和客户端都支持 MQTT 5.0 时,优先考虑 MQTT 5.0。
但如果需要兼容大量已有设备:
MQTT 3.1.1 仍然是非常重要的兼容版本。
2. 核心概念解析
2.1 Broker
Broker(消息代理) 是 MQTT 系统的核心服务器。
它负责:
-
接收客户端连接
-
维护客户端连接状态
-
接收 PUBLISH 消息
-
管理订阅关系
-
根据 Topic 匹配订阅者
-
转发消息
-
处理 QoS
-
管理 Retained Message
-
管理离线会话
-
处理 LWT
-
执行认证和 ACL
例如:
设备:
client_id = device-001
发布:
iot/device/device-001/telemetry
消息:
{
"temperature": 26.5
}
Broker 收到后,会查找哪些客户端订阅了:
iot/device/device-001/telemetry
然后把消息转发给这些客户端。
2.2 Client
Client(客户端) 是连接 MQTT Broker 的任何程序或设备。
例如:
-
STM32
-
ESP32
-
RK3399 工控机
-
Linux 网关
-
Windows 程序
-
Java 后端
-
Python 服务
-
Go 服务
-
手机 App
-
Web 前端
因此:
MQTT Client 不等于"设备"。
服务器程序同样可以是 MQTT Client。
例如:
传感器 ─────► Broker ◄───── Python程序
▲
│
└──────── Go程序
2.3 Connection
Connection 指 Client 与 Broker 之间建立的网络连接。
通常:
Client
│
│ TCP
▼
Broker
常见情况下 MQTT 建立在 TCP 上:
应用层:MQTT
传输层:TCP
网络层:IP
如果使用 TLS:
应用层:MQTT
安全层:TLS
传输层:TCP
网络层:IP
2.4 发布/订阅模型
MQTT 最大的特点之一就是:
Publish / Subscribe(发布/订阅)
传统 TCP 通信:
设备 A ───────────► 服务器 B
设备 A 必须知道服务器 B。
MQTT:
设备 A ──Publish──► Broker
│
├──► 服务器 B
├──► 手机 C
└──► 网关 D
设备 A 不需要知道 B、C、D 的 IP 地址。
它只需要:
publish(topic, payload)
这就是 MQTT 的解耦。
2.5 Topic
Topic 是 MQTT 消息的逻辑地址。
例如:
device/001/temperature
可以理解为:
device
└── 001
└── temperature
再比如:
iot/device/001/status
iot/device/001/telemetry
iot/device/001/command
iot/device/001/config
2.6 Payload
Payload 是消息实际携带的数据。
例如:
Topic:
iot/device/001/temperature
Payload:
26.5
或者:
{
"deviceId": "001",
"temperature": 26.5,
"humidity": 68.2,
"timestamp": 1787550000
}
MQTT 本身不规定 Payload 的业务格式。
Payload 可以是:
-
JSON
-
XML
-
二进制
-
Protobuf
-
MessagePack
-
自定义协议
这也是 MQTT 灵活的重要原因之一。
3. MQTT 工作原理与网络模型
3.1 MQTT 通信整体流程
典型流程:
Client
│
│ TCP Connect
▼
Broker
│
│ CONNECT
▼
Broker
│
│ CONNACK
▼
Client
│
├── SUBSCRIBE
│
├── PUBLISH
│
├── PINGREQ
│
└── DISCONNECT
3.2 CONNECT / CONNACK
MQTT 客户端建立 TCP 连接后,需要发送:
CONNECT
Broker 返回:
CONNACK
MQTT 3.1.1 的 CONNECT 中通常包含:
Protocol Name
Protocol Level
Connect Flags
Keep Alive
Client ID
Will
Username
Password
例如:
Client
│
│ CONNECT
│ ClientID=device-001
│ KeepAlive=60
│ Username=device001
▼
Broker
│
│ CONNACK
│ Return Code = 0
▼
Client
MQTT 3.1.1 CONNACK 返回码:
| 返回码 | 含义 |
|---|---|
| 0 | Connection Accepted |
| 1 | 不支持的协议版本 |
| 2 | Client Identifier 被拒绝 |
| 3 | Broker 不可用 |
| 4 | 用户名或密码错误 |
| 5 | 未授权 |
3.3 Keep Alive
Keep Alive 是 MQTT 非常重要的机制。
例如:
Keep Alive = 60 秒
表示客户端和 Broker 约定:
在正常通信情况下,不能长时间没有 MQTT 控制报文。
如果客户端在这个时间内没有其他 MQTT 报文发送,就发送:
PINGREQ
Broker 返回:
PINGRESP
流程:
sequenceDiagram
participant C as Client
participant B as Broker
C->>B: CONNECT
B-->>C: CONNACK
Note over C,B: 一段时间没有 MQTT 报文
C->>B: PINGREQ
B-->>C: PINGRESP
Note over C,B: 继续保持连接
MQTT 3.1.1 中,如果 Broker 在规定时间内没有收到客户端的 MQTT 控制报文,它可以判断连接失效;规范中的超时判断涉及 1.5 × Keep Alive。
Keep Alive 应该设置多大?
不要简单认为越小越好。
例如:
10 秒
优点:
- 断线检测快
缺点:
-
心跳频繁
-
蜂窝网络耗电
-
增加网络流量
工业设备常见思路:
30~120 秒
具体应该根据:
-
网络类型
-
设备功耗
-
断线检测要求
-
NAT 超时时间
-
Broker 负载
综合确定。
3.4 正常断开
客户端主动断开:
Client
│
│ DISCONNECT
▼
Broker
Broker 收到 DISCONNECT 后知道:
这是一次正常断开。
因此通常不会触发 LWT。
3.5 异常断开
例如:
设备突然断电
或者:
4G 网络突然断开
或者:
TCP 连接异常中断
客户端没有机会发送:
DISCONNECT
Broker 最终发现:
连接已经异常消失
此时可以发布 LWT。
4. 报文结构与 QoS
4.1 MQTT 控制报文
MQTT 控制报文基本结构:
┌──────────────────────┐
│ Fixed Header │
├──────────────────────┤
│ Variable Header │
├──────────────────────┤
│ Payload │
└──────────────────────┘
Fixed Header
所有 MQTT 控制报文都有固定头。
例如:
PUBLISH
SUBSCRIBE
PUBACK
CONNECT
CONNACK
PINGREQ
PINGRESP
DISCONNECT
固定头包含:
Message Type
Flags
Remaining Length
4.2 QoS 是什么?
QoS:
Quality of Service,服务质量
MQTT 定义三个等级:
QoS 0
QoS 1
QoS 2
可以简单理解:
| QoS | 含义 | 是否可能丢失 | 是否可能重复 | 开销 |
|---|---|---|---|---|
| 0 | 最多一次 | 是 | 一般不会 | 最低 |
| 1 | 至少一次 | 正常协议流程下尽量避免 | 是 | 中 |
| 2 | 恰好一次 | 正常协议流程下避免 | 协议层避免重复交付 | 最高 |
注意:
MQTT 的 QoS 不能直接等同于"业务一定只执行一次"。
例如:
QoS 2
可以保证 MQTT 协议层的消息交付语义,但如果你的业务程序:
收到消息
↓
执行数据库 INSERT
↓
程序崩溃
↓
重新处理
仍然需要业务层设计幂等机制。
4.3 QoS 0:最多一次
QoS 0:
At Most Once
也就是:
发一次,不等待确认。
流程:
sequenceDiagram
participant C as Client
participant B as Broker
C->>B: PUBLISH QoS 0
Note over C,B: 无 PUBACK
特点:
-
只有一次 PUBLISH
-
没有 ACK
-
网络断开可能丢失
-
开销最低
-
性能最高
适合:
实时温度
实时湿度
CPU使用率
GPS位置
实时电流
实时电压
例如:
每秒上报一次温度
26.1
26.2
26.3
26.4
26.5
丢一个:
26.3
通常没关系。
4.4 QoS 1:至少一次
QoS 1:
At Least Once
发送:
PUBLISH
Broker 返回:
PUBACK
流程:
sequenceDiagram
participant C as Client
participant B as Broker
C->>B: PUBLISH QoS 1
B-->>C: PUBACK
如果客户端没有收到 PUBACK:
Client
│
│ PUBLISH
▼
Broker
X
│
│ PUBACK 丢失
X
Client
客户端可能再次发送:
PUBLISH
于是 Broker 可能收到两次。
所以:
QoS 1 允许重复。
4.5 QoS 1 为什么叫"至少一次"?
假设:
发送 PUBLISH
Broker 已经成功接收。
但:
PUBACK
在网络中丢失。
客户端认为:
没有成功
于是重新发送:
PUBLISH
Broker 可能再次收到。
所以:
消息至少到达一次
但:
可能到达多次
因此:
QoS 1 + 业务幂等 = IoT 系统中非常常见的可靠消息方案。
4.6 QoS 2:恰好一次
QoS 2:
Exactly Once
MQTT 3.1.1 使用四步握手:
PUBLISH
PUBREC
PUBREL
PUBCOMP
完整流程:
sequenceDiagram
participant C as Client
participant B as Broker
C->>B: PUBLISH QoS 2
B-->>C: PUBREC
C->>B: PUBREL
B-->>C: PUBCOMP
Note over C,B: QoS 2 交互完成
4.7 QoS 2 为什么需要四次交互?
可以把它理解成一个状态机。
第一步:PUBLISH
客户端说:
"我有一条 QoS 2 消息。"
C ─── PUBLISH ───► B
第二步:PUBREC
Broker:
"我已经收到。"
C ◄── PUBREC ─── B
第三步:PUBREL
Client:
"好的,现在可以释放/继续处理这条消息了。"
C ─── PUBREL ───► B
第四步:PUBCOMP
Broker:
"整个 QoS 2 流程完成。"
C ◄── PUBCOMP ─── B
因此:
PUBLISH
↓
PUBREC
↓
PUBREL
↓
PUBCOMP
4.8 三种 QoS 如何选择?
一个非常实用的判断方法:
QoS 0
问自己:
"这条消息丢了没关系吗?"
如果:
是
选择:
QoS 0
QoS 1
问自己:
"消息不能轻易丢,但重复可以通过业务幂等解决吗?"
如果:
是
选择:
QoS 1
这是绝大多数 IoT 业务非常实用的选择。
QoS 2
问自己:
"业务确实需要 MQTT 层的 Exactly Once 交付语义,而且能够接受更高协议开销吗?"
如果:
是
再考虑:
QoS 2
不要因为:
"QoS 2 最可靠"
就把所有消息都设置成 QoS 2。
4.9 QoS 的一个重要规则
QoS 不是简单地说:
客户端设置 QoS 2
=
Broker 一定按照 QoS 2 给订阅者
发布者和订阅者之间的 QoS 需要结合实际订阅 QoS 和 Broker 的转发规则理解。
实际工程中可以简单记住:
发布端 QoS 与订阅端 QoS 都需要合理设计。
5. Topic 与通配符
5.1 Topic 的层级结构
Topic 使用:
/
作为层级分隔符。
例如:
iot/device/001/temperature
可以设计成:
iot
└── device
└── 001
└── temperature
推荐按照:
产品 / 设备 / 数据类型
设计。
例如:
iot/device/001/telemetry
iot/device/001/status
iot/device/001/event
iot/device/001/command
iot/device/001/config
5.2 推荐 Topic 设计
例如充电桩系统:
charger/{chargerId}/telemetry
charger/{chargerId}/status
charger/{chargerId}/event
charger/{chargerId}/command
charger/{chargerId}/config
例如:
charger/CHG001/telemetry
charger/CHG001/status
charger/CHG001/event
charger/CHG001/command
charger/CHG001/config
这种设计比:
temperature
更适合大型 IoT 系统。
5.3 单级通配符 +
+ 只能匹配一个层级。
例如:
charger/+/status
可以匹配:
charger/001/status
charger/002/status
charger/ABC/status
但是不能匹配:
charger/001/device/status
因为:
+
只代表一个 Topic Level。
5.4 多级通配符 #
# 可以匹配多个层级。
例如:
charger/#
可以匹配:
charger/001/status
charger/001/telemetry
charger/001/event
charger/002/status
charger/002/telemetry
甚至:
charger
及其后续层级。
5.5 通配符使用规则
+ 必须占据完整一级
正确:
charger/+/status
错误:
charger/+001/status
错误:
charger/001+/status
# 必须位于末尾
正确:
charger/#
正确:
charger/001/#
错误:
charger/#/status
5.6 通配符性能问题
千万不要让大量业务客户端无脑订阅:
#
或者:
iot/#
原因是 Broker 需要:
消息
↓
Topic匹配
↓
大量订阅关系
↓
大量客户端转发
如果:
10000个客户端
都订阅:
#
那么系统消息分发压力可能非常大。
生产环境应当:
按照业务边界精确订阅 Topic。
5.7 $SYS
很多 Broker 提供:
$SYS/...
用于系统状态监控。
例如:
$SYS/#
可能可以查看:
-
Broker 状态
-
客户端数量
-
消息数量
-
网络流量
-
Topic 状态
但是:
$SYS不是所有 Broker 都保证完全一致。
具体 Topic 结构需要查看对应 Broker 文档。
6. 高级特性
6.1 遗嘱消息 LWT
LWT:
Last Will and Testament
中文通常称:
遗嘱消息
它解决的问题是:
设备异常断开后,如何通知其他系统?
6.1.1 为什么需要 LWT?
假设设备:
device-001
正常运行:
status = online
突然:
设备断电
设备没有机会发送:
status = offline
Broker 可以在客户端连接时提前保存一条 Will Message:
Topic:
device/001/status
Payload:
offline
如果客户端异常断开:
Broker
│
│ 检测异常断开
▼
发布 Will Message
6.1.2 LWT 流程
sequenceDiagram
participant D as Device
participant B as Broker
participant S as Server
D->>B: CONNECT + Will Message
B-->>D: CONNACK
D->>B: PUBLISH status=online
B->>S: status=online
Note over D: 设备突然断电
B->>B: 检测 TCP/KeepAlive 异常
B->>S: PUBLISH status=offline
6.1.3 LWT 最典型应用
device/{id}/status
在线:
{
"status": "online"
}
离线:
{
"status": "offline"
}
这样后台系统就可以实时维护:
设备在线列表
设备离线列表
设备最后在线时间
6.2 Retained Message
Retain 是 MQTT 中另一个非常重要的机制。
普通消息:
Client A
│
│ PUBLISH
▼
Broker
│
└──► 当前在线订阅者
如果 Client B 后来才订阅:
Client B
│
│ SUBSCRIBE
▼
Broker
默认情况下:
B 不会收到以前已经发送的普通消息。
但是如果发布消息时设置:
retain = true
Broker 可以保存该 Topic 的最后一条 retained message。
例如:
Topic:
device/001/status
Payload:
online
retain=true
以后新客户端订阅:
device/001/status
Broker 可以立即把:
online
发送给它。
6.2.1 Retain 的典型应用
非常适合:
设备当前状态
设备当前配置
设备当前模式
设备在线状态
例如:
device/001/status = online
device/001/mode = auto
device/001/config = {...}
6.3 Retain 与普通消息的区别
| 类型 | Broker保存 | 新订阅者能收到旧状态 |
|---|---|---|
| 普通消息 | 否 | 否 |
| Retained Message | 是 | 是 |
因此:
Retain 更适合"当前状态",不适合充当完整历史消息队列。
例如:
temperature=26
temperature=27
temperature=28
如果全部 retain:
最终通常只需要保存当前状态 28。
如果需要历史数据:
MQTT
↓
Kafka / Redis / MySQL / 时序数据库
应该由业务系统持久化。
6.4 Clean Session / Clean Start
这是 MQTT 会话机制中最容易混淆的部分之一。
MQTT 3.1.1
使用:
Clean Session
例如:
Clean Session = true
表示:
不需要持久化该客户端的 MQTT 会话状态。
如果:
Clean Session = false
Broker 可以维护持久会话。
包括:
-
订阅关系
-
QoS 1/2 未完成消息
-
离线期间符合条件的消息
6.5 MQTT 5.0
MQTT 5.0 把这一机制拆得更加清晰:
Clean Start
+
Session Expiry Interval
可以理解成:
Clean Start
↓
这次连接是否重新开始会话?
Session Expiry
↓
断开后会话保留多久?
例如:
Clean Start = false
Session Expiry = 3600
表示:
断线后会话可以保留 3600 秒。
6.6 离线消息
例如:
设备
│
│ 断网
X
Broker
设备断线期间:
服务器 ──► command
如果对应会话满足持久化条件,Broker 可以保存符合规则的 QoS 消息。
设备重新连接:
设备
│
│ CONNECT
▼
Broker
│
└──► 离线期间消息
实际使用时要注意:
离线消息不是"Broker 永久保存所有消息"。
它受到:
-
Session
-
QoS
-
Broker 配置
-
Session Expiry
-
Message Expiry
-
队列限制
等因素影响。
7. 安全与认证机制
7.1 MQTT 本身不等于安全
非常重要:
MQTT
本身并不自动提供:
加密
身份认证
权限控制
生产环境不能简单地:
互联网
↓
1883
↓
MQTT Broker
然后认为系统安全。
7.2 TLS
推荐:
MQTT
↓
TLS
↓
TCP
常见端口:
1883 MQTT/TCP
8883 MQTT/TLS
TLS 可以保护:
用户名
密码
Payload
Topic
MQTT控制报文
避免被网络中间人直接窃听。
7.3 单向 TLS
常见方式:
Client
│
│ TLS
▼
Broker
Client 验证 Broker 证书。
适合大多数互联网 IoT 系统。
7.4 双向 TLS / mTLS
更高安全级别:
Client
│
│ Client Certificate
▼
Broker
│
│ Broker Certificate
▼
Client
双方都验证证书。
特别适合:
-
工业互联网
-
车联网
-
企业 IoT
-
高安全设备
7.5 用户名密码
最常见的应用层认证:
username
password
例如:
username=device001
password=********
但是:
用户名密码一定应该配合 TLS 使用。
否则密码可能在网络上被窃听。
7.6 Token
也可以使用:
username = device001
password = JWT/Token
或者由 Broker 的认证扩展机制实现 Token/JWT。
典型结构:
设备
│
│ Token
▼
认证服务
│
▼
Broker
7.7 ACL
ACL:
Access Control List
解决:
这个 Client 能访问哪些 Topic?
例如设备:
device001
只允许:
publish:
device/device001/telemetry
subscribe:
device/device001/command
device/device001/config
禁止:
publish:
device/device002/telemetry
也禁止:
subscribe:
#
7.8 ACL 设计原则
推荐:
一个设备
↓
一个身份
↓
限定自己的 Topic
例如:
device/{deviceId}/telemetry
device/{deviceId}/status
device/{deviceId}/command
而不是:
所有设备共享一个账号
否则一台设备泄露账号后:
所有设备都可能受到影响。
8. 实战指南与代码示例
8.1 Python MQTT 客户端
这里使用 Eclipse Paho MQTT Python。
Paho Python 客户端支持 MQTT 3.1、3.1.1 和 5.0,并提供连接、发布、订阅、回调和网络循环等功能。当前文档推荐使用新版 Callback API。
安装:
pip install paho-mqtt
8.2 完整 MQTT 3.1.1 示例
下面实现:
-
连接 Broker
-
设置 Client ID
-
设置用户名密码
-
设置 LWT
-
订阅 Topic
-
发布消息
-
接收消息
-
自动网络循环
-
正常退出
import json
import timeimport paho.mqtt.client as mqtt
=========================
MQTT Broker 配置
=========================
BROKER_HOST = "127.0.0.1"
BROKER_PORT = 1883USERNAME = "iot_user"
PASSWORD = "iot_password"CLIENT_ID = "python-device-001"
当前设备自己的 Topic
STATUS_TOPIC = f"device/{CLIENT_ID}/status"
TELEMETRY_TOPIC = f"device/{CLIENT_ID}/telemetry"
COMMAND_TOPIC = f"device/{CLIENT_ID}/command"=========================
连接成功回调
=========================
def on_connect(client, userdata, flags, reason_code, properties):
"""
Broker 返回 CONNACK 后调用。reason_code == 0 表示连接成功。 """ print(f"MQTT 连接结果: {reason_code}") if reason_code != 0: print("MQTT Broker 连接失败") return print("MQTT Broker 连接成功") # 连接成功后订阅命令 Topic。 # # 一个非常重要的实践: # 把 subscribe() 放在 on_connect() 中。 # # 原因: # 如果网络断开后自动重连, # on_connect() 会再次执行, # 从而自动恢复订阅。 client.subscribe(COMMAND_TOPIC, qos=1) print(f"已订阅: {COMMAND_TOPIC}")=========================
收到消息回调
=========================
def on_message(client, userdata, message):
"""
收到 Broker 转发的消息后调用。
"""topic = message.topic payload = message.payload.decode("utf-8", errors="replace") print("=" * 60) print(f"收到 Topic: {topic}") print(f"QoS: {message.qos}") print(f"Retain: {message.retain}") print(f"Payload: {payload}") # 尝试解析 JSON try: data = json.loads(payload) print("JSON 数据:") print(data) # 根据业务执行命令 command = data.get("command") if command == "restart": print("收到重启命令") elif command == "status": print("收到状态查询命令") except json.JSONDecodeError: print("Payload 不是 JSON")=========================
断开连接回调
=========================
def on_disconnect(client, userdata, disconnect_flags,
reason_code, properties):
"""
MQTT 连接断开时调用。
"""print(f"MQTT 连接断开: {reason_code}")=========================
创建 MQTT Client
=========================
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=CLIENT_ID,
protocol=mqtt.MQTTv311
)=========================
用户名密码
=========================
client.username_pw_set(
USERNAME,
PASSWORD
)=========================
设置 LWT
=========================
client.will_set(
STATUS_TOPIC,
payload=json.dumps({
"status": "offline",
"timestamp": int(time.time())
}),
qos=1,
retain=True
)=========================
设置回调函数
=========================
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect=========================
连接 Broker
=========================
print("正在连接 MQTT Broker...")
client.connect(
BROKER_HOST,
BROKER_PORT,
keepalive=60
)=========================
启动网络循环
=========================
client.loop_start()
try:
# 发布在线状态 online_payload = json.dumps({ "status": "online", "timestamp": int(time.time()) }) client.publish( STATUS_TOPIC, online_payload, qos=1, retain=True ) print(f"已发布在线状态: {online_payload}") # 持续上报遥测数据 while True: telemetry = { "deviceId": CLIENT_ID, "temperature": 26.5, "humidity": 68.2, "timestamp": int(time.time()) } result = client.publish( TELEMETRY_TOPIC, json.dumps(telemetry), qos=1, retain=False ) if result.rc == mqtt.MQTT_ERR_SUCCESS: print(f"遥测数据发送成功: {telemetry}") else: print(f"发送失败,错误码: {result.rc}") time.sleep(10)except KeyboardInterrupt:
print("程序退出")finally:
# 正常退出前主动发送 DISCONNECT client.disconnect() # 停止网络线程 client.loop_stop() print("MQTT 客户端已退出")
Paho 文档特别建议把订阅操作放在 on_connect() 中,这样客户端重新连接后可以重新建立订阅。新版 Paho 2.x 的回调 API 使用 CallbackAPIVersion.VERSION2,并统一了 MQTT 3.x/5.x 回调接口。
8.3 代码运行流程
启动:
Python
│
│ connect()
▼
Broker
│
│ CONNACK
▼
on_connect()
│
├── subscribe()
│
└── publish online
│
▼
loop_start()
│
├── 接收消息
├── 心跳
├── ACK
└── 自动网络处理
8.4 为什么必须调用 loop_start?
这是很多 MQTT 初学者容易踩的坑。
MQTT 不只是:
publish()
后台还需要处理:
-
TCP 网络数据
-
PINGREQ/PINGRESP
-
PUBACK
-
PUBREC
-
PUBREL
-
PUBCOMP
-
SUBACK
-
重连
-
回调
Paho 提供:
client.loop_start()
让后台线程持续处理 MQTT 网络事件。Paho 同时提供 loop_forever() 和手动 loop() 等网络循环方式。
8.5 Broker 快速部署
这里以 Eclipse Mosquitto 为例。
Eclipse Mosquitto 是一个轻量级开源 MQTT Broker,同时支持 MQTT 5、3.1.1 和 3.1。其官方 Docker 镜像提供 /mosquitto/config、/mosquitto/data 和 /mosquitto/log 等目录用于配置、持久化和日志。
8.5.1 创建目录
mkdir -p ~/mqtt/mosquitto/config
mkdir -p ~/mqtt/mosquitto/data
mkdir -p ~/mqtt/mosquitto/log
8.5.2 创建配置文件
vim ~/mqtt/mosquitto/config/mosquitto.conf
最简单配置:
# MQTT TCP
listener 1883
# 允许匿名访问
#
# 仅用于本地测试。
# 生产环境不要这样配置。
allow_anonymous true
# 持久化
persistence true
persistence_location /mosquitto/data/
# 日志
log_dest file /mosquitto/log/mosquitto.log
8.5.3 Docker 启动
docker run -d \
--name mosquitto \
--restart unless-stopped \
-p 1883:1883 \
-v ~/mqtt/mosquitto/config:/mosquitto/config \
-v ~/mqtt/mosquitto/data:/mosquitto/data \
-v ~/mqtt/mosquitto/log:/mosquitto/log \
eclipse-mosquitto:2.1.2-alpine
官方镜像支持 2.1.2-alpine、latest 等标签,并支持包括 amd64、arm64 在内的多种架构。生产环境更推荐固定具体版本,而不是长期使用 latest。
8.6 验证 Broker
查看:
docker ps
查看日志:
docker logs mosquitto
或者:
tail -f ~/mqtt/mosquitto/log/mosquitto.log
8.7 使用 mosquitto-clients 测试
订阅:
mosquitto_sub \
-h 127.0.0.1 \
-p 1883 \
-t 'test/topic'
另一个终端发布:
mosquitto_pub \
-h 127.0.0.1 \
-p 1883 \
-t 'test/topic' \
-m 'hello mqtt'
订阅端应该收到:
hello mqtt
8.8 MQTT Broker 的生产环境配置
测试环境可以:
allow_anonymous true
生产环境不要这么做。
生产环境应该:
TLS
+
认证
+
ACL
+
日志
+
监控
+
持久化
典型架构:
Internet
│
│ 8883/TLS
▼
┌───────────────┐
│ MQTT Broker │
│ │
│ Authentication│
│ ACL │
│ TLS │
│ Monitoring │
└───────┬───────┘
│
▼
IoT Backend
8.9 EMQX
如果系统规模较大,除了 Mosquitto,也可以考虑 EMQX。
EMQX 提供:
-
MQTT Broker
-
Dashboard
-
认证
-
ACL
-
集群
-
规则引擎
-
数据集成
-
WebSocket
-
MQTT over TLS
-
MQTT 5
-
大规模设备连接能力
EMQX 官方文档提供 Docker 部署方式,并支持将数据和日志目录持久化;当前官方文档也提供单节点和 Docker Compose 集群部署方案。
例如:
docker run -d \
--name emqx \
--restart unless-stopped \
-p 1883:1883 \
-p 8883:8883 \
-p 8083:8083 \
-p 8084:8084 \
-p 18083:18083 \
emqx/emqx:latest
其中:
1883 MQTT TCP
8883 MQTT TLS
8083 MQTT WebSocket
8084 MQTT Secure WebSocket
18083 Dashboard
EMQX 官方 Docker 文档列出了这些常用监听端口。
9. 最佳实践与避坑指南
9.1 Client ID 必须合理设计
错误:
device
如果 10000 个设备都使用:
device
Broker 会认为它们是同一个 Client ID。
可能出现:
设备 A 登录
↓
device
设备 B 登录
↓
device
A 被踢下线
推荐设计
例如:
device-{product}-{deviceId}
实际:
charger-001-CHG000001
charger-001-CHG000002
sensor-001-SN000001
Client ID 应当:
-
全局尽量唯一
-
稳定
-
可追踪
-
不要频繁随机变化
-
不要包含敏感信息
9.2 不要把大文件塞进 MQTT
例如:
1 MB 图片
20 MB 视频
100 MB 固件
直接 MQTT:
PUBLISH
Payload = 100MB
通常不是好设计。
更推荐:
设备
│
│ MQTT
│
├──► 控制消息
│
└──► 文件URL
│
▼
HTTP/HTTPS
例如 MQTT Payload:
{
"type": "firmware",
"version": "2.1.5",
"url": "https://example.com/firmware/2.1.5.bin",
"sha256": "..."
}
设备收到后:
MQTT
↓
获取下载地址
↓
HTTPS下载
↓
SHA256校验
↓
升级
这比 MQTT 直接传大文件更加合理。
9.3 如果必须传大消息
可以采用:
分片
例如:
file/001/chunk/0001
file/001/chunk/0002
file/001/chunk/0003
...
但是需要自行处理:
-
分片编号
-
总分片数
-
顺序
-
丢包
-
重传
-
校验
-
超时
-
文件组装
因此:
能用 HTTP/HTTPS 下载文件,就尽量不要使用 MQTT 传大文件。
9.4 弱网环境优化
物联网设备经常面对:
4G
5G
Wi-Fi
NB-IoT
卫星网络
工业无线网络
网络可能:
高延迟
丢包
抖动
频繁断线
NAT
IP变化
建议:
1. 合理 Keep Alive
不要:
KeepAlive = 5秒
除非确实需要。
2. 自动重连
采用:
指数退避
例如:
1秒
2秒
4秒
8秒
16秒
30秒
60秒
避免所有设备同时上线造成:
惊群效应 / Thundering Herd
3. QoS 选择合理
实时遥测:
QoS 0
重要业务:
QoS 1
极少数严格场景:
QoS 2
4. 消息不要过大
尽量:
几百字节
几KB
而不是:
几MB
几十MB
9.5 消息幂等
这是 MQTT 项目中非常重要的一点。
例如:
QoS 1
可能重复收到:
messageId = 10001
第一次:
执行开门
第二次:
再次执行开门
如果业务不能重复执行,就需要:
消息ID
+
幂等表
+
业务状态机
例如:
{
"messageId": "MSG202608240001",
"deviceId": "001",
"command": "open"
}
数据库:
messageId
↓
是否处理过?
│
├── 是 → 忽略
│
└── 否 → 执行业务
9.6 MQTT 不等于业务可靠性
必须建立一个正确认识:
MQTT QoS
解决的是:
MQTT 消息传输层面的交付语义。
它不能自动解决:
数据库事务
业务幂等
订单状态
支付一致性
设备执行成功
业务最终一致性
例如:
MQTT QoS 1
↓
Broker
↓
后端
↓
数据库
后端收到消息不代表:
数据库一定成功
因此大型系统应该设计:
MQTT
↓
消息接收服务
↓
消息队列 / 数据库
↓
业务处理
↓
状态确认
9.7 消息丢失排查方法
遇到:
"设备明明发送了,但是服务器没收到。"
不要直接认为是 MQTT 丢消息。
按照下面顺序排查:
设备
↓
TCP
↓
Broker
↓
Topic
↓
订阅关系
↓
QoS
↓
Session
↓
ACL
↓
后端程序
第一步:设备是否真的连接成功?
检查:
CONNACK
第二步:Topic 是否正确?
例如设备发送:
device/001/telemetry
后端订阅:
device/001/status
当然收不到。
第三步:QoS 是否合理?
例如:
QoS 0
网络断开时消息可能丢失。
第四步:是否订阅成功?
检查:
SUBSCRIBE
SUBACK
第五步:是否受到 ACL 限制?
例如:
Client 001
只允许:
device/001/#
却订阅:
device/002/#
可能被拒绝。
9.8 重复消费排查
如果服务器发现:
同一条消息处理了两次
首先检查:
QoS 1
然后检查:
Client重连
PUBACK丢失
消息重传
之后再检查业务代码:
是否幂等?
不要简单地说:
MQTT 出 Bug 了。
9.9 连接被拒绝排查
MQTT 3.1.1:
| CONNACK | 含义 | 常见原因 |
|---|---|---|
| 0 | 成功 | 正常 |
| 1 | 协议版本不支持 | Client/Broker版本不兼容 |
| 2 | Client ID 被拒 | ID异常或Broker策略 |
| 3 | Server不可用 | Broker状态异常 |
| 4 | 用户名密码错误 | 认证失败 |
| 5 | 未授权 | ACL/认证策略 |
例如:
Connection refused: not authorised
优先检查:
用户名
密码
认证插件
ACL
Client ID
Paho 的官方 API 文档也列出了 MQTT 3.x 常见 CONNACK 拒绝原因,包括协议版本、Client ID、服务器不可用、用户名密码以及未授权等。
9.10 生产环境推荐架构
一个比较典型的 IoT 架构:
flowchart LR
A[IoT设备] -->|MQTT/TLS| B[MQTT Broker]
B --> C[设备接入服务]
C --> D[业务服务]
C --> E[Kafka]
E --> F[数据处理]
F --> G[MySQL]
F --> H[时序数据库]
D --> I[API服务]
I --> J[Web/App]
B --> K[监控系统]
职责可以划分为:
MQTT Broker
↓
负责连接与消息传输
IoT 接入服务
↓
负责协议适配、设备身份、消息解析
Kafka
↓
负责大规模异步消息流
MySQL
↓
负责业务数据
时序数据库
↓
负责设备遥测数据
Redis
↓
负责实时状态/缓存
Web/App
↓
负责业务展示
10. 附录
10.1 常见 MQTT 端口
| 端口 | 常见用途 |
|---|---|
| 1883 | MQTT TCP |
| 8883 | MQTT over TLS |
| 8083 | MQTT over WebSocket |
| 8084 | MQTT over Secure WebSocket |
| 18083 | EMQX Dashboard |
注意:
端口不是 MQTT 标准强制规定的业务端口。
例如 MQTT 服务器完全可以监听:
31883
38883
实际端口由 Broker 配置决定。
10.2 MQTT over WebSocket
MQTT 不只可以运行在传统 TCP 上。
浏览器无法像普通 MQTT Client 那样直接建立任意 TCP Socket,因此 Web 应用通常使用:
MQTT over WebSocket
结构:
Browser
│
│ WebSocket
▼
MQTT Broker
例如:
ws://server:8083/mqtt
安全环境:
wss://server:8084/mqtt
这对于:
Vue
React
Web管理平台
浏览器监控页面
非常有用。
10.3 MQTT 控制报文速查
| 报文 | 方向 | 作用 |
|---|---|---|
| CONNECT | Client → Broker | 建立 MQTT 会话 |
| CONNACK | Broker → Client | CONNECT 响应 |
| PUBLISH | 双向 | 发布消息 |
| PUBACK | 双向 | QoS 1 确认 |
| PUBREC | 双向 | QoS 2 第一步确认 |
| PUBREL | 双向 | QoS 2 释放 |
| PUBCOMP | 双向 | QoS 2 完成 |
| SUBSCRIBE | Client → Broker | 订阅 |
| SUBACK | Broker → Client | 订阅确认 |
| UNSUBSCRIBE | Client → Broker | 取消订阅 |
| UNSUBACK | Broker → Client | 取消订阅确认 |
| PINGREQ | Client → Broker | 心跳 |
| PINGRESP | Broker → Client | 心跳响应 |
| DISCONNECT | 双向语义中的主动断开报文 | 正常断开 |
10.4 MQTT QoS 速查
QoS 0
PUBLISH
↓
完成
QoS 1
PUBLISH
↓
PUBACK
↓
完成
QoS 2
PUBLISH
↓
PUBREC
↓
PUBREL
↓
PUBCOMP
↓
完成
记忆口诀:
0 不确认,1 一次确认,2 四步握手。
10.5 MQTT 核心机制速查
| 机制 | 解决什么问题 |
|---|---|
| QoS 0 | 低开销实时消息 |
| QoS 1 | 重要消息可靠传输 |
| QoS 2 | MQTT 层 Exactly Once 交付语义 |
| Keep Alive | 检测连接状态 |
| LWT | 异常离线通知 |
| Retain | 保存最新状态 |
| Session | 保存客户端会话 |
| Clean Session | MQTT 3.1.1 会话生命周期 |
| Clean Start | MQTT 5.0 新会话控制 |
| Session Expiry | MQTT 5.0 会话保留时间 |
| ACL | Topic 权限控制 |
| TLS | 网络加密 |
| Username/Password | 身份认证 |
| Token | 动态身份认证 |
10.6 MQTT 3.1.1 与 MQTT 5.0 实战选择建议
如果是:
老设备接入
MQTT 3.1.1
通常更容易兼容。
新 IoT 平台
MQTT 5.0
更推荐。
大规模平台
建议重点考虑 MQTT 5.0 的:
Reason Code
Properties
Session Expiry
Message Expiry
Receive Maximum
Topic Alias
Maximum Packet Size
Request/Response
User Properties
10.7 推荐学习路线
建议不要一上来就研究 MQTT 报文二进制编码。
推荐按下面顺序学习:
第一阶段
MQTT是什么
↓
Client / Broker
↓
Publish / Subscribe
↓
Topic
第二阶段
CONNECT / CONNACK
↓
SUBSCRIBE / SUBACK
↓
PUBLISH
↓
PINGREQ / PINGRESP
↓
DISCONNECT
第三阶段
QoS 0
↓
QoS 1
↓
QoS 2
第四阶段
LWT
↓
Retain
↓
Session
第五阶段
TLS
↓
认证
↓
ACL
第六阶段
Broker集群
↓
消息持久化
↓
Kafka
↓
规则引擎
↓
监控
↓
IoT平台架构
10.8 官方标准与学习资源
MQTT 3.1.1 官方规范
OASIS MQTT Version 3.1.1 Specification
MQTT 3.1.1 是学习 MQTT 协议细节最重要的规范之一。
MQTT 5.0 官方规范
OASIS MQTT Version 5.0 Specification
MQTT 5.0 在 3.1.1 基础上增加了大量可扩展性、错误处理、会话和消息属性等能力。
Eclipse Paho Python
Eclipse Paho MQTT Python Documentation
适合使用 Python 快速编写 MQTT Client。
Eclipse Mosquitto
Eclipse Mosquitto 官方 Docker 镜像
适合:
-
本地开发
-
MQTT 协议学习
-
测试环境
-
小型 IoT 项目
-
嵌入式环境
官方镜像同时支持 MQTT 5、3.1.1 和 3.1。
EMQX
适合进一步学习:
-
大规模设备接入
-
MQTT 集群
-
认证
-
ACL
-
规则引擎
-
数据集成
-
Dashboard
-
企业级 IoT 平台
11. 总结
MQTT 真正值得掌握的不是几个 API,而是下面这套完整模型:
┌───────────────┐
│ MQTT Broker │
│ │
│ Topic路由 │
│ QoS │
│ Session │
│ Retain │
│ LWT │
│ ACL │
└───────┬───────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
IoT设备 网关 后端服务
│ │ │
└────── Publish / Subscribe ───────┘
从开发角度看,可以把 MQTT 总结为:
MQTT
│
├── Client
│
├── Broker
│
├── Connection
│
├── Topic
│
├── Payload
│
├── Publish / Subscribe
│
├── QoS
│ ├── QoS 0
│ ├── QoS 1
│ └── QoS 2
│
├── Keep Alive
│
├── LWT
│
├── Retain
│
├── Session
│
├── TLS
│
└── ACL
如果进一步做生产级 IoT 系统,则应该继续向:
MQTT
↓
Broker集群
↓
设备认证
↓
ACL
↓
IoT接入层
↓
Kafka
↓
业务服务
↓
MySQL / Redis / 时序数据库
↓
监控与告警
演进。
最终需要建立一个核心认识:
MQTT 解决的是"设备与系统之间如何高效、可靠地传输消息",而完整的 IoT 平台还需要解决设备身份、权限、安全、数据持久化、消息幂等、业务状态机、设备影子、OTA、监控和故障恢复等问题。
掌握 Topic + QoS + Session + LWT + Retain + TLS + ACL 这七个核心机制,基本就掌握了 MQTT 工程实践中最重要的部分。