1. 先弄明白MQTT的底层逻辑:它凭什么成为物联网事实标准
做物联网项目做得久了,你会发现一个很有意思的现象:不管是做智能家居、工业数据采集,还是做智慧农业、车联网,大家最后都会不约而同地选MQTT来跑业务消息。我在几个项目里试过HTTP轮询、TCP长连接自研协议、甚至直接拿WebSocket硬顶,兜兜转转一圈下来,最后还是回到MQTT。原因不复杂,它解决的是物联网场景里最痛的那几个问题:设备网络不稳定、设备数量多、消息量小但频繁、有的消息丢了也不行,有的消息丢了也无所谓。
1.1 发布/订阅模型:通信不再是一对一的直连
MQTT全称Message Queuing Telemetry Transport,消息队列遥测传输。名字里带"遥测"两个字,就知道它一开始就是为远程数据采集设计的。1999年IBM出的协议,最初用在石油管道卫星通信上,那个场景里带宽贵、网络差、数据要省着传,所以协议本身就做得非常克制——报文头最小只有2个字节,一个PUBLISH消息的固定头部压缩到极致,这对嵌入式设备来说简直是救星。
它和传统HTTP最大的区别在于通信模型。HTTP是请求-响应模式,客户端问一次、服务器答一次,设备得知道服务器的地址,主动去连。MQTT则是发布/订阅模式,所有的消息都通过一个中间人——Broker(消息代理服务器)来转发。发送消息的人(Publisher)不关心谁在听,接收消息的人(Subscriber)也不关心消息是谁发的,双方只和Broker打交道。
我用一个比喻帮你理解:MQTT的Broker就像一个公告板。你想把设备温度告诉所有人,就把写着温度的字条贴到公告板上,谁想看谁自己来读。不需要知道读的人是谁,也不需要等对方确认收到了。而HTTP就像打电话,你得知道对方的号码,拨过去,一句一句对话。
这个模型解决了物联网里一个很实际的痛点:设备和服务器之间,到底谁先找谁?如果设备在内网,没有公网IP,服务器想主动连设备是做不到的。HTTP轮询倒是可以让设备主动问,但设备多了以后效率极低,而且实时性差。MQTT让设备主动去连Broker,建立起一条长期维持的TCP通道,然后这条通道双向都能走消息——服务器想给设备下发指令,直接往对应的Topic上发消息,Broker就推给设备了。设备端的NAT、防火墙问题全部绕开,这是MQTT在物联网里能被大规模采用的底层原因。
1.2 QoS等级:一条消息三种活法
MQTT定义了三个QoS(Quality of Service,服务质量)等级,对应消息从发送端到接收端三种不同的保障级别。这是协议里最容易让新手困惑的地方,也是实际开发中踩坑最多的地方。
- QoS 0(至多一次):发出去就不管了,不确认、不重发。消息可能丢,适合丢一条也无所谓的数据,比如周期性上报的温度、湿度。
- QoS 1(至少一次):发送端发出消息后等Broker回一个PUBACK确认包,等不到就重发。能保证消息到达,但可能重复。适合命令下发场景,丢指令的后果比重复执行严重得多。
- QoS 2(恰好一次):通过四步握手协议(PUBLISH→PUBREC→PUBREL→PUBCOMP),保证消息既不丢也不重。代价是报文交互翻倍,吞吐量降得厉害,适合计费、订单这类要求严格不重复的业务。
很多初学者一上来就全部用QoS 2,觉得"越可靠越好",这是典型的过度设计。物联网设备普遍算力有限、网络带宽有限,消息量大时QoS 2的握手开销能把Broker拖垮。我的习惯是:设备遥测数据用QoS 0或QoS 1,命令下发用QoS 1,真正涉及资金和状态机切换的才用QoS 2。你可以在开发环境里用QoS 2测试,生产环境按业务场景降级,这个后面实战部分我会再细说。
1.3 主题Topic设计:消息的路由规则
Topic是MQTT消息的路由地址,结构上类似文件系统的路径,用斜杠分隔层级,比如factory/zone1/device001/temperature。它不像HTTP有URL那么强的语义,Topic对Broker来说就是一个字符串匹配规则,但设计得好不好,直接决定你后面业务开发是丝滑还是痛苦。
Topic支持通配符,这是它路由能力的核心:
factory/+/device001/temperature:单层通配,匹配factory/任何内容/device001/temperaturefactory/zone1/#:多层通配,匹配factory/zone1/下面的所有层级
我在多个项目里总结了一套比较稳的Topic命名规范,你可以直接参考:
| Topic类型 | 命名示例 | 用途 |
|---|---|---|
| 遥测数据 | dev/{deviceId}/telemetry | 设备周期性上报的测量值 |
| 事件上报 | dev/{deviceId}/event | 告警、开关门、故障等事件 |
| 命令下发 | dev/{deviceId}/cmd | 服务器或上位机下发的指令 |
| 指令响应 | dev/{deviceId}/resp | 设备执行指令后返回的结果 |
这套规范的好处是:设备ID固定,消息类型清晰,权限控制好做,后续加新设备类型也不用改结构。还有一点经验是Topic层级不要超过四层,太深了影响Broker匹配性能,而且在通配符订阅时容易出幺蛾子。
2. Windows下搭建MQTT服务器:从装包到跑通的完整过程
热词里很多人搜"windows安装mqtt安装包",可见Windows环境是开发者接触MQTT的第一站。我最初也是在Windows上搭的开发环境,不做生产服务器,图的就是方便、能快速验证业务逻辑。这里我把两个主流Broker的搭建过程都讲一遍,你可以按需选择。
2.1 选型对比:EMQX还是Mosquitto?
我个人推荐的组合是:本地开发用Mosquitto,功能验证和压测用EMQX。两者的定位差异很明显。
Mosquitto是Eclipse基金会出的轻量Broker,C语言写的,安装包才几MB,资源占用极低,一台树莓派都能跑。功能相对基础,但MQTT 3.1.1和5.0都支持,基本的用户名密码鉴权、TLS加密、WebSocket监听都有。它最大的短板是管理界面,官方不带图形化控制台,调试的时候得靠命令行工具mosquitto_sub和mosquitto_pub,或者借助第三方可视化工具。
EMQX是国产开源Broker里国际影响力最大的一个,Erlang语言写的,天生擅长处理高并发连接。单机百万级连接对它来说是小意思,而且自带一个完整的Dashboard控制台,在线连接数、消息吞吐、订阅关系全部可视化,还内置了规则引擎,消息进来可以直接转发到数据库或HTTP服务。缺点是安装包大(Windows安装包两三百MB),内存占用相对高,跑在开发机上有点杀鸡用牛刀。
如果你是刚开始学MQTT,先装Mosquitto;如果你在给真实项目选型,直接上EMQX,省得后期迁移。
2.2 Mosquitto的安装与基础配置
Mosquitto在Windows上的安装已经很友好了,从官网或GitHub Release页下载exe安装包,一路Next就能装完。装完之后它不是以服务方式自动启动的,需要手动跑一下,或者注册成Windows服务。我用最直接的命令行方式演示:
# 进入安装目录 cd C:\Program Files\mosquitto # 前台方式启动,使用默认配置,默认监听1883端口 mosquitto -v看到Starting mosquito version x.x.x和Opening ipv4 listen socket on port 1883这两行,说明Broker已经起来了。此时打开另一个命令行窗口,用自带的命令行客户端验证一下:
# 订阅一个主题 mosquitto_sub -t "test/topic" -v # 新开一个窗口,发布消息 mosquitto_pub -t "test/topic" -m "hello mqtt"如果你在订阅窗口能看到hello mqtt,恭喜,你的MQTT服务器已经跑通了。
需要说明的是,mosquitto -v启动的是前台进程,窗口一关服务就没了。要长期运行,建议注册成服务:
mosquitto install net start mosquittoMosquitto的配置文件在安装目录下的mosquitto.conf,几个常用的配置项我给你列出来:
| 配置项 | 取值示例 | 作用 |
|---|---|---|
port | 1883 | 默认MQTT监听端口,明文传输 |
allow_anonymous | false | 禁止匿名连接,生产环境必须关掉 |
password_file | C:/mosquitto/pwfile | 用户名密码文件路径 |
listener 8883 | - | 开启TLS加密端口,生产环境推荐 |
顺便提醒一句:1883端口是MQTT的默认端口,明文传输。这意味着消息在网络上是裸奔的,只要能抓包就能看到内容。生产环境务必配置TLS加密(8883端口),或者至少做好网络隔离。
2.3 EMQX的安装与Dashboard使用
EMQX从5.0开始,Windows安装包已经非常成熟,到官网下载Windows zip包,解压后进入目录执行:
bin\emqx start启动成功后浏览器打开http://localhost:18083,默认用户名admin,密码public,登录后就能看到Dashboard。这里你可以直观地看到Broker的运行状态,包括当前连接数、消息收发速率、订阅主题数量。
Dashboard里我用的最多的是两个功能。一是"主题订阅"页面,可以模拟一个客户端去订阅任意Topic,查看实时消息流;二是"规则引擎",可以把指定Topic的消息直接桥接到MySQL、Kafka或者Webhook。之前做工业数据采集项目,我就是用EMQX规则引擎把设备遥测数据实时转发到Kafka,后端数据服务直接从Kafka消费,完全不用自己写数据接入层。
Windows上跑EMQX做开发没问题,但生产环境我强烈建议部署到Linux服务器上,性能和稳定性都不是一个量级。
3. 订阅与发布实战:把这套消息模型跑起来
服务器搭好了,接下来要解决的是实际开发问题。热词里"mqtt订阅与发布消息"被搜的次数很多,可见很多人卡在客户端开发这一步。我把开发工具和写代码两个层面都讲清楚。
3.1 用MQTTX快速打通全链路
写代码之前,强烈建议先用一款图形化客户端把全链路跑通。我用的是MQTTX,跨平台免费,界面清爽,最关键的它同时支持Web端和桌面端,可以在两台设备上各开一个客户端,模拟真实的发布订阅场景。
MQTTX里新建连接时,只需要填三样东西:Broker地址(比如mqtt://localhost:1883)、客户端ID(每个连接必须唯一,我习惯用mqttx_前缀加随机串)、用户名密码(如果Broker开了鉴权)。连接成功后,左边添加订阅,输入Topic名称和QoS等级,右边输入要发送的Topic和Payload,点发送就能在订阅窗口看到消息。
用MQTTX做联调有一个好处:它能非常直观地展示消息的收发时序和QoS确认过程。调QoS 1和QoS 2时,你可以在"消息日志"里看到完整的PUBACK、PUBREC、PUBREL、PUBCOMP报文交互记录。我的调试习惯是先在MQTTX里验证协议行为,确认无误后再对着写业务代码,能省掉一半的排查时间。
3.2 Python客户端:订阅与发布的最小实现
Python下用paho-mqtt库,这是在物联网开发社区里最主流的MQTT客户端库,没有之一。装好之后,一个完整的订阅加发布代码如下:
import paho.mqtt.client as mqtt BROKER_ADDR = "localhost" BROKER_PORT = 1883 CLIENT_ID = "python_demo_001" # 连接回调 def on_connect(client, userdata, flags, reason_code, properties=None): if reason_code == 0: print("连接成功") client.subscribe("dev/001/telemetry", qos=1) else: print(f"连接失败,错误码: {reason_code}") # 消息回调 def on_message(client, userdata, msg): print(f"收到主题: {msg.topic}, 消息: {msg.payload.decode()}") client = mqtt.Client(client_id=CLIENT_ID, protocol=mqtt.MQTTv311) client.username_pw_set("iot_user", "iot_pass") client.on_connect = on_connect client.on_message = on_message client.connect(BROKER_ADDR, BROKER_PORT, keepalive=60) client.loop_forever()这段代码的逻辑很简单:连接Broker,连接成功后订阅dev/001/telemetry这个主题,收到消息就打印出来。你要发布消息,只需要调用一行接口:
client.publish("dev/001/telemetry", '{"temp": 26.5, "humidity": 60}', qos=1)publish方法的第一个参数是主题,第二个是消息内容,第三个是QoS等级。注意publish是异步的,它会立即返回一个MQTTMessageInfo对象,真正的发送是在后台事件循环里完成的。
这里的client.loop_forever()是阻塞式的消息循环,适合脚本程序。如果你的程序同时还要做别的事情(比如处理串口数据),改用client.loop_start()启动一个后台线程处理网络消息。这个区别看似小,实际项目里掉了好多坑。
3.3 QoS和Retained消息要在什么场景用
网上很多教程把QoS讲得很玄乎,其实选型逻辑非常朴素。QoS 0适合高频遥测,比如温湿度传感器每5秒上报一次,丢一帧根本无所谓;QoS 1适合大部分场景,特别是指令下发,宁可重复也不能丢;QoS 2用得极少,除非消息内容会触发资金变动、状态机强一致切换这类业务。
还有一个很容易被忽略的功能是Retained消息(保留消息)。发布消息时可以设置retain=True,Broker会把这条消息存起来,之后任何客户端订阅这个主题,都会立刻收到最新一条保留消息。这个特性在设备状态同步场景下极其好用。比如网关程序上线后,往dev/{deviceId}/status发一条online的保留消息,任何新的订阅者一上来就知道当前设备在线,不需要等下一次心跳上报。
有一类典型的需求是"新客户端连上来,我要立刻知道当前设备状态",用普通发布消息做不到——客户端来晚了,消息已经发过了。Retained消息就是为了解决这个问题设计的。我自己在设备管理项目里就是这样用的,设备上下线状态、最近一次仪表读数,全部用retain=True发布,管理端刷新列表时直接缓存命中,体验好到飞起。
4. 把MQTT接到485设备上:指令下发与数据上云的完整链路
"mqtt如何给485设备发指令"这个热词搜的人特别多,说明这是一个既有代表性和含金量的实战场景。RS485是工业现场最常用的总线标准,电表、水表、温控器、变频器、各种传感器,出厂基本都带RS485接口。而这些设备本身没有网络功能,更不会说MQTT,要让它们接入物联网平台,必须有一层转换。
4.1 485设备为什么需要MQTT这张"皮"
RS485是一种串行通信标准,半双工,最远传输距离1200米左右,一条总线上可以挂128个设备。它的通信方式是主从问答式:主站(通常是PLC、工控机或电脑)发一条指令报文,从站设备响应,一问一答,协议层面常见的是Modbus RTU。主站发什么、从站回什么,全部是按字节组织的十六进制数据帧。
问题来了:RS485走的是串口,数据只能在物理连线的设备之间传递。你要在千里之外的办公室里看电表读数,或者远程控制车间的阀门,需要把485总线和互联网打通。MQTT在这里扮演的角色就是"物联网的消息总线"——485设备的数据先被本地网关采集到,网关把十六进制帧解析成JSON格式,再通过MQTT发布到Broker的Topic上;服务器要下发指令,也是往Topic上发一条JSON指令,网关订阅到之后转换成Modbus报文,从485串口发出去。
整个链路可以概括成三层:
- 物理接入层:RS485总线上的设备,通过USB转485、串口服务器或网关接入本地计算设备
- 协议转换层:边缘网关负责Modbus RTU协议和MQTT消息之间的翻译
- 云端平台层:Broker负责消息路由,应用端通过Topic订阅/发布完成业务逻辑
这里最关键的是第2层,协议转换的处理质量直接决定了整个系统的稳定性和响应速度。第一次做485接入MQTT的朋友最容易犯的错,是直接把原始的十六进制报文原封不动地发到MQTT上去,让云端去解析。这样做不是完全不行,但会把协议解析的压力全部压到应用端,而且Modbus的功能码、寄存器地址、CRC校验这些细节暴露在业务链路上,后续维护非常痛苦。
我的经验是:概念上要让MQTT消息贴近业务语义,而不是贴近串口报文的物理表示。网关向上发布的时候,已经把寄存器地址翻译成了业务字段。比如电表的电压,你最终看到的是一条JSON消息:
{ "deviceId": "meter_001", "type": "voltage", "value": 220.5, "timestamp": 1711417200 }而不是一行01 03 02 08 A1这样的原始报文。
4.2 硬件选型:三种常见的485转MQTT网关方案
把485设备接入MQTT,市面上有现成的硬件方案,也有自己写软件的自研方案,我按适配场景把它们分类说明。
方案一:硬件串口服务器直连MQTT
这是最省事的方式。有些厂商(比如有人物联网、四信)的串口服务器/工业网关固件里直接内置了MQTT功能,你只需要在网页配置界面里填上:串口波特率(比如9600)、数据位(8)、校验位(无)、停止位(1)、目标Broker地址、设备Topic、心跳间隔,它就会把串口收到的数据帧原封不动地推到MQTT上去。这种方案适合快速验证,但要注意:设备发上来的原始Modbus报文不会自动解析,你拿到的是十六进制字符串,需要在云端自行解析。
方案二:边缘网关/边缘计算盒子做协议转换
针对有解析需求的项目,方案是选一个边缘网关(树莓派、工控机、Jetson都可以),在边缘节点上跑一个协议转换程序。这个程序向下通过串口收发Modbus报文,向上发布解析好的JSON到MQTT Broker。这样做的好处一是数据在边缘侧已经清洗好了,云端的逻辑简单;二是网关本身可以做本地缓存、断网自动重发;三是可以在边缘直接执行本地联动逻辑,比如超限报警,不必等云端决策。
方案三:DTU透传+Bridge桥接
现有的Modbus网关不支持MQTT,但支持TCP Client模式(即DTU)。你可以在Broker所在服务器上跑一个桥接程序,这个程序同时作为TCP Server接收DTU的透传数据,再用MQTT客户端把数据转发到Broker。本质是让普通的DTU也能间接接入MQTT。这个方案成本低,但稳定性依赖中间桥接进程,适合场景有限、预算有限的小规模项目。
4.3 软件实现:网关程序的核心逻辑示例
我以一个比较典型的场景为例:一台RS485电表,用的是Modbus RTU协议,通过USB转485接到一台Linux工控机上,要求把电表的电压、电流、功率数据通过MQTT定时上报到云端,同时支持远程下发命令读取某个特定寄存器。
先解释一下Modbus RTU报文是怎么构成的。以读保持寄存器为例:
- 从站地址:1字节(比如
01代表1号设备) - 功能码:1字节(
03代表读保持寄存器,06代表写单个寄存器,10代表写多个寄存器) - 起始寄存器地址:2字节
- 寄存器数量:2字节
- CRC校验:2字节(低字节在前)
所以读1号从站、起始寄存器地址0、读2个寄存器的原始报文是:
01 03 00 00 00 02 C4 0B在Python里,用pyserial库发送这段报文,然后接收响应。完整的网关核心代码大概是:
import serial import json import time import paho.mqtt.client as mqtt SERIAL_PORT = "/dev/ttyUSB0" BAUD_RATE = 9600 BROKER_ADDR = "192.168.1.100" BROKER_PORT = 1883 def build_modbus_read_frame(slave_id, func_code, start_addr, quantity): frame = bytearray() frame.append(slave_id) frame.append(func_code) frame.extend(start_addr.to_bytes(2, byteorder='big')) frame.extend(quantity.to_bytes(2, byteorder='big')) crc = calc_crc(frame) frame.extend(crc.to_bytes(2, byteorder='little')) return frame def calc_crc(data): crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc # 初始化串口 ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1) # 初始化MQTT客户端 client = mqtt.Client(client_id="gateway_485") client.username_pw_set("gw_user", "gw_pass") client.connect(BROKER_ADDR, BROKER_PORT, keepalive=60) client.loop_start() # 读取Modbus数据并发布到MQTT def poll_and_publish(): frame = build_modbus_read_frame(1, 3, 0, 2) # 读1号设备0寄存器2个量 ser.write(frame) resp = ser.read(7 + 2 * 2) # 7字节响应头 + 2寄存器*2字节 + 2字节CRC # 这里需要完整解析响应帧,并换算成实际电压/电流值 payload = {"deviceId": "meter_001", "voltage": 220.5, "current": 1.32} client.publish("dev/meter_001/telemetry", json.dumps(payload), qos=1) while True: poll_and_publish() time.sleep(10)这只是个演示用的骨架代码,实际项目里你需要处理的事情包括:Modbus响应帧的超时重试、数据异常值过滤、CRC校验失败丢弃、多个从站设备轮询调度。这些逻辑如果放在循环里全凭经验写,很容易出问题。我第5章里会专门讲几个坑。
4.4 完整演示:远程下发指令到485设备并取回结果
再来演示一个更完整的指令往返流程,这个流程基本覆盖了"给485设备发指令"的全部链路。
场景:云端服务器的某个业务系统要读取485电表上的某个寄存器地址,让它返回数据。
第一步:业务系统往命令主题发布一条指令消息
client.publish( "dev/meter_001/cmd", json.dumps({ "cmd": "read_register", "slave_id": 1, "func_code": 3, "start_addr": 2, "quantity": 2 }), qos=1 )第二步:网关程序通过MQTT订阅到这条指令,解析消息,组装Modbus帧,通过串口发给485设备:
def on_cmd_message(client, userdata, msg): cmd = json.loads(msg.payload.decode()) if cmd.get("cmd") == "read_register": frame = build_modbus_read_frame( cmd["slave_id"], cmd["func_code"], cmd["start_addr"], cmd["quantity"] ) ser.write(frame) resp = ser.read(7 + cmd["quantity"] * 2) # 解析寄存器值并发布到响应主题 parsed = parse_modbus_response(resp) client.publish( "dev/meter_001/resp", json.dumps({"request_id": cmd.get("request_id"), "data": parsed}), qos=1 )第三步:云端业务系统订阅响应主题,拿到结果。整个流程在1秒内完成。
这套"cmd下发→网关执行→resp响应"的消息结构,你几乎可以套用到任何485设备上。指令是Modbus读还是写、是单个还是多个寄存器,都只是Instruction Payload字段的变化,消息骨架不用动。我用这套方案接了电表、温控器、阀门执行器多种设备,全部一套逻辑跑通。
5. 实践中的坑和优化经验
5.1 消息洪峰与QoS降级:别让可靠性拖垮Broker
我最开始做MQTT接入的时候,设备侧全部用QoS 1上报,觉得这样最稳。结果设备数量一上千,Broker的CPU和带宽占用直线飙升,1883端口的消息吞吐出现明显瓶颈。后来分析才明白,QoS 1每一次消息送达都要多一次PUBACK往返,消息量翻了将近一倍;换成QoS 2更是翻了四倍。
这里我的建议是:遥测类的数据,如果业务上允许丢失少量点,就用QoS 0;如果担心偶发丢包,可以在应用层做序列号检查,发现有空洞就主动重发,比全局QoS 1高效得多。对于指令类消息,QoS 1是底线,且一定要设计指令的幂等性——也就是说,重复收到同一条指令也不能产生副作用,比如"开阀门"指令设计成"设置阀门开度为80",而不是"切换阀门开关状态"。这样即使消息重复了,执行的结果也是一样的。
5.2 心跳、保活和断线重连的机制配合
MQTT连接的本质是TCP长连接,但TCP连接是会假死的——网线被拔、设备睡眠、网络切换,这些情况下TCP层面可能感知不到连接已经失效。MQTT的解决方案是keepalive机制:客户端每隔interval秒发一个PINGREQ包给Broker,Broker在1.5倍interval时间内没有收到任何包,就把这个连接断开。
客户端里的keepalive参数就是这个interval,默认60秒。很多设备放在信号不稳定的环境里,60秒已经偏长了。我的习惯是根据场景设到20-30秒,太短又会在弱网环境下造成频繁断连,需要找平衡点。
断线重连是另一个很重要的机制。客户端因为网络抖动连不上Broker了,不能退出,要按退避策略逐步重连。paho-mqtt库有一个reconnect_delay_set方法可以设置最小和最大重连延迟,但这只是基础退避,真实场景我还会结合连接状态回调做本地缓存。设备侧的逻辑可以是:
- 连接正常时,数据实时发送
- 连接断开时,数据落本地缓存(内存队列或SQLite)
- 重连成功后,按时间顺序补发缓存里的数据
这套逻辑在网关程序里非常实用,尤其是在485转MQTT的场景,串口数据本来就来自真实的物理设备,丢了就再也补不回来,所以本地缓存是保数据完整性的关键手段。
5.3 主题权限最小化与多租户隔离
当你的Broker上不止一个客户端时,权限控制就是刚需了。EMQX和Mosquitto都支持ACL(Access Control List)规则,可以限制某个用户名能订阅和发布哪些Topic。这个必须从项目第一天就设计好,否则后面设备越来越多,权限只能一刀切,安全隐患非常大。
一个常见的权限设计模式是:
- 设备端证书/账号只能发布
dev/{deviceId}/telemetry和dev/{deviceId}/event,只能订阅dev/{deviceId}/cmd - 应用端账号可以订阅所有
dev/#,但只能发布dev/{deviceId}/cmd - 管理后台账号可以订阅
$SYS/#(Broker系统主题)监控整体运行状态
这么做的好处是,即使设备的账号泄露,攻击者也只能操控这一台设备,无法干扰其他设备的数据流。
5.4 Broker的$SYS主题值得你多看一眼
很多初学者不知道,EMQX和Mosquitto都有以$SYS开头的系统主题,里面发布的是Broker自己的运行指标。比如订阅$SYS/broker/load/publish/received,你就可以实时看到Broker每秒收到多少条消息。我自己做项目排查的第一步永远是去Dashboard或者$SYS主题看一眼设备和消息量,确认不是Broker层面的瓶颈,再往下逐层排查。
生产环境运维MQTT,我建议至少要积累这些指标:当前连接数、消息发布速率、订阅总数、在线设备数、带宽占用。EMQX的Dashboard已经把这些都可视化了,但如果你用的是Mosquitto,需要自己定时去捞$SYS主题做记录。
6. 最后再说一句关于MQTT版本的选择
MQTT有两个主版本:MQTT 3.1.1和MQTT 5.0。新项目选型时到底用哪个,我的建议是:如果没有特殊需求,直接选MQTT 5.0。
3.1.1是2014年发布的版本,稳定、普及率高,几乎所有物联网平台都支持。5.0在2019年发布,并没有大改协议本身的发布订阅模型,而是增加了会话过期时间、订阅标识符、用户属性、消息过期时间、请求响应机制这些"锦上添花"的能力。
5.0里我最喜欢的一个特性是消息过期时间(Message Expiry Interval)。它允许一条消息在指定时间内没有消费者读取就自动丢弃,非常适合指令类业务——比如"开灯"指令如果设备离线30秒,这条指令本来就没有意义了,与其等设备上线后再执行,不如让它过期作废。这在3.1.1里是做不到的。
如果是刚开始学习MQTT,3.1.1足够用了,API跟5.0基本兼容,学会了3.1.1再切5.0几乎没有学习成本。但如果是新项目从零开始,建议直接按5.0设计,毕竟协议本身的演进方向是明确的。
我自己做过的几个项目里,遇到过各种奇奇怪怪的问题——485设备收不到指令、MQTT消息堆积、断线重连风暴、Topic权限配错导致数据泄露,每一个坑背后都有很具体的原因和排查思路。这个领域的知识体系其实不大,核心就是协议机制、Broker配置、客户端开发、边缘网关转换这四块,把这四块打通了,任何物联网场景的MQTT应用都能拿得下来。