ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MQTT协议实战:发布订阅模型、QoS等级与遗嘱消息全解析

MQTT协议实战:发布订阅模型、QoS等级与遗嘱消息全解析 1. 先把发布订阅模型吃透MQTT 不是你问我答1.1 为什么 HTTP 式思维会让你绕远路我刚开始接触 MQTT 的时候也犯过一个典型的错误习惯性地用 HTTP 那套客户端发起请求、服务端返回响应的思路去理解它。后来发现这种思维完全带偏了方向。MQTT 的核心是发布订阅模型不是请求响应模型。这两者的本质差别在哪你可以把 Broker消息代理服务器当成一个邮局客户端不需要知道对方在哪、是否在线只要把信投到邮局邮局再按订阅关系把信送给感兴趣的人。消息的发布者和消费者之间完全解耦发布者不关心谁会读这条消息订阅者也不需要知道消息从哪来。打个更直白的比方你在小区业主群里发了一条今晚停水你不会挨个私聊每一户群里所有关注这条消息的人自然能看到。MQTT 就是这个群Broker 就是群的服务器Topic就是群名。这种模式在物联网场景里有多重要随便举几个例子一个温湿度传感器每分钟上报一次数据可能有 8 个后端服务都在订阅这份数据做不同处理存储、告警、可视化。一个控制指令比如打开空调只能发给指定的那台设备不能广播给整个网络。移动设备网络不稳定断线后再次上线希望把离线期间漏掉的消息补回来。这些场景用 HTTP 也能实现但要么轮询代价太高要么逻辑极其别扭。MQTT 用一套简单的发布订阅机制就把它们全部覆盖了这也是它在物联网领域通吃十来年的根本原因。1.2 Topic 的层级设计与通配符先把地址系统搭对Topic 是 MQTT 消息的路由地址长得像文件路径sensor/warehouse/room1/temperature device/2312/command gateway/58:7a:62:11:22:33/status层级用斜杠 / 分隔每一级代表一个维度的分类。设计 Topic 时有两个通配符特别关键单层通配符匹配一层任意值。订阅sensor//room1/temperature可以收到sensor/warehouse/room1/temperature和sensor/office/room1/temperature但收不到sensor/warehouse/room2/temperature。#多层通配符匹配剩余的所有层级。订阅device/2312/#可以收到device/2312/command、device/2312/status、device/2312/config/update等所有子级消息。我在实际项目中踩过一个坑订阅端开头的$系统主题。以$SYS/开头的主题是 Broker 自己发布的服务端信息客户端数量、消息流量等普通通配符订阅是收不到$SYS开头的主题的因为规则里#和不能匹配以$开头的层级。这不是 bug是协议层面对系统主题的刻意保护。如果你订阅#却一直收不到负载数据先检查 Broker 是不是把消息发布到了$SYS下面。Topic 命名还有几条实践中总结出来的经验不要以斜杠开头/sensor/temp和sensor/temp在 MQTT 协议里是不同的主题前者带一个空层级很容易造成订阅时匹配不上的诡异问题。层级不要过深3 到 5 层是一个比较合理的深度层数过多会增大 Broker 的路由匹配开销也增加维护成本。区分大小写Sensor/Temp和sensor/temp是两个完全不同的主题团队内部要约定统一用小写字母加下划线。把设备唯一标识放中间层比如device/{deviceId}/telemetry这样既方便按设备维度做权限控制也方便用通配符批量订阅。1.3 Broker 在体系里到底扮演什么角色很多人以为 Broker 只是一个消息中转站把消息从 A 搬到 B。真去研究 Broker 内部机制就会发现它的职责远不止转发维护订阅关系每个客户端连接上来之后它订阅了哪些 TopicBroker 要用一张订阅树来维护消息进来时快速匹配所有相关订阅者。处理连接生命周期客户端的连接、心跳、断线、重连Broker 都要有对应的状态管理。QoS 状态机管理QoS 1 和 QoS 2 的会话状态、消息重发、去重都是 Broker 端的核心工作。权限控制谁可以发布到哪个 Topic、谁可以订阅哪个 Topic需要在 Broker 层面做 ACL访问控制列表。消息存储与转发持久会话的离线消息、保留消息、遗嘱消息都要由 Broker 代为管理。用一句话概括Broker 是整个 MQTT 体系的调度中心客户端的实现相对简化复杂度集中到了 Broker 端。所以在选型时Broker 的稳定性、并发能力和扩展性往往决定了整个系统的上限。2. QoS 等级0、1、2 背后的取舍逻辑与重传细节2.1 三档等级的工作过程拆解QoSQuality of Service服务质量是 MQTT 里最容易被背下来但不理解的概念。三个等级对应的是消息投递的三档可靠程度QoS 0最多一次At most once发出去就不管了不等待确认不重传。消息可能到达也可能在网络抖动中丢失。开销最低吞吐量最高。适合传感器周期性上报、日志采集这类偶尔丢一条也没关系的数据。你可以把它理解成发一条朋友圈发完就完事了不关心别人到底看没看到。QoS 1至少一次At least once发送方把消息发出去后必须等接收方回一个PUBACK发布确认包。如果没收到发送方就重发这条消息。这种方式保证消息一定能到达但同一个消息可能被接收方收到多次——因为PUBACK可能在路上丢了发送方重发接收方就会收到两条一样的消息。QoS 2恰好一次Exactly onceMQTT 协议里最复杂的流程核心目的就是去重。整个过程分两个阶段涉及四个报文发送方发PUBLISH。接收方回PUBREC收到记录。发送方再发PUBREL释放表示我已经知道你收到了现在把消息交给上层应用吧。接收方回PUBCOMP完成。整个握手过程配合报文标识符Packet ID两边的状态机共同保证无论网络怎么抖动、重发多少次上层应用只会收到一条消息。开销最大吞吐量最低适合资金交易、设备控制指令这类绝对不能丢也不能重复的场景。需要注意的是QoS 是端到端的但不能简单理解成发布端指定多少就是多少。这一条放到后面QoS 降级里详细说。2.2 真实场景里 QoS 怎么选一张表说清楚在真实项目里我见过很多人在 QoS 上乱选要么一律 QoS 0要么一律 QoS 2。其实最合适的选法取决于这条数据丢了/重复了会有什么后果。场景推荐 QoS理由温度、湿度、电量等周期遥测QoS 0 或 QoS 1下一条数据马上就会来偶尔丢一条影响不大QoS 1 更稳但吞吐略下降设备状态变更上线/离线QoS 1丢一条状态信息可能导致误判重复收到状态变更还能接受控制指令开灯、锁门、下发配置QoS 1 或 QoS 2指令不能丢如果重复执行会导致严重后果比如重复扣费必须 QoS 2OTA 升级指令或缺包重传请求QoS 1需要可靠到达重复请求通常幂等调试日志、实时数据展示QoS 0延迟优先允许丢数据画个曲线图掉几个点看不出来选 QoS 的一个基本原则能用 QoS 1 就不上 QoS 2。因为 QoS 2 的状态机复杂对 Broker 和服务端程序的开发难度、存储开销影响很大除非业务真的不能容忍重复否则 QoS 1 加业务侧幂等处理往往是性价比更高的方案。我自己做网关程序的时候指令下发默认 QoS 1消息里带一个requestId请求唯一标识接收方根据requestId做去重。这样既保证了可靠性又把复杂度留在业务层比直接上 QoS 2 好维护得多。2.3 关于 QoS 降级和数据包重发机制这里有个关键机制很多人学完第一遍都会搞混消息最终使用的 QoS 等级 min(发布端发布的 QoS, 订阅端订阅时的 QoS)。举个例子发布端用 QoS 2 发布了一条消息但如果订阅端是用 QoS 0 订阅的那这条消息传给订阅者时就是 QoS 0。为什么因为订阅端根本没打算处理PUBACK这些确认包它只按 QoS 0 的接收逻辑去收消息。Broker 会根据每个订阅者的 QoS 需求分别复制和转发消息不同订阅者收到的 QoS 等级可以不同。还有一点容易被忽略QoS 1 和 QoS 2 的消息重发需要配合会话状态。Broker 相当于一个有记忆的邮局它会保存未确认的消息和报文标识符状态。如果客户端断线重连时用的是全新会话Clean Session 为 trueBroker 就把之前的状态全清了那些没发完的消息也不会再补发。所以如果你想保证消息在发布端发出去、订阅端掉线了这种场景下不丢失只靠 QoS 1/2 是不行的还必须配合持久会话Clean Session 为 false使用。这也是很多人在线上环境踩坑的根源总以为 QoS 1 就高枕无忧了结果订阅端一重启离线期间的消息全没了——因为默认的 Clean Session 把会话和队列都清掉了。3. 遗嘱消息与保留消息让掉线这件事可感知3.1 遗嘱消息原理Broker 替你发的最后一条消息遗嘱消息Last Will and Testament也叫 LWT是 MQTT 里一个特别贴心的特性。它的设计意图是当设备异常掉线时让其他设备或服务能立刻感知到这一事件。毕竟在物联网场景里设备掉线往往是故障的第一步越早发现越好。遗嘱的使用方式很简单客户端在连接 Broker 时在CONNECT报文里带上遗嘱相关的字段包括遗嘱主题Will Topic遗嘱消息要发布到哪个 Topic。遗嘱内容Will Payload遗嘱消息的实际内容比如offline。遗嘱 QoSWill QoS发布遗嘱消息时的 QoS 等级。遗嘱保留标志Will Retain遗嘱消息是否作为保留消息发布。客户端连接建立完后Broker 会把这套遗嘱记在账上。正常情况下客户端不会触发遗嘱但一旦出现下列情况Broker 就会主动替这个客户端发布遗嘱消息网络异常断开Broker 在规定的 Keep Alive 时间内没收到客户端的任何报文。客户端发送了违反协议格式的报文Broker 主动断开连接。客户端的 TCP 连接异常复位。注意一个关键点如果客户端正常发送DISCONNECT报文后优雅断开Broker 是不会发遗嘱的。因为这是我主动下线了不是我异常死掉了。这个区分很重要——如果你在写业务逻辑时要用遗嘱判断设备在线状态必须保证客户端在正常重启、升级时先发DISCONNECT否则会出现设备明明在正常重启告警却刷了一屏的尴尬情况。还有一个新特性值得提一下MQTT 5.0 里遗嘱和会话过期时间Session Expiry Interval配合使用时会有一个延迟遗嘱的机制——客户端可以设置遗嘱延迟Will Delay Interval让 Broker 在断线后等一段时间再发遗嘱。只有在这段时间内没有重连才把遗嘱发出去。这个特性可以有效避免网络抖动导致的误告警。3.2 保留消息不是遗嘱但经常和遗嘱搭着用保留消息Retained Message是另一个容易跟遗嘱弄混的概念。它的作用是Broker 为每个 Topic 保留最新一条消息的内容新的订阅者一订阅立刻收到保留的消息而不是等下一次发布才有数据。举个典型场景一个门锁设备的状态是online或offline它上线后往device/123/status发布一条online的保留消息。这样哪怕后端的订阅程序比设备启动得晚也能在订阅的第一时间拿到当前状态不需要等设备下一次上报。保留消息和遗嘱消息搭在一起用非常经典设备连接时设置遗嘱Will Topic device/123/statusWill Payload offlineWill Retain true。设备正常上线后发布一条保留消息device/123/status online。订阅方订阅device/123/status立即收到online。设备异常掉线Broker 发布遗嘱device/123/status offline且因为设置了 Retain这条 offline 继续作为最新的保留消息存着。订阅方收到offline后触发告警即使告警程序此时才启动订阅后看到的也是offline状态。这个组合能非常优雅地实现物联网设备的状态同步。有一个坑必须提醒清除保留消息需要发布一条空的保留消息。也就是说向同一个 Topic 发布一条Payload为空、Retain为 true 的消息Broker 才会把该 Topic 对应的保留消息删除掉。如果你只是想让某个 Topic 不再有保留消息千万别发个字符串那是删除不了它的。MQTT 里空 Payload 和 Payload 为空字符串是两种完全不同的东西。3.3 关键参数背后的计算逻辑Keep Alive 与心跳遗嘱触发条件里提到了 Keep Alive它是 MQTT 连接机制里另一个值得展开的参数。客户端在CONNECT报文里上报自己期望的 Keep Alive 时间单位是秒表示如果我在这个时间内没有任何报文发给你你可以认为我挂了。但这里必须深入一层Keep Alive 的检测不是让客户端每 N 秒发一个心跳而是只要在这段时间内有任意报文产生即可。如果业务数据比较稀疏客户端需要主动发一个PINGREQ心跳报文来维持连接如果业务数据本身就足够频繁心跳可以省掉。Broker 端实际判断超时的算法更宽松一点1.5 倍 Keep Alive 时间内如果没收到客户端的任何报文才判定连接超时。这是为了应对网络抖动给客户端留一点缓冲期。所以在设置 Keep Alive 时不要只看 Broker 文档说的1.5 倍客户端自身的重连配置也要配合好。比如 Keep Alive 设 60 秒那么客户端最好在 45 秒左右就开始发心跳或业务报文重连间隔可以设成 5 秒一次重连失败退避到 30 秒这样一个循环下来才不会触发误判。4. 从 Broker 选型到客户端接入一套能直接落地的接线方案4.1 Broker 选型对比根据自己的规模对号入座很多人学完协议概念后卡在了第一步到底用什么 Broker其实主流的开源和商业方案就那么几个核心差异在并发规模、运维成本和功能丰富度上。方案协议支持适合规模特点MosquittoMQTT 3.1/3.1.1/5.0小规模、原型验证、嵌入式设备轻量资源占用极低适合树莓派、边缘网关插件机制偏弱集群能力有限EMQXMQTT 3.1/3.1.1/5.0大规模生产环境基于 Erlang/OTP百万级连接是它的强项自带规则引擎、数据集成、Dashboard有开源版NanoMQMQTT 3.1.1/5.0边缘计算、嵌入式轻量高性能适合部署在边缘节点VerneMQMQTT 3.1/3.1.1/5.0中大规模同样基于 Erlang集群能力强但社区活跃度和插件生态不如 EMQX阿里云/腾讯云等 IoT 平台MQTT 3.1.1/5.0 定制大型商业项目省去自建运维成本自动带设备影子、规则引擎、数据存储等配套设备接入认证体系也完善我的建议是先跑通逻辑用 Mosquitto,正式做项目直接上 EMQX。不是说 Mosquitto 不稳定而是后续一旦需要设备管理、消息流转、规则处理Mosquitto 那套纯转发逻辑会明显不够用迁移成本远高于一开始就选对。另外提醒一句很多云平台默认就用 8883 端口做 TLS 加密接入如果你用tcp://去连会被直接拒绝。现在物联网安全要求越来越严从最开始开发就要把 TLS 接入跑通不要图省事用明文1883上生产。4.2 客户端接入的通用流程与调试利器无论你用哪门语言接入 MQTT流程都是同一个模子建立 TCP/TLS 连接。发送CONNECT报文携带 ClientID、用户名密码、Keep Alive、Clean Session、遗嘱等参数。收到 Broker 返回的CONNACK报文。发送SUBSCRIBE订阅相关 Topic。收发PUBLISH消息。定期心跳异常时重连。调试阶段强烈建议先装一个MQTTX来验证连接参数和主题设计。它是图形化客户端支持多连接、自定义 Payload 格式还能直接模拟遗嘱消息。我之前调一个设备反复掉线的 bug就是先在 MQTTX 里设了遗嘱和 Keep Alive观察它在各种断网操作下的表现才定位到是客户端底层 TCP 长时间空闲被运营商掐了连接而不是 Broker 配置问题。连接参数里有几个容易搞错的点ClientID 必须唯一。两个客户端用同一个 ClientID 连接同一个 Broker前一个会被顶掉线这是协议规定的会话接管机制。很多人把 ClientID 写死成同一个导致设备之间互相踢线。用户名和密码是可选的。MQTT 协议本身不强制认证但不强制不代表你可以不配。上生产环境必须通过 Broker 的认证插件或云平台的认证体系来保护接入。Clean Session 的语义。MQTT 3.1.1 里 Clean Session 为 true 表示每次都开新会话false 表示恢复持久会话。到了 MQTT 5.0这个概念被拆成两个独立的参数Clean Start和Session Expiry Interval。写代码时如果用的是 3.1.1 兼容库先把 Clean Session 真/假的语义吃透再动手。4.3 代码接入的三种典型姿势我简单展示三种最常见的接入姿势覆盖后端、前端/桌面端和嵌入式设备三个方向。后端Node.js mqtt 包const mqtt require(mqtt); const client mqtt.connect(mqtt://broker.example.com:1883, { clientId: backend-service-01, username: iot_user, password: iot_password, keepalive: 60, clean: true, will: { topic: service/backend-01/status, payload: down, qos: 1, retain: true, }, }); client.on(connect, () { console.log(connected); client.subscribe(device//telemetry, { qos: 1 }); client.publish(service/backend-01/status, up, { qos: 1, retain: true }); }); client.on(message, (topic, payload) { const data JSON.parse(payload.toString()); // 处理业务逻辑 });前端Vue 3 MQTT浏览器环境不能直接用原生 TCP要用 MQTT.js 的 WebSocket 协议接入 BrokerEMQX 默认开了ws://和wss://端口import mqtt from mqtt; const client mqtt.connect(ws://broker.example.com:8083/mqtt, { clientId: web- Date.now(), username: web_user, password: web_password, });项目里用 Vue 3 MQTT 做实时数据大屏的很多订阅主题后把数据直接推到ref里驱动图表更新比原来的轮询方案实时性高了一个量级服务器压力也小很多。嵌入式STM32 AT 指令控制 4G 模块嵌入式场景常见的是 STM32 通过串口给 4G 模块发 AT 指令让模块直接跟 MQTT Broker 建立连接。以常见的 EC200/ML307 等模组为例流程大致是ATQMTOPEN0,broker.example.com,1883 ATQMTCONN0,device_001,username,password ATQMTSUB0,1,device/001/command,1 ATQMTPUB0,0,1,0,device/001/telemetry,{\temp\:25.6}注意这条发布命令的参数含义ATQMTPUBtcp连接id, 消息id, QoS, retain, topic, payload。不同模组的 AT 指令集有差异但思路一致。很多人在这一步把 QoS 和 retain 的位置搞反导致发布消息时参数错位。我遇到过不止一次明明代码看起来没错消息就是发不出去结果问题出在 AT 指令的 QoS 参数传成了字符串格式。Node-RED 也经常被用来做 OPC UA 转 MQTT 的桥接Node-RED 装一个node-red-contrib-opcua-server或客户端节点读取 OPC UA 数据点用 MQTT Out 节点发布到 Broker。它的消息格式可以自定义调试起来比纯代码直观太多了非常适合做工业协议到 IoT 平台的对接层。5. 几个一知半解时特别容易踩的协议细节5.1 连接参数与会话恢复时的假在线做设备状态管理时最常见的问题是设备在线状态不准。原因不一定是 MQTT 协议的 bug而往往是没有把连接存在和设备在线区分开。MQTT 里设备连上 Broker只代表连接层面的在线。但如果设备端逻辑卡死、网络黑洞TCP 连接看起来还在实际数据已经发不出去Broker 只能靠 Keep Alive 超时来被动检测。Keep Alive 设得太长异常掉线要很久才能被发现设得太短网络稍微抖一下就会误判离线。实际项目里我通常这样配置设备端 Keep Alive 设 30 到 60 秒。心跳报文按 Keep Alive 的一半间隔发送留足余量。设备端重连采用指数退避策略第一次 1 秒、第二次 2 秒、第三次 4 秒……最大到 60 秒封顶。业务层的在线判断不依赖 Broker 连接状态而是依据设备最近一次心跳上报给应用层的时间戳超过 3 个心跳周期没有上报就判定失联。这套组合拳下来既不会因为网络抖动误报又能在设备真正掉线时快速感知。5.2 保留消息在系统重启后的行为还有一个我见过非常多团队搞错的地方持久会话和保留消息的过期时机。有些团队用保留消息保存设备最新状态觉得只要 Broker 不重启数据就一直在。但保留了消息的 Topic 如果再收到一条新的保留消息会覆盖旧的如果收到空 Payload 的保留消息则会被删除。也就是说如果你的设备是上线时订阅 Topic A 的保留消息想获取最新状态但这个 Topic 的保留消息在设备掉线期间被别的服务用空负载清掉了那么设备重新上线后订阅这个 Topic什么都不会收到。这是个很经典的状态丢失场景排查起来往往要查很久。5.3 MQTT 5.0 里值得关注的新特性如果你用的是 EMQX 这类支持 MQTT 5.0 的 Broker有四个特性在工作中非常实用原因码Reason Code以前 Broker 断开客户端的连接客户端只能看到断开了这个事实现在 Broker 会告诉客户端具体原因比如用户名密码错误、Topic 权限不足、会话被接管等。排查问题效率高了一倍。用户属性User Properties可以在 MQTT 报文里自定义键值对类似 HTTP 的自定义 Header。比如给消息打上网关 ID、消息版本号、业务流水号非常实用。主题别名Topic Alias短报文场景下每次发布都带完整 Topic 通信开销不小。Topic Alias 允许客户端跟 Broker 先约定一个数字别名后续报文用别名代替长长的 Topic能显著降低高频小消息的流量。请求响应Request/ResponseMQTT 5.0 增加了请求响应的消息关联机制很多人误以为这是把 MQTT 变成 HTTP其实不是——它只是给发布订阅模型加了一个消息关联的语法糖数据流还是异步的不会破坏 MQTT 的解耦模型。5.4 排错顺序从 Wireshark 看握手说起最后分享一个我排查 MQTT 问题时的通用思路。无论报什么错先抓包看四个报文CONNECT - CONNACK - SUBSCRIBE - SUBACK。几乎 80% 的连接问题都能在这个阶段看出来如果CONNACK没回来先查 TCP 连接是否通、端口是否正确、TLS 是否匹配。如果CONNACK返回错误码根据返回码对应查用户名密码、ClientID 格式、协议版本不匹配等问题。MQTT 5.0 的 CONNACK 原因码信息更丰富比如0x86表示 ClientID 不合法0x87表示用户被禁止接入。如果SUBSCRIBE发出去没回SUBACK多数是权限控制把订阅请求拒绝了。如果SUBACK返回了非 0 的返回码说明某个 Topic 订阅失败对照协议文档查具体原因。Wireshark 自带 MQTT 协议解析器可以把报文解析成可读的字段列表我在调试 QoS 2 握手流程时就是靠它把PUBREC/PUBREL/PUBCOMP的状态流转看明白的。纸上谈兵背十遍状态机不如抓一次包看得透彻。我做 MQTT 相关项目这几年最大的体会是这个协议的入门门槛很低读一篇文章就能连上 Broker 收发消息但真正把它用对、用稳靠的是对发布订阅模型、QoS 语义、会话机制和遗嘱/保留消息这些细节的深入理解。上面提到的很多坑都是我在实际项目中一个个踩出来的希望你能绕过去。如果你正在折腾 MQTT 相关的东西先把 Broker 跑起来、用 Wireshark 抓包看一下握手流程再回来读这些细节收获会完全不同。
返回列表