MQTT

本页目录

MQTT 用发布/订阅方式传送应用消息:设备向 Broker 的 topic 发布,Broker 根据订阅过滤器向客户端分发。传输依赖可靠、有序、双向连接,常见为 TCP 或 WebSocket;它并不是直接在任意不可靠 UDP 上运行的同一套报文。

以温度传感器为例,周期读数可以容忍偶尔丢一条,“打开阀门”却需要确认是否执行。MQTT QoS 管理协议消息交付,业务命令还需要命令 ID、有效期与执行应答。两者应分别设计。MQTT 5.0

查阅点关键边界
Topic 与 Filter发布到具体名称,订阅按过滤器匹配
QoS 2协议接收状态不等于数据库原子事务
Packet Identifier有作用域、可复用,不是全局事件 ID
Retained当前保留值,不是完整历史日志
Session跨连接保留的订阅与交付状态
Will异常结束的通知机制,不是即时在线真相

Topic:名称、过滤器与权限分别处理

Topic Name 区分大小写,/ 划分层级;前后斜杠和空层都具有意义。PUBLISH 不使用 +、# 通配符;订阅 Filter 中 + 匹配一个层级,# 必须位于末尾并覆盖零个或多个层级。普通首层通配符不匹配以 $ 开头的系统主题。

FilterTopic匹配
home/+/temphome/bedroom/temp是
home/+/temphome/temp否,少一个层级
home/#home是,# 可以覆盖零层
home/+home/是,末尾是空层
#$SYS/broker/uptime否,需要显式订阅系统前缀

命名应同时考虑路由与授权。例如 tenant/a/device/17/state 可以明确租户与设备范围,但客户端自己声明这个字符串并不能证明它属于租户 a;Broker 的认证与 ACL 需要约束可发布和可订阅的范围。Mosquitto 访问控制配置

QoS:每一跳管理自己的包状态

发布端确认与订阅端确认,是两段独立的交付状态 设备 发布者 Broker 接收并按订阅分发 业务应用 订阅者 PUBLISH / ID 7 PUBACK PUBLISH / ID 21 PUBACK 同一应用消息可以有不同 Packet Identifier;发布者收到确认不表示数据库副作用已完成。

发布者到 Broker 和 Broker 到订阅者各自协商/执行交付。出站 QoS 受原始发布 QoS、订阅获准上限与服务器能力约束;不能因为源端用 QoS 2,就推断每个订阅者都得到 QoS 2。

级别基本交换所管理的性质
QoS 0PUBLISH无 MQTT 层确认,可能丢失
QoS 1PUBLISH、PUBACK至少一次交付流程,可能重复
QoS 2PUBLISH、PUBREC、PUBREL、PUBCOMP在相应协议会话状态内避免重复交付

QoS 2 不要求业务数据库与 MQTT 状态共享一个事务。应用已改变库存却在记录消费结果前崩溃,恢复后如何处理仍取决于客户端、应用提交边界和消息去重。对设备命令,应将事件 ID 与执行结果可靠关联;Packet Identifier 在完成交换后可复用,不能作为永久去重键。

MQTT 5 中,带拒绝原因的 PUBACK/PUBREC 也不能当作正常成功处理。发布端收到 Broker 的成功确认,通常只说明该跳的处理结果,不证明所有订阅者已收到,更不证明阀门已动作。

重连时重发什么

MQTT 5 的协议重发主要针对恢复会话中尚未确认的 QoS 1/2 PUBLISH 与 PUBREL;不应给仍存活的同一连接随意加一个超时定期重发算法。协议级重发 PUBLISH 时 DUP 标记说明可能重复,但 DUP=0 的另一条新发布也可能携带同一业务事件内容。MQTT 5 §4.4

即使网络长期不可达、会话已过期或管理员删除状态,协议也不能保证最终交付。QoS 的语义需要结合状态保留与错误结果,不能用“不重复不丢失”作为不带前提的运营承诺。

报文长度:两字节固定头只是最小情况

MQTT 包首字节包含类型与固定标志,随后 Remaining Length 用一到四字节的变长整数表示其余部分长度;超过 127 时不再只占一个字节。主题字符串长度以字节编码,Packet Identifier 是两字节且只在对应消息类型/QoS 下出现。

下面构造 MQTT 5 的 QoS 1 PUBLISH:topic home/temp,Packet Identifier 7,无属性,payload 23.5。首字节 0x32,Remaining Length 为 18,整包 20 B;尚未计入 TCP/TLS/IP。

import struct

def vbi(n):
    if not 0 <= n <= 268435455:
        raise ValueError('超出 MQTT 变长整数范围')
    result = bytearray()
    while True:
        digit = n % 128
        n //= 128
        result.append(digit | (0x80 if n else 0))
        if not n:
            return bytes(result)

assert vbi(127) == b'\x7f'
assert vbi(128) == b'\x80\x01'
assert vbi(16384) == b'\x80\x80\x01'
assert vbi(268435455) == b'\xff\xff\xff\x7f'
topic = b'home/temp'
variable_and_payload = struct.pack('!H', len(topic)) + topic + b'\x00\x07' + b'\x00' + b'23.5'
packet = b'\x32' + vbi(len(variable_and_payload)) + variable_and_payload
assert len(variable_and_payload) == 18 and len(packet) == 20
assert packet.hex() == '32120009686f6d652f74656d7000070032332e35'
print('MQTT 5 PUBLISH 编码及 Remaining Length 边界通过')

Topic Alias 的值固定为两字节,MQTT 5 属性标识与属性长度还有额外开销;它不是“1/2 字节别名”。别名映射按连接和方向管理,重连后不能假设 Broker 还记得旧别名。首次建立映射仍需主题名称,后续才能在允许范围内省去重复名称。

