MQTT 知识小册子
这份 Wiki 写给刚接触 MQTT 的你:协议怎么工作、主题怎么设计、QoS 怎么选、消息体怎么组织,以及怎么把设备连上 mqttyyc.top。一共 15 章,前半讲概念,后半全是能直接抄的代码,第 13 章更是四套改个凭据就能跑的完整模板。
MQTT 是什么
MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅消息协议,诞生于 1999 年,如今是物联网通信的事实标准(OASIS 标准,主流版本为 MQTT 3.1.1 和 5.0)。
📢 发布(Publish)
设备(发布者)把消息发到某个主题上,发完就走,不需要知道谁在收。
📬 订阅(Subscribe)
应用(订阅者)向 Broker 登记感兴趣的主题,一有消息到来,Broker 立刻推送。
中间人叫 Broker(消息代理服务器)——本站的 mqttyyc.top 就是一个公共 Broker(由 EMQX 提供)。发布者和订阅者互不认识,都只跟 Broker 打交道,这种"解耦"让设备端可以做得非常轻:
- 协议头极小:固定头部只有 2 字节,适合低带宽、不稳定网络;
- 基于 TCP:长连接 + 心跳(Keep Alive),弱网下也能保持在线;
- 三种角色解耦:时间解耦(双方不用同时在线处理)、空间解耦(不用知道对方地址)、同步解耦(收发互不阻塞)。
哪些场景特别适合 MQTT?
- 传感器遥测上报:温湿度、电量、位置等周期数据;
- 设备远程控制:手机/网页下发开关、亮度等指令;
- 状态实时同步:设备在线/离线、任务进度广播;
- 弱网 / 低功耗环境:4G Cat.1、NB-IoT、电池供电设备。
对比 HTTP 与版本选择
常有人问:都已经有 HTTP 了,为什么还要 MQTT?两者定位不同——HTTP 是"一问一答",MQTT 是"常驻在线的推送"。
| 对比项 | HTTP | MQTT |
|---|---|---|
| 通信模式 | 请求-响应,客户端主动拉取 | 发布-订阅,消息到达即时推送 |
| 连接形态 | 短连接为主,用完即断 | 长连接 + 心跳保活 |
| 协议开销 | 头部几百字节起步 | 固定头最小仅 2 字节 |
| 离线处理 | 无(请求失败就失败) | QoS 1/2 可缓存补发、遗嘱通知 |
| 适合 | 网页、API 查询、文件传输 | 设备遥测、指令下发、状态同步 |
举个例子:设备每秒上报一次温度,用 HTTP 就得每秒建连、带一堆请求头;用 MQTT 一条长连接常驻,一条消息只有几十字节开销,弱网下还能靠 QoS 补发。
连 3.1.1 还是 5.0?
- MQTT 3.1.1:兼容性最好,几乎所有库和老设备都支持,本站完全支持;
- MQTT 5.0:本站同样支持,新增连接失败原因码、消息属性(User Properties)、消息过期时间、共享订阅等能力,排查问题和做服务消费更方便;
- 怎么选:你用的库两者都支持就选 5.0;老固件、老库保持 3.1.1 完全没问题,两者可以互通。
$share/组名/主题 订阅同一主题,Broker 会把消息轮流分发给组内成员,实现负载均衡消费——这是做"MQTT 转存数据库"之类服务时的常用技巧。主题与通配符
主题(Topic)就是消息的"频道名",本质是一个 UTF-8 字符串,用 / 分层。发布时用精确主题,订阅时可以用通配符。
主题设计建议
# 推荐格式:项目前缀 / 设备或分类 / 数据项 myhome/livingroom/temp # 客厅温度 myhome/livingroom/humidity # 客厅湿度 farm/greenhouse-01/soil # 大棚 1 号土壤湿度 device/esp32-a1b2/status # 设备在线状态
✅ 好的主题
myhome/livingroom/temp——有自己的前缀、层级清晰、全小写,别人不容易撞车也不容易误读。
❌ 不好的主题
temp(太通用)、My Home/temp(含空格)、/a/b/(以斜杠开头结尾)、十层以上的超深嵌套。
- 加一个自己的项目前缀,避免和别人的主题撞车(公共服务上尤其重要);
- 用英文小写和中划线,层级不要太深(3~4 层足够);
- 主题里不要带空格和特殊字符,不要以
/开头或结尾; $SYS/开头的主题是 Broker 的内部监控主题,普通客户端无法订阅。
两个通配符(只能用于订阅)
| 通配符 | 含义 | 示例 | 能匹配到 |
|---|---|---|---|
| + | 单层通配,只占一层 | myhome/+/temp | myhome/livingroom/temp、myhome/bedroom/temp |
| # | 多层通配,必须放最后 | myhome/# | myhome 下所有层级的所有主题 |
# 单独订阅会收到 Broker 上所有消息,在公共服务里请克制使用。特殊主题
$SYS/开头的主题是 Broker 的内部监控主题(连接数、消息量等),普通客户端无法订阅;$share/组名/主题是共享订阅的写法(见第 2 章),同样只用于订阅。
服务质量 QoS
QoS(Quality of Service)决定消息投递的可靠程度,发布和订阅时可以分别指定,实际生效的是两者中较低的一级。
| 级别 | 语义 | 代价 | 适用场景 |
|---|---|---|---|
| QoS 0 | 最多一次(发了就算) | 最低,可能丢 | 传感器周期上报,丢一条无所谓 |
| QoS 1 | 至少一次(可能重复) | 中等,需确认 | 控制指令、重要状态变更 |
| QoS 2 | 恰好一次(不丢不重) | 最高,四次握手 | 计费、关键计数等绝不能重复的场景 |
🤔 怎么选?
90% 的 IoT 场景用 QoS 0 或 1 就够了。高频遥测用 0;怕丢的指令用 1,并在业务上容忍/去重。QoS 2 很少需要。
⚠️ QoS 1 的坑
"至少一次"意味着可能收到重复消息(比如重连后补发)。消费端做幂等处理(比如按消息里的时间戳/序号去重)。
投递过程是怎么确认的?
# QoS 1:两步确认 发布者 PUBLISH ────────▶ Broker 发布者 ◀──────── PUBACK # 收到确认,发布者才丢弃消息;没收到就重发 # QoS 2:四步握手,保证不重 发布者 PUBLISH ────────▶ Broker 发布者 ◀──────── PUBREC # Broker 已收下并记录 发布者 PUBREL ────────▶ Broker # 可以投递给订阅者了 发布者 ◀──────── PUBCOMP # 全流程完成
可以看到级别越高、往返报文越多,延迟和开销也越大——这就是为什么高频遥测推荐 QoS 0 的原因。另外注意:QoS 1/2 的补发依赖持久会话(Clean Session = false),否则断开期间 Broker 不会替你存消息。
保留消息与遗嘱消息
📌 保留消息(Retained)
发布时勾选 Retain,Broker 会把这个主题的最后一条消息存下来。之后任何人新订阅该主题,立刻就能收到这条"历史最新值",不用等下一次发布。
典型用法:设备状态(online/offline)、当前配置。想清除保留消息,向该主题发布一条空内容 + Retain 的消息即可。
🪦 遗嘱消息(Last Will)
连接时预先"立遗嘱":指定一个主题和内容。如果客户端异常断开(断网、崩溃,没走正常 DISCONNECT),Broker 就自动把遗嘱消息发布出去。
典型用法:设备上线时发 status=online(Retain),遗嘱设为 status=offline,其他人订阅该主题就能实时感知设备掉线。
用起来长什么样
# 1. 连接时就立好遗嘱:异常断开时代发 offline 连接参数: Last Will Topic = myproj/esp32-001/status Last Will Payload = offline,也勾选 Retain # 2. 连接成功后立刻报在线(保留消息) PUBLISH myproj/esp32-001/status = online (Retain) # 3. 任何人订阅这个主题,立刻知道设备在不在线 SUBSCRIBE myproj/+/status
连接参数与返回码
任何 MQTT 客户端连接时都要填这几样,理解它们能少走很多弯路:
🆔 Client ID(客户端标识)
你在 Broker 上的唯一身份证。同一个 Broker 上 Client ID 不允许重复——如果两台设备用了同一个 ID,会互相把对方挤下线(表现为反复掉线重连)。建议用"项目名 + 设备编号",如 myproj-esp32-001。
👤 用户名 / 密码
本站已关闭匿名连接,必须先在账户中心注册。明文端口(1883/8083)上密码是明文传输,涉及真实业务请优先用 TLS 端口。
💓 Keep Alive(心跳间隔)
客户端每隔一段时间发一次心跳保活,默认常见 60 秒。移动网络/NAT 环境下可适当调小(30~45 秒)防止被中间设备切断;值设为 0 表示不心跳。
🧹 Clean Session / Clean Start
为 true 时每次连接都是"全新会话",断开后订阅关系全部丢弃;为 false 时 Broker 会保留订阅和未投递的 QoS 1/2 消息,重连后补发。初学者保持默认(true)即可。
连接被拒?先看返回码
连接失败时客户端会给出 CONNACK 返回码(MQTT 5.0 的 reason code 更细,下面是常见对照):
| rc(3.1.1) | 含义 | 在本站通常意味着 |
|---|---|---|
| 0 | 连接成功 | 一切正常,可以开工 |
| 1 | 协议版本被拒 | 客户端协议版本选错(如选了 3.1),改成 3.1.1 或 5.0 |
| 2 | Client ID 被拒 | ID 为空或含非法字符;本站不允许匿名,ID 不能留空 |
| 3 | 服务不可用 | 认证服务暂时不可达,稍后重试;持续出现请联系维护者 |
| 4 | 用户名/密码格式错误 | 凭据字段缺失或非法,检查是否复制完整 |
| 5 | 未授权 | 最常见:账户不存在或密码错误,去账户中心核对 |
本站接入指南
三步:到账户中心注册 → 选一个端口连接 → 订阅发布。四个端口地址如下,点右侧按钮复制:
mqtt://mqttyyc.top:1883
mqtts://mqttyyc.top:8883
ws://mqttyyc.top:8083/mqtt
wss://mqttyyc.top:8084/mqtt
从零到第一条消息
- 打开账户中心注册:只需用户名和密码,不需要邮箱和手机号;用户名一旦注册不可更改,想好再填;
- 选一个端口按上面的地址连接,Client ID 给每台设备用唯一值;
- 先订阅
mytest/#,再向mytest/hello发布一条消息,自己能收到就说明收发都通了; - 把主题换成自己项目的前缀,正式开始。
选哪个端口?
| 你的场景 | 推荐端口 | 原因 |
|---|---|---|
| 生产环境 / 真实业务 | 8883(TLS) | 凭据和数据全程加密 |
| 嵌入式快速测试(内网) | 1883(TCP) | 最省资源,所有客户端都支持 |
| 浏览器 / 小程序(HTTPS 页面) | 8084(WSS) | HTTPS 页面只能连 wss |
| 浏览器(HTTP 页面 / 本地调试) | 8083(WS) | 无加密,仅适合调试 |
使用限制(普通账户)
- 单条消息 ≤ 64 KB;单客户端订阅 ≤ 100 个主题;
- 每端口每秒最多 5 个新连接;单连接发布 ≤ 100 条/秒 且 ≤ 128 KB/秒(超限会被暂停接收,不会断连);
- 单个 IP 每天最多注册 2 个账户;频繁断连重连会触发防抖检测被临时封禁;
- 需要更大额度可申请超级用户(消息 ≤ 1 MB、订阅不限、专属端口 1884)。
客户端实战
下面是最常用的五种接入方式,把 your_username / your_password 换成自己的就能用。
8.1 命令行(mosquitto-clients,最快验证)
服务器上调试或快速验证连通性,两条命令就够:
# ── 第一条命令:订阅(持续接收消息,按 Ctrl+C 停止)── # -h 服务器地址 -p 端口 -u 用户名 -P 密码 # -i Client ID(自己随便起名,但要全网唯一,别和别人撞) # -t 要订阅的主题(# 是通配符,收下 mytest 下所有消息) # -v 打印时把主题也一起显示出来 mosquitto_sub -h mqttyyc.top -p 1883 \ -u your_username -P your_password \ -i myproj-cli-sub -t 'mytest/#' -v # ── 第二条命令:发布一条消息(另开一个终端窗口执行)── # -m 后面是消息内容;发完命令自动退出 mosquitto_pub -h mqttyyc.top -p 1883 \ -u your_username -P your_password \ -i myproj-cli-pub -t mytest/hello -m '你好 MQTT' # 想要加密连接:把端口换成 8883,再加上系统 CA 证书路径: # -p 8883 --capath /etc/ssl/certs
8.2 MQTTX 图形客户端(推荐新手)
下载 MQTTX(桌面版或网页版),新建连接:
Name: mqttyyc Host: mqttyyc.top Port: 8883 (勾选 SSL/TLS) Username: your_username Password: your_password
连上后左下角添加订阅(如 mytest/#),右上角发布一条消息试收试发。
8.3 Python(paho-mqtt)
import paho.mqtt.client as mqtt # 导入 MQTT 客户端库(先 pip install paho-mqtt 安装) # 连接成功时自动调用的回调函数;rc 是返回码,0 表示成功(对照第 6 章) def on_connect(client, userdata, flags, rc): print('已连接, rc =', rc) client.subscribe('mytest/#') # 连上后立刻订阅:# 收下 mytest 下所有主题 # 每收到一条消息就自动调用的回调函数 def on_message(client, userdata, msg): # msg.topic 是主题;payload 是字节串,decode() 把它转成文字 print(msg.topic, '->', msg.payload.decode()) client = mqtt.Client(client_id='myproj-py-001') # 创建客户端,ID 每台设备唯一 client.username_pw_set('your_username', 'your_password') # 换成账户中心注册的账号密码 client.tls_set() # 开启 TLS 加密,配合下面的 8883 端口 client.on_connect = on_connect # 登记两个回调函数 client.on_message = on_message client.connect('mqttyyc.top', 8883) # 连接 Broker(8883 是加密端口) client.publish('mytest/hello', '你好 MQTT') # 发布第一条消息 client.loop_forever() # 常驻运行:维持连接并持续接收消息
8.4 JavaScript(MQTT.js,浏览器用 WSS)
import mqtt from 'mqtt'; // 引入 MQTT.js 库(先 npm install mqtt 安装) // 浏览器 / HTTPS 页面必须用 wss:// 加密地址 8084,结尾 /mqtt 不能丢 const client = mqtt.connect('wss://mqttyyc.top:8084/mqtt', { username: 'your_username', // 换成账户中心注册的账号 password: 'your_password', // 换成对应密码 }); client.on('connect', () => { // 连接成功的回调 client.subscribe('mytest/#'); // 订阅主题:# 收下 mytest 下所有消息 client.publish('mytest/hello', '你好 MQTT'); // 发布一条试试 }); client.on('message', (topic, payload) => { // 收到消息的回调 console.log(topic, '->', payload.toString()); // 打印主题和内容 });
8.5 ESP32 / Arduino(PubSubClient)
#include <WiFi.h> // ESP32 官方 WiFi 库 #include <PubSubClient.h> // MQTT 客户端库(Arduino IDE 库管理器搜 PubSubClient 安装) WiFiClient wifiClient; // 底层 TCP 连接对象 PubSubClient mqtt(wifiClient); // MQTT 客户端,建立在上面的 TCP 之上 void setup() { // 开机只执行一次 WiFi.begin("你的WiFi", "WiFi密码"); // 连接家里 WiFi,换成自己的 while (WiFi.status() != WL_CONNECTED) delay(300); // 没连上就等,直到连上为止 mqtt.setServer("mqttyyc.top", 1883); // 指定 Broker 地址和端口 while (!mqtt.connected()) { // 连不上就一直重试 // 三个参数:Client ID(每台设备必须唯一)、用户名、密码 mqtt.connect("myproj-esp32-001", "your_username", "your_password"); delay(500); } mqtt.subscribe("mytest/#"); // 订阅主题,准备收消息 mqtt.publish("mytest/hello", "你好 MQTT"); // 发布第一条消息 } void loop() { // 之后反复循环执行 mqtt.loop(); // 必须经常调用:收消息、发心跳全靠它 }
消息体(Payload)设计
MQTT 不关心消息内容,它只搬运一串字节,格式由收发双方自己约定。绝大多数项目选 JSON,可读、好调试、各语言都有现成库。
推荐的 JSON 格式
{
"dev": "esp32-001", // 设备标识,方便定位
"type": "env", // 数据类型,方便消费端分发
"temp": 26.5, // 数值直接用数字,别转字符串
"humi": 61,
"batt": 3.92,
"ts": 1755600000 // 时间戳,去重和排序都靠它
}
- 带上设备标识和时间戳,排查问题和 QoS 1 去重都用得上;
- 字段名短一点(
temp而非temperature),高频上报能省不少流量; - 一条消息只装一类数据,别把所有传感器塞进一个巨型 JSON,按主题拆开发更好订阅。
体积与二进制
- 普通账户单条 ≤ 64 KB,实际应尽量控制在几 KB 以内;大文件请拆分、压缩或改用 HTTP 传输,不要硬塞 MQTT;
- 只上报变化的数据(增量上报),比每次全量轮询省流量得多;
- 追求极致体积可以用二进制协议(如 protobuf、自定义字节序),但要求收发两端都实现解析,小项目不值得。
安全与最佳实践
本站是公共 Broker:任何注册用户都能订阅任何主题、向任何主题发布。请把每一个主题都当作公开广场来设计你的系统。
🔒 尽量走 TLS
明文端口(1883/8083)上用户名密码是明文传输。涉及真实业务请用 8883 / 8084,凭据和数据全程加密。
🗄️ 凭据别写死在代码里
密码放到环境变量、配置文件或固件加密区,不要提交到公开仓库;固件量产时最好每台设备一个账户。
📵 别传敏感内容
任何人知道主题名就能订阅。个人隐私、密钥、口令这类内容不要放进消息体;确需传输请先自行加密。
🛡️ 指令要验证
任何人也能向你的控制主题发布消息。设备端对收到的指令做基本校验(来源约定、字段合法性),别收到啥执行啥。
- 给主题加项目前缀不仅是防撞车,也是隐私边界:别人不容易猜到你的设备主题;
- 怀疑账户泄漏(比如密码被提交到公开仓库)时,尽快通过邮件联系维护者重置密码;
- 客户端开启自动重连时加退避(如 2 秒起步、逐步翻倍),避免重连风暴触发封禁。
经典项目模式
三个几乎每个物联网项目都会用到的模式,直接套用。
模式一:传感器周期上报
设备定时把数据发到自己的主题,服务端订阅入库展示。高频数据用 QoS 0,配合第 9 章的 JSON 格式:
# 设备:每 30 秒发布一次 PUBLISH farm/greenhouse-01/temp {"v": 26.5, "ts": 1755600000} QoS 0 # 服务端:一个通配订阅收下整个大棚 SUBSCRIBE farm/greenhouse-01/#
模式二:设备在线监测(Retained + 遗嘱)
第 5 章讲了原理,完整时序长这样:
模式三:远程控制与回执
下发指令要"有去有回":设备收到指令后回一条 ack,应用端才知道执行结果:
# 应用下发(QoS 1,怕丢) PUBLISH myproj/esp32-001/cmd/set {"led": true, "id": 42} # 设备订阅自己的指令主题 SUBSCRIBE myproj/esp32-001/cmd/# # 设备执行后回执(带上指令 id 方便对应) PUBLISH myproj/esp32-001/cmd/set/ack {"id": 42, "ok": true} # 应用订阅回执 SUBSCRIBE myproj/esp32-001/cmd/+/ack
工业机器人与 PLC 接入
车间数采、远程监工类项目越来越多地要把 PLC 和工业机器人的数据搬到 MQTT 上。这一章讲清楚通用的接入姿势、各品牌的常见连法,以及必须守住的安全边界。
12.1 通用架构:设备不直接连 Broker
工业设备讲究“稳定优先”,一般不会让 PLC / 机器人控制器直接连公共 Broker,标准姿势是中间加一层网关:
PLC / 机器人控制器
│ 现场总线或厂商接口(Modbus、PROFINET、MC、FOCAS…)
▼
IoT 网关 / 边缘上位机 ← 协议转换、缓存补发、主题映射都在这层
│ MQTT(推荐 TLS 8883)
▼
Broker(mqttyyc.top)
│
▼
上位监控 / MES / 看板、报警推送
这样做的好处:设备端程序不用改;网关断线时能缓存补发;多台设备共用一个网关连接,Client ID 和账户都好管理。
12.2 PLC 怎么接
| PLC 类型 | 常见连法 |
|---|---|
| 西门子 S7-1200/1500 | 较新固件可用官方 TLS + MQTT 通信库直接连;也可用第三方 MQTT 库块,在 PLC 里直接发布订阅 |
| 三菱 FX/Q/L 等 | 多数没有原生 MQTT,常用网关按 MC 协议 / Modbus 采集后转 MQTT,或在工控机上跑桥接程序 |
| 欧姆龙 / 台达 / 信捷等 | 同上,Modbus / 专用协议网关转 MQTT 是最省事的方案 |
| DIY / 学习场景 | 树莓派、工控机跑桥接程序:pymodbus 轮询寄存器 → paho-mqtt 发布,见第 8 章的 Python 示例 |
12.3 工业机器人怎么接
主流工业机器人控制器都不原生支持 MQTT,实际项目里基本是两条路:
🖥️ 边缘机采集转发
用厂商接口取数据:ABB 的 PC SDK、FANUC 的 FOCAS、KUKA 的 RSI / 以太网接口、安川的高速服务器等。边缘机读到坐标、节拍、报警后按第 9 章的 JSON 格式发布。
🔌 机器人状态进 PLC
机器人把运行 / 报警 / 节拍等状态通过硬接线 IO 或现场总线送进 PLC,由 PLC 统一转 MQTT。链路最短、最好维护,产线改造里最常用。
12.4 主题与信号设计
套用第 11 章的“上行 + 下行 + 回执”模式,按产线 / 设备分层:
factory/line1/plc-01/data # PLC 数据上行:QoS 0,周期发布 factory/line1/plc-01/cmd # MES 下发指令:QoS 1,怕丢 factory/line1/plc-01/cmd/ack # PLC 执行回执:带上指令 id factory/line1/robot-01/status # 机器人状态:Retained + 遗嘱(见第 5 章) factory/line1/robot-01/alarm # 报警事件:QoS 1,触发即发
12.5 必须守住的边界
- 本站这类公共 Broker 适合学习、联调和小规模试点;正式生产建议自建私有 Broker,做好主题鉴权、TLS 和网络隔离;
- 工艺参数、产量等数据出车间前要做脱敏或加密评估,别让产线数据“裸奔”;
- 网关到 Broker 的链路要有断线缓存和退避重连,车间网络抖动很常见,别把 Broker 打出重连风暴。
复制即用模板库
第 8 章的示例是帮你理解的最小版本,这一章是"改个凭据就能跑"的完整文件:重连退避、遗嘱、保留状态、指令回执全部内置。点右上角按钮复制全文,保存成对应文件名直接运行。
13.1 Python 设备端完整模板
# device_template.py —— 改凭据即可运行(基于 paho-mqtt 2.x,1.x 见注释) import json, os, time # Python 自带库:JSON 编解码 / 环境变量 / 时间 import paho.mqtt.client as mqtt # MQTT 客户端库,先 pip install paho-mqtt # ========= 改成你自己的 ========= USERNAME = os.getenv("MQTT_USER", "your_username") # 优先读环境变量,否则用后面的默认值 PASSWORD = os.getenv("MQTT_PASS", "your_password") # 把默认值换成自己的账号密码 CLIENT_ID = "myproj-py-001" # 在 Broker 上的身份证,每台设备必须唯一 PREFIX = "myproj/py-001" # 自己的主题前缀,避免和别人撞车 # ====================================== # 连接成功(或失败)时自动调用的回调 def on_connect(client, userdata, flags, reason_code, properties): # paho 1.x 用户:签名改为 (client, userdata, flags, rc),创建行见下方注释 if reason_code == 0: # 0 表示连接成功(对照第 6 章返回码表) print("已连接") client.subscribe(PREFIX + "/cmd/#", qos=1) # 订阅自己的指令主题,QoS 1 怕丢 client.publish(PREFIX + "/status", "online", qos=1, retain=True) # 报在线(保留消息,见第 5 章) else: print("连接失败:", reason_code) # 失败时打印原因码好排查 # 每收到一条订阅的消息就自动调用 def on_message(client, userdata, msg): print("收到:", msg.topic, msg.payload.decode()) # payload 是字节,decode() 转文字 if msg.topic.endswith("/cmd/set"): # 收到的是“设置”指令 → 回执告诉对方已执行 client.publish(PREFIX + "/cmd/set/ack", json.dumps({"ok": True, "ts": int(time.time())}), qos=1) client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID) # 创建客户端(2.x 写法) # paho 1.x 用户:上一行改为 mqtt.Client(client_id=CLIENT_ID) client.username_pw_set(USERNAME, PASSWORD) # 设置登录账号密码 client.tls_set() # 开启加密;删掉这行则用明文 1883 client.will_set(PREFIX + "/status", "offline", qos=1, retain=True) # 遗嘱:异常掉线时 Broker 代发 offline client.on_connect = on_connect # 登记回调函数 client.on_message = on_message client.reconnect_delay_set(2, 30) # 断线后 2 秒起重连,逐渐延长到 30 秒,防重连风暴 client.connect_async("mqttyyc.top", 8883, keepalive=45) # 异步连接,每 45 秒发一次心跳 client.loop_start() # 后台线程维持连接、收发消息 try: n = 0 while True: # 主循环:模拟传感器定时上报 n += 1 client.publish(PREFIX + "/data", json.dumps({ "dev": CLIENT_ID, "temp": 25 + n % 5, "ts": int(time.time())}), qos=0) # 上报用 QoS 0 轻快即可 time.sleep(5) # 每 5 秒上报一次 except KeyboardInterrupt: # 按 Ctrl+C 退出时优雅收尾 client.publish(PREFIX + "/status", "offline", qos=1, retain=True) # 主动报离线,避免误触发遗嘱 client.loop_stop()
13.2 Python 服务端采集存 CSV
# collector_template.py —— 服务端示例:把收到的消息存进 CSV 表格,Ctrl+C 退出 import csv, os, time # Python 自带库:写 CSV / 环境变量 / 时间 import paho.mqtt.client as mqtt # MQTT 客户端库 USERNAME = os.getenv("MQTT_USER", "your_username") # 换成自己的账号 PASSWORD = os.getenv("MQTT_PASS", "your_password") # 换成自己的密码 SUB_TOPIC = "myproj/#" # 订阅的主题:# 收下 myproj 前缀下所有消息,换成自己的前缀 CSV_FILE = "messages.csv" # 保存的文件名,用 Excel 就能打开 # 连接成功后自动订阅 def on_connect(client, userdata, flags, reason_code, properties): print("已连接,订阅:", SUB_TOPIC) client.subscribe(SUB_TOPIC, qos=1) # QoS 1:断线重连后能补收期间消息 # 每收到一条消息就追加写入 CSV 一行 def on_message(client, userdata, msg): with open(CSV_FILE, "a", newline="", encoding="utf-8") as f: # "a" = 追加模式,不覆盖旧数据 csv.writer(f).writerow([time.strftime("%F %T"), msg.topic, # 三列:时间、主题、内容 msg.payload.decode(errors="replace")]) # errors 防止脏字节导致报错 print("已存:", msg.topic) client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="myproj-collector-001") # 创建客户端,ID 唯一 client.username_pw_set(USERNAME, PASSWORD) # 登录凭据 client.tls_set() # 开启加密,配合 8883 client.on_connect = on_connect # 登记回调 client.on_message = on_message client.reconnect_delay_set(2, 30) # 重连退避 client.connect("mqttyyc.top", 8883, keepalive=45) # 连接 Broker client.loop_forever() # 常驻运行,一直收消息存文件
13.3 ESP32 完整模板(重连 + 遗嘱 + 控灯)
// esp32_template.ino —— 改 WiFi 和凭据后直接烧录 #include <WiFi.h> // WiFi 库 #include <PubSubClient.h> // MQTT 库(Arduino IDE 库管理器安装) // ========= 改成你自己的 ========= const char* WIFI_SSID = "你的WiFi"; // WiFi 名称 const char* WIFI_PASS = "WiFi密码"; // WiFi 密码 const char* MQTT_USER = "your_username"; // 账户中心注册的账号 const char* MQTT_PASS = "your_password"; // 对应密码 const char* CLIENT_ID = "myproj-esp32-001"; // 每台设备必须唯一 const char* PREFIX = "myproj/esp32-001"; // 自己的主题前缀 // ================================ WiFiClient wifiClient; // 底层 TCP 连接 PubSubClient mqtt(wifiClient); // MQTT 客户端 unsigned long lastReport = 0; // 记录上次上报时间,用于定时 // 连接家里 WiFi,没连上就一直等 void connectWifi() { WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi 已连接"); } // 收到订阅消息时的回调:topic 是主题,payload 是内容字节 void onMessage(char* topic, byte* payload, unsigned int len) { String t = String(topic), body = String((char*)payload).substring(0, len); // 把字节转成字符串好处理 if (t.endsWith("/cmd/set")) { // 收到开关灯指令:内容里有 true 就点亮 bool on = body.indexOf("true") >= 0; digitalWrite(LED_BUILTIN, on ? HIGH : LOW); // 控制开发板自带 LED mqtt.publish((String(PREFIX) + "/cmd/set/ack").c_str(), // 回执:告诉应用端已执行 ("{\"ok\":true,\"led\":" + String(on ? "true" : "false") + "}").c_str()); } } // 开机初始化,只跑一次 void setup() { Serial.begin(115200); // 打开串口监视器方便看调试输出 pinMode(LED_BUILTIN, OUTPUT); // LED 引脚设为输出 connectWifi(); configTime(8 * 3600, 0, "ntp.aliyun.com"); // 联网校时(东八区),这样上报能带真实时间戳 mqtt.setServer("mqttyyc.top", 1883); // Broker 地址;要加密换 WiFiClientSecure + 8883 mqtt.setCallback(onMessage); // 登记收消息回调 mqtt.setKeepAlive(45); // 每 45 秒心跳一次 } // 主循环,反复执行 void loop() { if (WiFi.status() != WL_CONNECTED) connectWifi(); // WiFi 断了就重连 if (!mqtt.connected()) { // MQTT 断了就重连 // connect 参数较多:Client ID、账号、密码、遗嘱主题、遗嘱 QoS、遗嘱保留、遗嘱内容 offline // 遗嘱作用:设备异常掉线时 Broker 自动代发 offline(见第 5 章) if (mqtt.connect(CLIENT_ID, MQTT_USER, MQTT_PASS, (String(PREFIX) + "/status").c_str(), 1, true, "offline")) { mqtt.publish((String(PREFIX) + "/status").c_str(), "online", true); // 连上后报在线(保留消息) mqtt.subscribe((String(PREFIX) + "/cmd/#").c_str()); // 订阅自己的指令主题 Serial.println("MQTT 已连接"); } } mqtt.loop(); // 必须经常调用:收消息、发心跳全靠它 if (millis() - lastReport > 5000) { // 距上次上报超过 5 秒 → 上报一次 lastReport = millis(); // 手工拼 JSON:dev 设备名、rssi 信号强度、up 运行秒数、ts 时间戳 String json = "{\"dev\":\"" + String(CLIENT_ID) + "\",\"rssi\":" + String(WiFi.RSSI()) + ",\"up\":" + String(millis() / 1000) + ",\"ts\":" + String((long)time(nullptr)) + "}"; mqtt.publish((String(PREFIX) + "/data").c_str(), json.c_str()); } }
13.4 Web 调试页(存成 html 双击就用)
不想装任何工具?把下面整份存成 mqtt-debug.html,双击用浏览器打开,填上账户密码就能在网页里订阅发布(走 WSS 8084):
<!-- 保存为 mqtt-debug.html,双击打开即用 --> <!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>MQTT Web 调试</title> <script src="https://unpkg.com/mqtt@5/dist/mqtt.min.js"></script> <style> body { font: 14px/1.7 system-ui, sans-serif; max-width: 760px; margin: 24px auto; padding: 0 16px; } input, button { padding: 6px 10px; margin: 3px 2px; border: 1px solid #888; border-radius: 6px; } #log { height: 280px; overflow: auto; background: #12261b; color: #9fd8ae; padding: 10px; border-radius: 8px; white-space: pre-wrap; } </style> </head> <body> <h2>MQTT Web 调试页</h2> <input id="user" placeholder="用户名"> <input id="pass" type="password" placeholder="密码"> <button onclick="doConnect()">连接</button> <button onclick="client && client.end()">断开</button><br> <input id="sub" value="mytest/#" size="18"> <button onclick="client.subscribe(sub.value)">订阅</button> <input id="topic" value="mytest/hello" size="14"> <input id="msg" value="你好 MQTT" size="14"> <button onclick="client.publish(topic.value, msg.value)">发布</button> <pre id="log"></pre> <script> let client; // MQTT 客户端对象,连接前是空的 const logEl = document.getElementById("log"); // 页面底部的日志区域 const log = (s) => { logEl.textContent += s + "\n"; }; // 往日志里追加一行 // 点“连接”按钮时执行 function doConnect() { client = mqtt.connect("wss://mqttyyc.top:8084/mqtt", { // 浏览器必须用 wss 加密地址 username: user.value, // 从输入框读取账号密码 password: pass.value, clientId: "web-debug-" + Math.random().toString(16).slice(2, 8), // 随机 ID,避免和别人撞 reconnectPeriod: 3000, // 断线 3 秒后自动重连 }); client.on("connect", () => log("✔ 已连接")); // 连接成功→日志提示 client.on("message", (t, p) => log(t + " -> " + p.toString())); // 收到消息→打印主题和内容 client.on("error", (e) => log("✘ " + e)); // 出错→打印错误信息 } </script> </body> </html>
常见问题排查
先对照第 6 章的返回码表,再查下面的现象清单:
| 现象 | 大概率原因 | 怎么查 |
|---|---|---|
| 连接立刻被拒(rc=4/5) | 用户名密码错误,或没注册 | 到账户中心确认账户存在;重新复制凭据,注意前后空格 |
| 连上几秒就被踢 | 另一台设备用了相同 Client ID | 给每台设备换唯一的 Client ID |
| 连 8883 报证书错误 | 客户端没开 TLS,或不信任公共 CA | 确认协议选的是 mqtts/SSL;关闭自签严格校验或改用系统根证书 |
| 发布了但订阅方收不到 | 主题不匹配 / 通配符写错 / QoS 搭配 | 检查订阅主题是否精确匹配或通配正确;先订阅再发布 |
| 网页里连不上 | HTTPS 页面用了 ws://,或漏了 /mqtt 路径 | HTTPS 页面改用 wss://mqttyyc.top:8084/mqtt |
| 反复掉线重连 | 频繁断连触发防抖检测(临时封禁 15 分钟),或 Client ID 冲突 | 修复重连风暴(加退避),等 15 分钟自动解除;检查网络稳定性 |
| 大消息发不出去 | 超过普通账户 64 KB 限制 | 拆分消息或申请超级用户(≤ 1 MB) |
| 发布被"吞"了(无报错也不转发) | 触发发布限流(100 条/秒 或 128 KB/秒) | 限流时 Broker 暂停接收而不断连;降低上报频率或合并消息 |
| 订阅报错 / 被拒 | 超过单客户端 100 个主题的订阅上限 | 用通配符合并订阅,或拆分到多个连接 |
术语表
| 术语 | 含义 |
|---|---|
| Broker | 消息代理服务器,负责接收、过滤、分发消息(本站由 EMQX 提供) |
| Publisher / Subscriber | 发布者 / 订阅者,即发送和接收消息的客户端 |
| Topic | 主题,消息的"频道",用 / 分层 |
| Payload | 消息体,具体内容,可以是文本也可以是二进制 |
| Client ID | 客户端在 Broker 上的唯一标识 |
| QoS | 服务质量等级:0 最多一次 / 1 至少一次 / 2 恰好一次 |
| Retained Message | 保留消息,新订阅者立刻能收到的最新一条 |
| Last Will | 遗嘱消息,客户端异常断开时由 Broker 代发 |
| Keep Alive | 心跳间隔,客户端定期发 PINGREQ 保活,Broker 回 PINGRESP |
| CONNACK | 连接应答报文,携带连接成功/失败的返回码(见第 6 章) |
| PUBACK / PUBREC / PUBREL / PUBCOMP | QoS 1/2 投递确认报文,见第 4 章握手流程 |
| Session | 会话,保存订阅关系和未投递消息的状态 |
| Shared Subscription | 共享订阅,$share/组名/主题,组内成员负载均衡消费消息 |
| $SYS | Broker 内部监控主题前缀,普通客户端不可订阅 |
| TLS/SSL | 传输加密层,对应本站 8883 / 8084 端口 |