简而言之
MQTT(Message Queuing Telemetry Transport)是一套轻量的消息协议,基于发布与订阅,专为链路糟糕、内存有限、还得省电的设备而生。
它由 IBM 的 Andy Stanford-Clark 与 Arcom 的 Arlen Nipper 在 1999 年创造,用来通过卫星传送输油管道的遥测数据。当时的条件毫不宽容:带宽又贵又窄,链路频繁中断,设备孱弱。这些约束塑造了协议的方方面面——极小的报文头、一条长久保持的连接,以及断线后不丢消息的能力。
如今 MQTT 是 OASIS 标准,3.1.1 版本还被采纳为国际标准 ISO/IEC 20922。它早已走出石油行业:智能家居、各类计量表、机床、汽车和医疗设备都跑在它上面。
每条消息最小的额外开销只有 两个字节。相比之下,一次 HTTP 请求光是请求头就要花掉数百字节,正文还没开始。当野外的设备通过蜂窝模块每秒上报一次温度时,这个差别就决定了整套设计。
工作方式:发布、订阅与代理服务器
在 HTTP 里,客户端发问、服务器作答。在 MQTT 里,谁也不向谁发问:设备把消息发布到某个主题,所有订阅了该主题的一方都会收到。发送方不知道谁在读,甚至不知道有没有人在读;接收方也不知道是谁发的。把两者联系起来的,只有主题的名字。
中间永远站着一台代理服务器——它维持连接、保管订阅树、把消息分发出去。没有它这套模型就转不起来:正是代理服务器把「发向虚空」变成「送到每一个需要的人手里」。
代理服务器做些什么
- 接受并维持连接——同时数千条,连续数年不中断。
- 核验来连接的是谁:用户名和密码、证书,或者令牌。
- 决定每个客户端能做什么:发布和订阅的权限按主题授予。
- 把每条发布的消息与订阅树比对,并分发副本。
- 保存状态:最后已知的值(retain)、给不在线客户端的队列、尚未完成的确认。
- 跟踪谁还活着,并代替消失的一方发布它的遗嘱。
把发送方与接收方解耦,并不是技术细节,而是这套模型的全部要义。你可以换掉一个传感器、再加一个消费方,或者在旁边挂上一套分析,而无需改写任何东西:大家都在跟主题说话,而不是跟彼此说话。
主题与通配符
主题是一串以斜杠分隔的层级。什么都不必事先登记:向一个新主题发布消息,它就当场诞生了。
office/floor2/room14/temperature
office/floor2/room14/humidity
office/floor2/room15/temperature
订阅时可以用两个通配符一次覆盖整条分支:
| 通配符 | 含义 | 示例 | 覆盖范围 |
|---|---|---|---|
+ | 恰好一个层级 | office/floor2/+/temperature | 二楼每个房间的温度 |
# | 该层级及其之下的一切 | office/# | 办公室里的所有主题 |
# 只能作为最后一个层级:office/#/temperature 不是合法的过滤器。主题区分大小写,office/floor2 与 office/floor2/ 是两个不同的主题。
$ 开头的主题保留给服务器自用,通常是承载代理统计数据的 $SYS。通配符够不到它们:订阅 # 不会显示 $SYS,那需要一个明确的过滤器。服务质量:QoS 0、1 和 2
MQTT 允许你为每条消息选择投递上要花多少力气。发布和订阅时分别设定,实际生效的是两者中较低的那个。
| 级别 | 保证 | 工作方式 | 适用场合 |
|---|---|---|---|
| QoS 0 | 至多一次 | 发出去就不管,没有确认 | 高频遥测,反正下一条读数一秒后就来 |
| QoS 1 | 至少一次 | 接收方回 PUBACK;没有回应就重发 | 不能丢失、但可以接受重复的事件 |
| QoS 2 | 恰好一次 | 四个报文的握手,把重复剔除 | 命令和计费,重复一次就是缺陷 |
保证越强,代价越高:QoS 2 是四个报文而不是一个,两端还都要维持状态。请抵住「以防万一,处处都用 2」的冲动:一万个传感器每秒以 QoS 2 发布,代理服务器的负担会高出一个数量级,而好处一点没有。
retain、遗嘱消息、会话与保活
retain —— 最后已知的值
普通消息只会送到此刻正在订阅的一方。带 retain 标志的消息,代理服务器还会记住它,并在有人订阅的那一刻立即交给新订阅者。这就是对那个老问题的标准答案:仪表盘打开是空的,而且要一直空到传感器肯发下一条读数为止。
每个主题只保留一条 retain 消息,也就是最新的那条。要清除它,请向同一主题发布一条带 retain 标志的空载荷。
遗嘱消息 —— 为链路断掉那一刻准备的
连接时,客户端可以把一份遗嘱交给代理服务器:一个主题、一段载荷和相应的选项。如果连接不体面地断掉——没有 DISCONNECT 报文——代理服务器就代它发布那条消息。经典做法是用一个带 retain 的 device/42/status 主题,连接时写入 online,遗嘱设为 offline。这样系统始终知道谁在线,完全不必轮询。
会话:清理会话与持久会话
清理会话的寿命与连接相同:断开之后订阅就被忘掉了。持久会话能挺过中断:代理服务器记住订阅,并在客户端缺席期间把 QoS 1 和 2 的消息排进队列,等它回来再一并送达。对于每小时才醒一次的设备来说,这是不漏消息的唯一办法。
保活 —— 察觉链路已经死了
一条断掉的 TCP 连接,可能在很长时间里对两端都显得好端端的。因此客户端会声明一个保活间隔,没有别的话要说时就发一个 ping。若在一个半间隔内什么也没听见,代理服务器就判定该客户端已经消失,关闭连接并发布它的遗嘱。
各个版本:3.1、3.1.1 和 5.0
实践中会遇到三个版本,它们之间的差别并不只是表面功夫。
| 版本 | 年份 | 状态 | 关键之处 |
|---|---|---|---|
| MQTT 3.1 | 2010 | 已过时 | IBM 的规范;客户端 ID 限 23 个字符;如今没有理由再选它 |
| MQTT 3.1.1 | 2014 | OASIS 标准,ISO/IEC 20922 | 部署最广的版本;几乎所有产品都支持 |
| MQTT 5.0 | 2019 | OASIS 标准 | 一次大幅翻修:报文属性、原因码、共享订阅 |
另有一支 MQTT-SN,是面向没有 TCP 的网络(ZigBee、无线链路、UDP)的兄弟协议。它不是「MQTT 的某个版本」,而是独立的规范;这类网络会配一台网关,把流量翻译成普通的 MQTT。
MQTT 5.0 实际带来了什么
版本 5 源自实践中积攒的种种不便。影响最大的几点是:
- 原因码。在 3.1.1 里,一次拒绝看上去就是套接字被悄悄关掉,剩下的只能靠猜。5.0 里服务器会说明缘由:密码不对、该主题没有权限、报文太大。
- 报文属性。消息可以携带元数据:
Content Type、Correlation Data、任意的键值对。以前这些只能塞进载荷或主题名里。 - 请求与响应。
Response Topic和Correlation Data两个字段,把普通的 RPC 变成一等公民,而不再是各自约定的土办法。 - 共享订阅(
$share/组/过滤器):同一个消费方的多个实例分摊同一条数据流。这就是协议层面的负载均衡与故障接管。 - 会话与消息的存活时长。
Session Expiry和Message Expiry让你可以说「留一天,然后忘掉」,而不是只能在「清理」与「不清理」之间二选一。 - 流量控制。
Receive Maximum限制在途未确认消息的数量,Maximum Packet Size保护小设备不被自己消化不了的报文噎住。 - 主题别名。长名字只发一次,之后用两个字节的编号代替——层级越深,省得越多。
- 订阅标识符、No Local、Retain As Published、Retain Handling。这些细节消除了由来已久的坑:听见自己消息的回声、每次重连都被保留消息淹没、不知道消息是从哪条订阅来的。
- 延迟的遗嘱。
Will Delay Interval让一次瞬时抖动不至于触发告警:只有客户端未能及时回来,遗嘱才会被发布。
实践结论是:新项目请从 5.0 开始。共享订阅和易懂的错误码能省下好几周的调试时间,而且与老设备的兼容性并不会因此丢失。
MQTT 与 HTTP 及其他选择
「已经有 REST 了,为什么还要 MQTT?」这是个合理的问题。差别不在时髦与否,而在于对话的方向和代价。
| MQTT | HTTP / REST | |
|---|---|---|
| 模型 | 发布与订阅,多对多 | 请求与响应,一对一 |
| 谁先开口 | 两边都行:服务器随时可以推送 | 只有客户端;没人问,服务器就不出声 |
| 连接 | 一条,长久保持 | 通常每个请求一条新连接 |
| 额外开销 | 从 2 个字节起 | 数百字节的请求头 |
| 得知变化的方式 | 它自己送上门 | 按固定间隔轮询 |
| 断线时的表现 | 队列、重发、retain、遗嘱 | 请求直接失败 |
| 最擅长 | 遥测、命令、事件 | 文档、文件、系统对接、Web |
轮询最能说明问题。要在一秒内通过 HTTP 得知某个事件,一千台设备就得每秒问上一千次,而几乎每次的回答都是「没有新东西」。在 MQTT 里,事件发生的那一刻就自己送到,其间不消耗任何流量。
CoAP、AMQP 和 WebSocket 又如何
- CoAP 是面向极小节点的、跑在 UDP 上的 REST。比 MQTT 更轻,但不下功夫的话,既没有队列也没有可靠投递。
- AMQP 更重也更丰富:复杂的路由、事务、企业级队列。在服务器之间用很合理,用在传感器上则过头了。
- WebSocket 是一种传输方式,不是替代品:MQTT 自己就跑在 WebSocket 之上,好在浏览器里工作。
- Sparkplug B 不是对手,而是 MQTT 之上的一层:它规定工业系统中主题如何命名、载荷如何编码,好让不同厂商的 SCADA 软件读懂同一批数据。
传输、端口与载荷
MQTT 跑在 TCP 之上,只要求可靠、有序的投递。惯用的端口如下:
| 端口 | 用途 |
|---|---|
1883 | TCP 上的 MQTT,不加密 |
8883 | TLS 上的 MQTT —— 加密连接的标准端口 |
80 / 443 | WebSocket 上的 MQTT:往往是从浏览器出去、或穿过严格的企业防火墙的唯一通路 |
对协议而言,载荷不过是一串字节。MQTT 不了解内容,也不做任何规定:JSON、CBOR、protobuf、一个数字或一张图片都行。形式上的上限是 256 MB,但实践中超过几百 KB 的内容应交给 HTTP,MQTT 上只传一个链接。
实践中 JSON 占主流:人能读懂,任何语言都能解析,而且足够紧凑。链路很窄时,二进制格式才配得上它的位置——而恰恰在那里,MQTT 5.0 的 Content Type 属性能派上用场,老老实实说明里面装的是什么。
安全
MQTT 本身不加密任何东西:在 1883 端口上,用户名和密码都是明文传输。安全性来自三个彼此独立的层次,其中任何一层都不可省略。
为通道加密
在 8883 端口上使用 TLS。对于弱到跑不动 TLS 的设备,唯一体面的办法是把它们放在隔离网络里,除了经由网关,什么也不放出去。
认证
最经典的是 CONNECT 报文里的用户名和密码。更严格的是客户端证书:设备出示自己的证书,代理服务器校验签名。更灵活的是 JWT:设备带来一个有签名、有期限的令牌,撤销权限时也不必改动代理服务器。好的代理服务器还能从外部数据库、CSV 文件或 HTTP 服务读取账号,免得设备清单在两处各存一份。
主题权限
认证回答「你是谁」,授权回答「你能做什么」。访问控制列表规定客户端可以读哪些主题、可以写哪些主题。正确的设定是除明确允许者外一律拒绝。
# 的读写权限。只要有人把固件镜像提取出来,攻击者就能看到整个系统的流量,并向其中任何设备下达命令。请把每台设备限制在自己的分支里:device/{id}/#,仅此而已。在此之上还要加上发布频率限制(免得一台失控的设备把代理服务器埋掉)、报文长度上限,以及寻常的网络卫生:代理服务器的控制台没有理由暴露在公网上。
MQTT 真正被用在哪里
智能家居与楼宇自动化
按数量算是最大的领域。Home Assistant、Zigbee2MQTT、ESPHome 和 openHAB 都通过 MQTT 代理服务器交流,而这台代理服务器同时充当了不同厂商设备之间的黏合剂:三家品牌的插座、漏水传感器和采暖控制器可以配合得天衣无缝,因为每一个都只往自己的主题里写。
工业与 SCADA
机床读数、产线状态、运行时长计数。经典做法是用一台网关从设备读取 Modbus 或 OPC UA,再用 MQTT 重新发布,随后 SCADA、历史库和预测性维护系统可以同时取用而互不干扰。Sparkplug B 与 MQTT 5.0 的共享订阅正是在这里体现价值。
计量与公用事业
水表、燃气表、电表和热量表:靠电池供电的设备每小时醒来一次,上报读数,然后继续休眠。这里挑大梁的是持久会话和 retain——即便某块表要到傍晚才露面,服务器也始终能看到最后的读数。
交通与车联网
坐标、油耗、冷藏箱状态、驾驶行为。链路是一张会在隧道里和城外消失的蜂窝网络。排队与重连后的重发,把断断续续的连接变成了连绵不断的数据流。
能源、农业与医疗
光伏电站与变电站、灌溉系统与气象站、病人监护仪与疫苗冰箱。它们的共同点是:端点众多、链路薄弱、有些事件绝不能丢,而且故障必须立刻知晓。
智慧城市与物流
路灯、停车、会上报装满程度的垃圾箱、货物追踪和冷链温度。数以万计的设备发送简短而稀疏的消息——正是 MQTT 当初所针对的负载形态。
设计主题:值得尽早定下的事
主题结构就是你系统的 API。日后再改会很痛,因为固件和服务器必须同步更新。下面几条规矩能省下不少时间。
- 由大到小。
plant/hall3/line2/machine7/temperature让你可以在任意层级订阅。顺序反过来,通配符就没用了。 - 给设备 ID 单独一个层级。正因为如此,一条 ACL 规则就能把设备锁在自己的分支里。
- 开头不要加斜杠。
/office/temp会造出一个空的第一层——虽然合法,却是长期的困惑之源。 - 只用拉丁字母、数字、连字符和下划线。名字里的空格以及
+、#、$会引发谁也想不到的故障。 - 绝不要把会变的数据编进主题。
sensor/42/temp/21.5是个错误:数值应当放进载荷,否则代理服务器会得到一棵无边无际的主题树,retain 也就没用了。 - 把状态和命令分开。比如用
device/42/state表示设备上报的内容,用device/42/cmd表示下达给它的内容。否则自己命令的回声迟早会让逻辑绕成一圈。 - retain 用于状态,而不是遥测。retain 适合「最后已知的状态」,对连绵的读数流则毫无意义。
v1 层级,今天几乎不花什么代价,两年后却能让你在不弄坏一大批老设备的前提下推出新的载荷格式。如何挑选代理服务器
代理服务器有很多,而选择通常归结为少数几个问题。
- MQTT 5.0 是否完整支持?部分支持很常见,而且往往是在最糟糕的时刻才发现。
- 能不能看见正在发生什么?能看到谁连着、哪些主题是活的、此刻有什么在飞,可以省下好几个小时的排查。没有控制台的代理服务器,会把每个问题都变成翻日志的考古工作。
- 设备如何开通?数量上千时手工操作绝无可能——你需要一个 API,或者把账号放在外部数据库里。
- 重启后能否幸存?保留消息、持久会话和队列必须落盘,否则一次计划内的重启就意味着丢数据。
- 提供哪些限制手段?一台失控的设备不该拖垮整个系统:频率和流量的限制很重要。
- 价格多少、跑在哪里?云服务在开始按消息计费之前都很省心;自建服务器需要人来运维,但数据留在自己手里。
对于中小规模的部署——从智能家居到几千个端点的工厂——一台普通机器上的单个代理服务器通常就够了。集群的必要性远低于设计阶段的想象;更常见的做法是用一条桥接把两台独立的代理服务器连起来,只转发真正重要的主题。
五分钟上手
理解上述内容最快的办法,是自己跑起一台代理服务器,亲眼看看消息。在 Debian 或 Ubuntu 上只要三条命令:
sudo wget -qO /usr/share/keyrings/elx-repo.gpg https://repo.um-d.ru/elx-repo.gpg
echo "deb [signed-by=/usr/share/keyrings/elx-repo.gpg] https://repo.um-d.ru stable main" | sudo tee /etc/apt/sources.list.d/elx-repo.list
sudo apt-get update && sudo apt-get install elxmqttbroker
控制台在 http://你的服务器:8567 打开。创建一个用户,然后用任意客户端试试收发,比如 mosquitto 的命令行工具:
# 在一个窗口里 —— 订阅该设备发出的一切
mosquitto_sub -h localhost -u sensor-42 -P password -t 'sensor-42/#' -v
# 在另一个窗口里 —— 发布一条读数
mosquitto_pub -h localhost -u sensor-42 -P password -t 'sensor-42/temp' -m '21.5'
# 同样的消息,但为将来的订阅者记下来
mosquitto_pub -h localhost -u sensor-42 -P password -t 'sensor-42/temp' -m '21.5' -r
接着可以玩玩 retain 标志,断开一个订阅者看看持久会话里攒下了什么,设置一份遗嘱再拔掉网线。这样折腾半小时,胜过任何一篇文章,包括本文。
简短回答
用大白话说,MQTT 是什么?
它是设备之间通过一个中间人交换短消息的方式。设备把消息发到一个有名字的「主题」,所有订阅了该主题的程序会立刻收到。发送方和接收方彼此并不了解,正因如此,两边都可以各自独立地更换或增加。
为什么需要 MQTT 代理服务器?
代理服务器是那台维持着与所有设备连接的服务器:它核验权限,把发布的消息与订阅比对,并分发副本。没有它 MQTT 就转不起来:发布和订阅是在代理服务器内部相遇的。
MQTT 5.0 与 3.1.1 有什么不同?
主要是:用易懂的原因码取代悄悄关闭的连接、可携带元数据的报文属性、把请求响应升为一等模式、用于在多个消费方之间分摊负载的共享订阅、可控的会话与消息存活时长、流量控制,以及能省带宽的主题别名。
该不该迁到 MQTT 5.0?
新项目应该迁——好处实实在在,兼容性也不会丢。已有的 3.1.1 系统不必急着迁移:两个版本可以在同一台代理服务器上同时运行,设备可以随固件更新逐步切换。
该用哪个 QoS?
高频遥测、丢一条读数无所谓的,用 QoS 0。不能丢失的事件用 QoS 1,前提是接收方能剔除重复。只有命令和计费这类重复处理不可接受的场景才用 QoS 2。「以防万一」处处都上 QoS 2,是让代理服务器过载最省事的办法。
MQTT 里的 retain 是什么意思?
这是一个标志,告诉代理服务器记住某条消息,并在有人订阅的那一刻交给新订阅者。每个主题只保留最新的一条。它是对「立刻显示当前状态,而不是等传感器下一次更新」的标准答案。要清除它,发布一条带同样标志的空载荷即可。
什么是遗嘱消息?
这是客户端在连接时交给代理服务器的一条消息;如果连接没有正常断开就死掉了,代理服务器会代它发布。通常用来把设备标记为离线,好让系统无需轮询就能知道它掉线了。
MQTT 安全吗?
协议本身不加密任何东西:在 1883 端口上密码是明文传输的。安全性来自 8883 端口上的 TLS、基于密码、证书或 JWT 的认证,以及按「除明确允许者外一律拒绝」运作的主题访问控制列表。
一台代理服务器能带多少设备?
这取决于代理服务器和负载,但可以给个量级:普通服务器上的单个进程,在典型遥测场景下从容维持数万条并发连接。真正的上限通常不是设备数量,而是消息频率和你要求的 QoS。
MQTT 能在浏览器里用吗?
能,通过 WebSocket 上的 MQTT。浏览器不能任意打开 TCP 连接,所以把协议包在 WebSocket 里——这样一个网页控制台就能直接订阅主题,中间不需要服务器。
主题里的 + 和 # 是什么意思?
它们是订阅用的通配符。+ 代替恰好一个层级;# 覆盖余下的所有层级,且只能放在过滤器末尾。带通配符的主题不能用来发布——它们只用于订阅。
在物联网里,MQTT 为什么比 HTTP 更合适?
它不需要轮询:事件发生时消息自己就送到了。额外开销从两个字节起,而不是几百字节;连接只有一条且长久保持;队列、重发和 retain 让系统在断线时也不丢数据。至于报表、文件和系统对接,HTTP 仍然是更好的工具。