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 中 + 匹配一个层级,# 必须位于末尾并覆盖零个或多个层级。普通首层通配符不匹配以 $ 开头的系统主题。
| Filter | Topic | 匹配 |
home/+/temp | home/bedroom/temp | 是 |
home/+/temp | home/temp | 否,少一个层级 |
home/# | home | 是,# 可以覆盖零层 |
home/+ | home/ | 是,末尾是空层 |
# | $SYS/broker/uptime | 否,需要显式订阅系统前缀 |
命名应同时考虑路由与授权。例如 tenant/a/device/17/state 可以明确租户与设备范围,但客户端自己声明这个字符串并不能证明它属于租户 a;Broker 的认证与 ACL 需要约束可发布和可订阅的范围。Mosquitto 访问控制配置
QoS:每一跳管理自己的包状态
发布者到 Broker 和 Broker 到订阅者各自协商/执行交付。出站 QoS 受原始发布 QoS、订阅获准上限与服务器能力约束;不能因为源端用 QoS 2,就推断每个订阅者都得到 QoS 2。
| 级别 | 基本交换 | 所管理的性质 |
| QoS 0 | PUBLISH | 无 MQTT 层确认,可能丢失 |
| QoS 1 | PUBLISH、PUBACK | 至少一次交付流程,可能重复 |
| QoS 2 | PUBLISH、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 还记得旧别名。首次建立映射仍需主题名称,后续才能在允许范围内省去重复名称。
会话、保留值与遗嘱的生命周期
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还受会话结束时刻约束。
{"id":"mqtt-independent-lifetimes","title":"会话还在,命令和保留值就一定还在吗?","summary":"不同期限作用于不同对象。长会话不延长短命令,retained不属于订阅会话;Will还受会话结束时刻约束。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"expiry\" data-i18n=\"expiry\"></label><output for=\"expiry\" id=\"expiry-value\"></output><input id=\"expiry\" type=\"range\" min=\"0\" max=\"120\" value=\"90\" step=\"1\"></div><div class=\"control\"><label for=\"time\" data-i18n=\"time\"></label><output for=\"time\" id=\"time-value\"></output><input id=\"time\" type=\"range\" min=\"0\" max=\"120\" value=\"20\" step=\"1\"></div><div id=\"lifetimes\" class=\"intervals\"></div><section class=\"panel\"><h3 data-i18n=\"states\"></h3><div id=\"states\"></div></section><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#states .tr{grid-template-columns:1fr 1fr}.tr>*{min-height:3.6em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const e=+$('#expiry').value,n=+$('#time').value,w=Math.min(e,30);set('expiry-value',e);set('time-value',n);intervals('lifetimes',[{start:0,end:e,label:'session '+e},{start:0,end:Math.min(10,e),label:'command ≤ '+Math.min(10,e),cls:'gold'},{start:0,end:60,label:'retained 60'},{start:0,end:w,label:'Will at '+w,cls:'gold'}],120);table('states',[[t('session'),t(n<e?'live':'gone')],[t('command'),t(n<Math.min(10,e)?'live':'gone')],[t('retained'),t(n<60?'live':'gone')],[t('will'),t(n<w?'pending':'published')]]);$('#states').dataset.snapshot=JSON.stringify({session:n<e,command:n<Math.min(10,e),retained:n<60,will:n>=w});}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"command":"排队命令","expiry":"Session Expiry(s)","gone":"不可再取","heading":"会话还在,命令和保留值就一定还在吗?","intro":"移动断线后的时间,独立观察会话、排队命令、保留值和Will触发。","live":"保留","pending":"等待","published":"已触发发布","reset":"重置","retained":"Retained","scope":"t=0异常断线,无重连;Session Expiry可调,命令剩余10s,retained剩余60s,Will Delay30s。假设已有Will,正常规则到期发布,不模拟检测断线所需时间。","scopeLabel":"模型范围与约定","session":"会话","states":"对象状态","time":"断线后时间(s)","will":"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。