会话、保留值与遗嘱的生命周期

三种持久状态服务不同读者,不能用一个“缓存”解释 Retained 每个 topic 的最近保留消息 新订阅者取得当前快照 Session 订阅与未完成交付等状态 同一 Client ID 恢复通信 Will 连接时登记的发布意图 异常离线后通知其他订阅者 Message Expiry 限制消息寿命,Session Expiry 限制会话寿命,Will Delay 控制遗嘱等待。

Clean Start 与 Session Expiry

MQTT 5 用 Clean Start 决定连接时是否开始新会话,用 Session Expiry Interval 决定断开后保存多久。Clean Start=1 也可以配非零到期时间,表示本次清掉旧会话、之后保留新会话;Clean Start=0 不代表一定恢复成功,要看 CONNACK 的 Session Present。

恢复会话应使用稳定 Client ID,并恢复客户端自身的未完成交付状态。两台同时使用同一 Client ID 的设备可能互相顶掉连接,产生持续重连;为每次连接随机生成 ID 又会破坏原本希望恢复的订阅与队列关系。

MQTT 3.1.1 的 Clean Session 与 MQTT 5 的这两个字段不能直接混写。3.1.1 也允许服务器在规定条件下接受零长度 Client ID,不是“只有 5.0 才能由服务器分配”。MQTT 5 则有相应 Assigned Client Identifier 属性向客户端返回分配值。MQTT 3.1.1

Retained:给后来者的快照

带 RETAIN 发布的消息可成为该 topic 的保留值,后续订阅者按订阅选项取得它;新值替换旧值,不能依靠 retained 重放每一次历史温度。带 RETAIN 的零长度 payload 用于清除该 topic 的保留消息,但当前订阅者仍可能收到这次发布。

Retained 状态不属于某个订阅客户端的 session,清除该客户端会话不等于清除所有 topic 的保留值。MQTT 5 的 Message Expiry 还能限制消息寿命,因此“服务器曾保留过”不表示新订阅者永远可获得。

Will:离线推断不是即时故障检测

客户端在 CONNECT 中登记 Will。未正常结束等条件触发后,Broker 按规则发布;正常断开通常取消遗嘱,而 MQTT 5 也定义了明确请求发布 Will 的 DISCONNECT 原因。

Will Delay 可以给短暂重连留时间,若适用会话及时恢复则可避免错误离线通知;会话更早结束时,不能认为遗嘱一定继续等满配置延迟。在线状态消息应携带时间或代际,让订阅者识别陈旧通知,而不是把一条 retained offline 当成永不过期的事实。

会话还在,命令和保留值就一定还在吗?

在20秒处,会话仍在,10秒命令却已失效,不能因恢复了订阅就执行它。把Session Expiry缩到15秒,Will无需等满30秒;retained则仍按自己的期限存在。

正在呈现知识画面
会话还在,命令和保留值就一定还在吗?

不同期限作用于不同对象。长会话不延长短命令,retained不属于订阅会话;Will还受会话结束时刻约束。

Keep Alive、消息到期与慢订阅者

Keep Alive 是客户端发送 MQTT 控制包的间隔约束,已有 PUBLISH 等控制包就能满足活动要求,不是每隔 K 秒无条件发送 PINGREQ。无其他流量时使用 PINGREQ/PINGRESP;服务端在超过约 1.5 倍有效 Keep Alive 未收到控制包时按规则关闭连接,值为 0 则关闭这一机制。MQTT 5 服务端还可以通过 Server Keep Alive 指定使用值。

Session Expiry、Message Expiry 和 Will Delay 各管不同对象。会话保存一天,并不代表一条十秒有效的控制命令能在第二天重新上线后继续执行。设备应用也应核对命令有效期,不能只依赖传输队列配置。

Receive Maximum 限制相应 QoS 1/2 并行交付数,不限制所有 QoS 0 流量,也不是总字节内存上限。Maximum Packet Size 控制单包大小;Broker 队列、客户端缓冲、磁盘持久化和发布速度仍需要各自约束。

假设每秒积压 50 条、每条实际占用 1 KiB,离线十分钟就是约 29.3 MiB,尚未算索引与协议状态。给会话设置很长寿命却不限制队列,不会自动变成安全的离线存储。

Shared Subscription 与业务请求应答

$share/workers/jobs/+ 表示共享组 workers 的订阅,同组中的一个成员接收相应分发,适合工作分摊。具体选哪位成员由服务器策略决定,协议不保证 round-robin、公平处理时长或跨所有 worker 的业务完成顺序。

MQTT 5 的 Response Topic 与 Correlation Data 可帮助应用实现请求/应答,但它们没有自动提供同步 RPC 的超时、授权或事务。请求方要先准备接收应答、为请求分配业务 ID,并在超时后考虑“操作已完成但应答丢失”的分支。

从连接成功到业务有效

Broker 认证解决谁能连接,topic ACL 解决能发布或订阅什么,payload schema 与业务状态解决消息能否执行。Client ID 是会话标识,不应在没有可信绑定时直接充当设备身份。

Mosquitto 等实现的持久化与队列配置会影响重启恢复和资源限制;协议 QoS 不会自动保证每次确认前磁盘已按你的灾备标准持久化。Mosquitto 配置

排查时记录 Client ID、连接代际、topic、业务事件 ID、QoS、原因码和 Session Present。发布无应答先看连接与流控;已获发布确认却没有业务结果,要继续检查订阅匹配、权限、到期及消费提交,而不是只把 QoS 从 1 改成 2。