MQTT 这个协议,我第一次接触是在一个远程环境监测项目里。当时设备分布在好几个不同的物理位置,网络条件参差不齐,有的地方信号弱到 HTTP 请求十次有三次超时。后来换成 MQTT,同样的硬件、同样的网络,消息到达率直接上了一个台阶。从那以后,但凡遇到设备间通信、数据采集、远程控制这类场景,我基本都会优先考虑 MQTT。
这篇文章想聊的,是我在实际项目中积累下来的 MQTT 核心机制理解——发布订阅模型到底怎么运转、QoS 等级怎么选才不浪费资源也不丢消息、遗嘱消息在什么场景下能救命、持久会话又解决了什么问题。内容会从基础概念一路讲到实战配置,中间穿插我在真实项目里踩过的坑和总结出来的参数选择逻辑。不管你是刚接触物联网通信的开发者,还是已经在用 MQTT 但想搞清楚底层机制的老手,应该都能从里面找到对自己有用的东西。
1. 发布订阅模型:为什么它比请求响应更适合物联网
1.1 从 HTTP 的痛点说起
大部分开发者最早接触的网络通信模型是请求响应式的,比如 HTTP。客户端发一个请求,服务器返回一个响应,一问一答,逻辑清晰。这种模式在 Web 场景下工作得很好,但放到物联网环境里就有点力不从心了。
我做过一个温度监控系统,最初用 HTTP 轮询的方式:每个传感器节点每隔几秒向服务器发一次数据上报请求。看起来没问题,但设备数量一多,问题就暴露了。一百个节点、每五秒轮询一次,服务器每秒要处理二十个请求,这还只是小规模。如果是一千个节点呢?每秒两百个请求,而且大部分请求携带的数据可能根本没变化。更麻烦的是反向控制——服务器想给某个设备下发指令,只能等设备下次轮询时才能捎带回去,实时性完全没法保证。
发布订阅模型从根本上改变了这个局面。它引入了一个中间角色——消息代理(Broker),发布者和订阅者之间不再直接通信,而是通过 Broker 进行消息路由。发布者只管把消息发到某个主题(Topic)上,订阅者只管从自己关心的主题上收消息,双方互相不知道对方的存在。
1.2 发布订阅的核心角色拆解
整个模型里有三个关键角色,理解它们的分工是理解 MQTT 的基础。
发布者(Publisher)是消息的生产方。它不需要知道谁在听,只需要把消息按照约定的主题格式发给 Broker。比如一个温度传感器,它可能每隔几秒向sensors/room1/temperature这个主题发布一条包含当前温度值的消息。
订阅者(Subscriber)是消息的消费方。它向 Broker 表达自己对哪些主题感兴趣,可以精确订阅一个主题,也可以用通配符订阅一批主题。比如一个监控大屏可能订阅sensors/+/temperature,这样所有房间的温度数据都能收到。
消息代理(Broker)是中间人,负责接收发布者的消息,并根据订阅关系把消息转发给对应的订阅者。Broker 是整个系统的核心,它的性能和稳定性直接决定了整个通信链路的质量。
这种解耦带来的好处非常明显。发布者和订阅者在时间上不需要同时在线——发布者发消息的时候订阅者可能离线,等订阅者上线后仍然能收到(前提是用了持久会话)。在空间上也不需要知道对方的地址——不需要配置 IP、端口,只需要约定好主题格式。在数量上更是灵活——一个发布者可以对应零个、一个或多个订阅者,反过来也一样。
1.3 主题设计与通配符的实战用法
主题是 MQTT 里最核心的概念之一,它决定了消息的路由规则。主题用斜杠/分隔层级,比如home/livingroom/light/status。这里有几个实战中总结出来的设计原则。
第一,层级从大到小,从左到右。先写大范围再写具体对象,比如factory/workshop1/machine3/vibration,这样用通配符订阅的时候更容易控制范围。
第二,避免用中文和特殊字符。虽然协议本身没有强制要求,但实际部署中中文主题在某些 Broker 实现上会出现编码问题,排查起来很头疼。
第三,不要以$开头。$开头的主题在 MQTT 里有特殊用途,比如$SYS/是 Broker 自己发布系统信息的主题,普通消息用$开头容易冲突。
通配符有两种,用好了能大幅简化订阅逻辑。
+匹配单层。比如home/+/temperature能匹配home/livingroom/temperature和home/bedroom/temperature,但匹配不了home/livingroom/sensor1/temperature。#匹配多层,必须放在主题末尾。比如home/#能匹配home下面所有层级的主题。
注意:
#只能出现在主题的最后,home/#/temperature这种写法是非法的。+可以出现在任意层级,但只能匹配一层。
我踩过一个坑:早期设计主题时用了device/data这种扁平结构,后来设备类型多了,想按类型筛选就非常麻烦。后来改成device/{type}/{id}/data的三层结构,用device/sensor/+/data就能一次性订阅所有传感器的数据,灵活多了。主题设计这件事,前期多花十分钟想清楚,后期能省十个小时的维护时间。
2. QoS 等级:消息可靠性的三档选择
2.1 三个等级到底有什么区别
QoS(Quality of Service)是 MQTT 保证消息可靠性的机制,分三个等级。很多人知道有这三个等级,但说不清楚它们的具体行为差异,导致选型时要么过度保守浪费资源,要么过于激进丢消息。
QoS 0——最多一次。发布者发出去就不管了,Broker 收到就转发,转发完就忘。消息可能丢失,但绝不会重复。适合那些偶尔丢一条也无所谓的场景,比如环境温度上报,丢一个数据点对整体趋势判断影响不大。
QoS 1——至少一次。发布者发出消息后等待 Broker 的 PUBACK 确认,如果没收到确认就重发。这保证了消息不会丢,但可能重复——比如 Broker 确实收到了消息,但 PUBACK 在返回途中丢了,发布者会重发,订阅者就会收到两条一样的消息。适合不能丢但能容忍重复的场景,比如开关控制指令,重复执行一次开灯操作结果是一样的。
QoS 2——恰好一次。通过四次握手(PUBLISH → PUBREC → PUBREL → PUBCOMP)确保消息既不丢失也不重复。这是可靠性最高的等级,但开销也最大,消息往返次数最多。适合那些重复会造成严重后果的场景,比如计费系统、医疗设备的数据上报。
2.2 消息传递中的 QoS 降级规则
这里有一个容易被忽略的细节:消息最终以发布和订阅中较低的 QoS 等级传递。
举个例子,发布者用 QoS 2 发布消息,但订阅者订阅时用的是 QoS 0,那么消息实际以 QoS 0 传递给这个订阅者。反过来,发布者用 QoS 0,订阅者用 QoS 2,消息也只能以 QoS 0 传递。
这个规则的原因很简单:QoS 0 的发布者根本没有保存消息副本用于重发,Broker 收到什么就只能转发什么,没法凭空提升可靠性。所以如果你需要某个主题的消息保证 QoS 2,必须发布端和订阅端都设置为 QoS 2。
2.3 不同场景下的 QoS 选型实战
选 QoS 等级的核心原则是:根据业务对丢消息和重复消息的容忍度来决定,而不是无脑选最高。
| 场景 | 推荐 QoS | 理由 |
|---|---|---|
| 环境传感器周期上报 | 0 | 数据量大,偶尔丢点不影响趋势分析 |
| 设备状态变更通知 | 1 | 不能丢,重复通知可以接受 |
| 远程控制指令 | 1 | 指令不能丢,重复执行通常幂等 |
| 计费/交易数据 | 2 | 重复会造成财务错误 |
| 心跳保活 | 0 | 丢了下一轮马上补上 |
我在一个智能农业项目里做过对比测试。大棚里有两百多个传感器节点,最初全部用 QoS 1,Broker 的 CPU 占用率一直在 60% 以上。后来把纯数据采集类的主题降到 QoS 0,只保留控制指令用 QoS 1,CPU 占用率直接降到 25% 左右,而业务上并没有感觉到数据质量下降。这个经验告诉我,QoS 选型要精细化,不要一刀切。
实操心得:如果你的 Broker 负载很高,先检查是不是所有主题都用了高 QoS。把那些"丢了也无所谓"的数据流降到 QoS 0,往往能释放大量资源。
3. 遗嘱消息:设备掉线时的最后一道防线
3.1 遗嘱消息的工作机制
遗嘱消息(Last Will and Testament)是 MQTT 里一个非常实用但经常被忽视的功能。它的逻辑是:客户端在连接 Broker 的时候,可以预先设置一条"遗嘱"——包括遗嘱主题、遗嘱内容和遗嘱 QoS。当客户端异常断开连接时(注意,是异常断开,不是主动断开),Broker 会自动把这条遗嘱消息发布出去。
什么算异常断开?网络中断、设备断电、程序崩溃导致 TCP 连接意外断开,这些都会触发遗嘱消息。而客户端主动发送 DISCONNECT 报文正常断开时,Broker 会丢弃遗嘱消息,不会发布。
这个机制解决了一个很实际的问题:如何及时知道一个设备掉线了。在没有遗嘱消息之前,我们通常靠心跳超时来判断,但心跳超时时间设短了容易误判,设长了发现掉线又太慢。遗嘱消息让设备在连接建立时就"留好遗言",一旦掉线 Broker 立刻发布,监控系统能第一时间收到通知。
3.2 遗嘱消息的配置参数详解
设置遗嘱消息需要在 CONNECT 报文中携带以下信息:
- Will Topic:遗嘱消息发布到哪个主题,比如
devices/sensor1/status - Will Payload:遗嘱消息的内容,通常是一个表示离线的字符串或 JSON,比如
{"status":"offline"} - Will QoS:遗嘱消息的 QoS 等级,建议用 1,确保监控端能收到
- Will Retain:遗嘱消息是否保留,建议设为 true,这样新上线的监控端也能立即看到设备离线状态
配置遗嘱消息的时机是在建立连接的时候,以 Python 的 paho-mqtt 库为例:
import paho.mqtt.client as mqtt client = mqtt.Client(client_id="sensor1") client.will_set( topic="devices/sensor1/status", payload='{"status":"offline","ts":' + str(int(time.time())) + '}', qos=1, retain=True ) client.connect("broker.example.com", 1883, 60)这段代码的意思是:如果 sensor1 这个客户端异常断开,Broker 会向devices/sensor1/status发布一条 QoS 1 的保留消息,内容是离线状态和时间戳。
3.3 遗嘱消息与保留消息的配合使用
遗嘱消息单独用效果有限,和保留消息(Retained Message)配合才能发挥最大价值。
保留消息的机制是:Broker 会为每个主题保存最后一条设置了 retain 标志的消息。当有新的订阅者订阅这个主题时,Broker 会立即把这条保留消息推送给它。这意味着监控端不需要等设备下次发布消息才能知道状态,一订阅就能拿到最新值。
把遗嘱消息设为 retain,效果就是:设备离线时 Broker 发布一条保留的离线消息,之后任何新订阅这个主题的监控端都会立刻收到"设备离线"的通知。设备重新上线后,正常发布一条 retain 的在线消息,覆盖掉之前的离线消息,新订阅者看到的就是在线状态。
这个组合我在多个项目里用过,效果很稳。有一个细节需要注意:设备正常上线后,一定要主动发布一条 retain 的在线状态消息,否则 Broker 上保留的还是上次的离线消息,新订阅者会误以为设备还没上线。
注意:遗嘱消息的延迟取决于 Broker 检测到连接断开的时间。如果 Broker 的 keepalive 设置是 60 秒,那么最坏情况下设备掉线 60 秒后遗嘱消息才会发布。对实时性要求高的场景,可以适当缩短 keepalive 时间,但会增加心跳包的数量。
4. 持久会话:让离线设备不再错过消息
4.1 会话状态到底保存了什么
MQTT 的持久会话(Persistent Session)解决的是这样一个问题:订阅者离线期间,发布者发布的消息怎么办?
默认情况下(Clean Session = true),每次客户端连接 Broker 都会创建一个全新的会话,之前的订阅关系全部丢失,离线期间的消息也不会保存。客户端重新连接后需要重新订阅,而且只能收到重新订阅之后发布的消息。
持久会话(Clean Session = false)则不同。Broker 会为客户端保存以下状态:
- 客户端的订阅关系,重新连接后自动恢复,不需要重新订阅
- 离线期间收到的 QoS 1 和 QoS 2 消息,等客户端上线后推送
- 未完成的 QoS 1 和 QoS 2 消息传输状态,比如已发送但未确认的消息
这里有一个关键点:只有 QoS 1 和 QoS 2 的消息才会被保存。QoS 0 的消息在客户端离线时会被直接丢弃,因为 QoS 0 本身就不保证送达。
4.2 Clean Session 参数的取舍逻辑
Clean Session 设 true 还是 false,取决于你的业务场景。
设 true 的场景:客户端每次连接都是全新的开始,不需要历史消息。比如一个临时的调试工具,连上去看看当前数据就行,不需要知道之前发生了什么。或者一个每次启动都重新订阅所有主题的客户端,反正订阅逻辑是固定的,重新订阅也不费事。
设 false 的场景:客户端可能频繁断线重连,而且断线期间的消息不能丢。比如一个移动网络下的车载终端,过隧道时信号中断,出来后需要补收中断期间的控制指令。或者一个偶尔休眠的传感器,醒来后需要收到休眠期间下发的配置更新。
这里有一个容易踩的坑:持久会话会占用 Broker 的存储资源。如果大量客户端都设了 Clean Session = false,而且长期不连接,Broker 上会积累大量未投递的消息和会话状态。所以设持久会话的同时,要关注 Broker 的会话过期配置。MQTT 5.0 引入了 Session Expiry Interval 参数,可以设置会话在客户端断开后多久过期,超时自动清理。MQTT 3.1.1 没有这个参数,需要 Broker 端配置或手动清理。
4.3 持久会话与消息堆积的实战处理
我在一个远程抄表项目里遇到过持久会话导致的消息堆积问题。项目里有几千个电表,每个电表每小时上报一次数据,用的是 QoS 1 + 持久会话。正常情况下没问题,但有一次 Broker 升级维护停了两个小时,恢复后所有电表同时重连,Broker 需要把积压的消息全部推送给订阅端,瞬间压力巨大,导致部分消息投递超时。
后来我们做了几个优化。第一,把数据上报的 QoS 从 1 降到 0,因为抄表数据丢一两个点可以接受,下次上报会带上累计值。第二,对确实需要持久会话的客户端,设置了合理的会话过期时间,避免长期不连接的客户端占用资源。第三,在 Broker 端限制了单个客户端的最大积压消息数,超过阈值就丢弃最旧的消息,防止无限堆积。
这些调整之后,系统稳定了很多。持久会话是个好功能,但要用在合适的地方,并且配合合理的清理策略。
5. 从零搭建 MQTT 环境与客户端实操
5.1 Broker 选型与搭建
自己搭建 MQTT 环境,第一步是选 Broker。目前主流的开源 Broker 有几个选择,各有侧重。
Mosquitto是最轻量的选择,安装包小,配置简单,适合快速搭建测试环境和小规模生产环境。它的缺点是集群能力弱,单机性能有上限。
EMQX是国内用得比较多的选择,支持大规模集群,有 Web 管理界面,MQTT 5.0 支持完善。功能全但相对重一些,适合中大规模部署。
RabbitMQ通过插件支持 MQTT,如果你已经在用 RabbitMQ 做消息队列,可以顺便把 MQTT 也接进来,不用额外维护一套 Broker。但它的 MQTT 支持不如专用 Broker 那么原生。
用 Docker 起一个 Mosquitto 是最快的验证方式:
docker run -d --name mosquitto \ -p 1883:1883 \ -p 9001:9001 \ -v /path/to/mosquitto.conf:/mosquitto/config/mosquitto.conf \ eclipse-mosquitto配置文件里至少要设置监听端口和是否允许匿名连接:
listener 1883 allow_anonymous true listener 9001 protocol websockets生产环境一定要把allow_anonymous设为 false,并配置用户名密码或证书认证。我见过不少因为 Broker 匿名开放导致的安全问题,这个口子不能开。
5.2 客户端工具的选择与使用
调试 MQTT 少不了客户端工具。MQTTX是我用得最多的图形化客户端,跨平台,支持 MQTT 5.0,界面清爽,订阅和发布在同一个窗口里就能完成,调试起来很顺手。下载安装后新建连接,填 Broker 地址和端口,点连接就能用。
命令行工具mosquitto_pub和mosquitto_sub适合脚本化操作和快速验证:
# 订阅主题 mosquitto_sub -h broker.example.com -t "sensors/+/temperature" -q 1 -v # 发布消息 mosquitto_pub -h broker.example.com -t "sensors/room1/temperature" -m "25.6" -q 1-v参数会同时打印主题名和消息内容,调试多主题订阅时很有用。-q指定 QoS 等级。
如果你用 JMeter 做压力测试,需要下载 MQTT 插件。JMeter 本身不带 MQTT 支持,装好插件后可以创建 MQTT Connect、MQTT Pub、MQTT Sub 等 Sampler,模拟大量客户端并发连接和收发消息,用来评估 Broker 的承载能力。
5.3 代码实战:Python 客户端完整示例
下面是一个完整的 Python MQTT 客户端示例,包含了连接、遗嘱消息设置、订阅、发布和持久会话的配置:
import paho.mqtt.client as mqtt import time import json BROKER = "broker.example.com" PORT = 1883 CLIENT_ID = "device_sensor_001" def on_connect(client, userdata, flags, rc): if rc == 0: print("连接成功") client.subscribe("commands/device_sensor_001/#", qos=1) else: print(f"连接失败,返回码:{rc}") def on_message(client, userdata, msg): print(f"收到消息 - 主题:{msg.topic},内容:{msg.payload.decode()},QoS:{msg.qos}") if msg.topic == "commands/device_sensor_001/reboot": print("执行重启操作...") def on_disconnect(client, userdata, rc): print(f"断开连接,返回码:{rc}") if rc != 0: print("异常断开,尝试重连...") client = mqtt.Client(client_id=CLIENT_ID, clean_session=False) client.username_pw_set("username", "password") client.will_set( topic=f"devices/{CLIENT_ID}/status", payload=json.dumps({"status": "offline", "ts": int(time.time())}), qos=1, retain=True ) client.on_connect = on_connect client.on_message = on_message client.on_disconnect = on_disconnect client.connect(BROKER, PORT, keepalive=60) client.loop_start() client.publish( topic=f"devices/{CLIENT_ID}/status", payload=json.dumps({"status": "online", "ts": int(time.time())}), qos=1, retain=True ) try: while True: temperature = 25.0 client.publish( topic=f"sensors/{CLIENT_ID}/temperature", payload=json.dumps({"value": temperature, "ts": int(time.time())}), qos=0 ) time.sleep(5) except KeyboardInterrupt: client.publish( topic=f"devices/{CLIENT_ID}/status", payload=json.dumps({"status": "offline", "ts": int(time.time())}), qos=1, retain=True ) client.disconnect() client.loop_stop()这段代码里有几个值得注意的点。clean_session=False开启了持久会话,客户端断线重连后订阅关系自动恢复。遗嘱消息设置了 retain,设备异常掉线后监控端能立即感知。正常退出时主动发布离线状态并调用disconnect(),这样 Broker 不会触发遗嘱消息,而是用我们主动发布的离线消息覆盖。loop_start()启动了一个后台线程处理网络循环,主线程可以继续做其他事情。
5.4 嵌入式设备接入的注意事项
在 STM32 这类资源受限的设备上跑 MQTT,和 PC 端有几个明显的差异。
内存管理要格外小心。MQTT 客户端库需要缓冲区来存放待发送和待接收的报文,STM32 上 RAM 有限,缓冲区不能设太大。一般 QoS 0 的消息缓冲区设 256 字节到 512 字节就够了,QoS 1 和 QoS 2 因为要保存消息副本用于重发,需要更大一些。
网络稳定性处理要更健壮。嵌入式设备经常遇到网络抖动,TCP 连接可能无声无息地断了。除了 MQTT 层面的 keepalive,建议在应用层再加一层心跳检测,比如每隔 30 秒发布一条心跳消息,如果连续几次发布失败就主动重连。
4G 模块的 AT 指令和 MQTT 库的配合也需要调试。有些 4G 模块自带 MQTT AT 指令,可以直接用模块内置的 MQTT 功能,省去在 MCU 上跑 MQTT 库的开销。但模块内置的 MQTT 功能通常比较基础,QoS 支持和遗嘱消息可能不完整,选型时要确认清楚。
6. 常见问题排查与避坑指南
6.1 连接类问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 连接被拒绝,返回码 1 | 协议版本不匹配 | 确认客户端和 Broker 的 MQTT 版本一致 |
| 连接被拒绝,返回码 4 | 用户名或密码错误 | 检查认证配置 |
| 连接被拒绝,返回码 5 | 未授权 | 检查 Broker 的 ACL 配置 |
| 连接成功但立即断开 | Client ID 冲突 | 确保每个客户端 Client ID 唯一 |
| 频繁断线重连 | Keepalive 设置过短 | 适当增大 keepalive 或优化网络 |
Client ID 冲突这个问题我遇到过好几次。两个客户端用了相同的 Client ID 连接同一个 Broker,Broker 会把先连接的那个踢掉。调试的时候如果发现设备莫名其妙掉线,先检查 Client ID 是不是重复了。生产环境建议用设备唯一标识(如 MAC 地址、IMEI)作为 Client ID。
6.2 消息收发异常排查
消息发出去了但订阅端收不到,排查思路按顺序来:先确认订阅端订阅的主题和发布端发布的主题是否完全匹配,包括大小写和斜杠。再确认 QoS 等级是否兼容,QoS 0 的消息在订阅端离线时不会保存。然后检查 Broker 的 ACL 是否限制了主题的发布或订阅权限。最后看 Broker 的日志,通常会有消息路由的记录。
消息重复收到,大概率是 QoS 1 的重发机制导致的。如果业务不能容忍重复,要么升级到 QoS 2,要么在应用层做去重,比如每条消息带一个唯一 ID,接收端记录已处理的 ID,重复的直接丢弃。
消息顺序错乱在 MQTT 里是可能发生的,尤其是 QoS 1 和 QoS 2 的消息在重发时。MQTT 协议不保证跨主题的消息顺序,同一主题同一 QoS 的消息在正常情况下是有序的,但重发可能打乱顺序。如果业务对顺序敏感,需要在消息里带序列号,接收端自己排序。
6.3 性能优化实战经验
Broker 性能优化有几个方向。连接数优化:每个 MQTT 连接都会占用 Broker 的文件描述符和内存,连接数上万时需要调整系统的文件描述符限制和 Broker 的最大连接数配置。消息吞吐优化:减少不必要的 QoS 1 和 QoS 2 消息,能降级到 QoS 0 的就降级。主题设计优化:避免过多的通配符订阅,尤其是#这种全匹配,会增加 Broker 的路由计算量。
客户端侧的性能优化主要是减少重连风暴。Broker 重启后,大量客户端同时重连会造成惊群效应。解决办法是在客户端重连逻辑里加随机退避,比如第一次断线后等 1 到 3 秒再重连,第二次等 3 到 8 秒,依次递增,避免所有客户端在同一时刻发起连接。
实操心得:在客户端重连逻辑里加指数退避加随机抖动,能有效缓解 Broker 重启后的连接风暴。具体做法是重连等待时间 = 基础时间 × 2^重试次数 + 随机毫秒数,设置一个上限比如 60 秒。
6.4 安全配置要点
生产环境的 MQTT 部署,安全配置不能省。最基本的几条:禁用匿名连接,为每个客户端分配独立的用户名密码;启用 TLS 加密,防止消息在传输过程中被窃听;配置 ACL,限制每个客户端只能发布和订阅自己权限范围内的主题。
TLS 配置在 Mosquitto 里需要指定证书文件:
listener 8883 cafile /path/to/ca.crt certfile /path/to/server.crt keyfile /path/to/server.key require_certificate false客户端连接时需要用tls_set()方法加载 CA 证书。如果用了自签名证书,客户端需要把 CA 证书文件带上,否则会报证书验证失败。
ACL 配置可以精确到用户和主题级别:
user sensor1 topic readwrite sensors/sensor1/# topic read commands/sensor1/# user monitor topic read sensors/#这样 sensor1 只能读写自己的主题,monitor 只能读所有传感器数据但不能发布。权限最小化原则在 MQTT 部署里同样适用。
7. 与其他协议的对比与选型参考
7.1 MQTT vs gRPC
gRPC 和 MQTT 经常被放在一起比较,但它们的设计目标完全不同。gRPC 基于 HTTP/2,主打高性能的远程过程调用,适合服务间的同步通信,比如微服务架构里服务 A 调用服务 B 的接口。MQTT 主打轻量级的异步消息传递,适合设备到云端的通信。
选型逻辑很简单:如果你的场景是"客户端发一个请求,服务端返回一个结果",用 gRPC。如果是"设备持续上报数据,多个消费方按需订阅",用 MQTT。两者也可以共存,比如设备用 MQTT 上报数据,后端服务之间用 gRPC 通信。
7.2 MQTT 在 ROS2 中的 QoS 实践
ROS2 默认的通信中间件是 DDS,但 ROS2 也支持通过桥接的方式接入 MQTT。ROS2 的 QoS 配置和 MQTT 的 QoS 概念有相似之处但不等价。ROS2 的 QoS 包括 Reliability(Reliable/Best Effort)、Durability(Transient Local/Volatile)、History(Keep Last/Keep All)等多个维度,比 MQTT 的三级 QoS 更细粒度。
在 ROS2 和 MQTT 桥接的场景里,需要做 QoS 映射。比如 ROS2 的 Reliable + Volatile 通常映射到 MQTT 的 QoS 1,Best Effort + Volatile 映射到 QoS 0。Transient Local 的 Durability 对应 MQTT 的 retain 消息。这个映射不是一对一的,需要根据具体业务需求调整。
7.3 工业场景中的协议对接
工业环境里常见的协议是 OPC UA,Kepware 这类 OPC Server 能不能对接 MQTT?答案是能,但通常需要中间件。Kepware 本身支持 MQTT 作为输出通道,可以把采集到的 OPC 标签数据转发到 MQTT 主题上。配置方式是在 Kepware 里添加 MQTT Agent,设置 Broker 地址和主题映射规则。
MCGS 这类组态软件对接 MQTT 通常通过脚本或驱动实现。有些版本的 MCGS 内置了 MQTT 驱动,直接配置就行;没有内置的可以通过 Lua 脚本或外部程序做协议转换。AEP 平台这类物联网平台通常提供标准的 MQTT 接入接口,设备按照平台规定的主题格式和消息格式接入即可。
MATLAB 也有 MQTT 支持,通过mqtt函数可以创建客户端对象,适合做算法验证和数据分析时的快速原型开发。Android 端的 MQTT 开发一般用 Eclipse Paho Android Service 库,封装了连接管理和消息收发,用起来比较方便。
Spring Boot 集成 MQTT 通常用 Eclipse Paho 的 Java 客户端或者 Spring Integration MQTT。Spring Integration 的方式更符合 Spring 的编程模型,通过消息通道和适配器配置,把 MQTT 消息接入 Spring 的消息流里。RabbitMQ 开启 MQTT 插件后可以同时处理 AMQP 和 MQTT 消息,适合已经在用 RabbitMQ 的团队快速接入 MQTT 设备。
MQTT 虚拟串口软件这个需求比较特殊,通常是为了让原本走串口通信的上位机软件能通过 MQTT 传输数据。实现方式一般是写一个虚拟串口驱动,把串口数据转发到 MQTT 主题,或者反过来把 MQTT 消息写入虚拟串口。这类工具在工业现场改造中比较常见,用来把老设备接入新的物联网平台。
选型这件事没有标准答案,关键是把业务需求拆清楚:通信是同步还是异步、消息可靠性要求多高、设备资源有多紧张、团队对哪个协议更熟悉。把这些想明白了,选哪个协议自然就清楚了。MQTT 不是万能的,但在设备通信这个领域,它确实是最趁手的工具之一。