简而言之

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/floor2office/floor2/ 是两个不同的主题。

$ 开头的主题保留给服务器自用,通常是承载代理统计数据的 $SYS。通配符够不到它们:订阅 # 不会显示 $SYS,那需要一个明确的过滤器。

服务质量:QoS 0、1 和 2

MQTT 允许你为每条消息选择投递上要花多少力气。发布和订阅时分别设定,实际生效的是两者中较低的那个。

级别保证工作方式适用场合
QoS 0至多一次发出去就不管,没有确认高频遥测,反正下一条读数一秒后就来
QoS 1至少一次接收方回 PUBACK;没有回应就重发不能丢失、但可以接受重复的事件
QoS 2恰好一次四个报文的握手,把重复剔除命令和计费,重复一次就是缺陷

保证越强,代价越高:QoS 2 是四个报文而不是一个,两端还都要维持状态。请抵住「以防万一,处处都用 2」的冲动:一万个传感器每秒以 QoS 2 发布,代理服务器的负担会高出一个数量级,而好处一点没有。

QoS 1 的「至少一次」就是字面意思:重复是可能的,也是正常的。如果重复处理会破坏你的逻辑,请在载荷里放一个事件 ID,由接收方丢掉重复项——这比 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.12010已过时IBM 的规范;客户端 ID 限 23 个字符;如今没有理由再选它
MQTT 3.1.12014OASIS 标准,ISO/IEC 20922部署最广的版本;几乎所有产品都支持
MQTT 5.02019OASIS 标准一次大幅翻修:报文属性、原因码、共享订阅

另有一支 MQTT-SN,是面向没有 TCP 的网络(ZigBee、无线链路、UDP)的兄弟协议。它不是「MQTT 的某个版本」,而是独立的规范;这类网络会配一台网关,把流量翻译成普通的 MQTT。

3.1.1 和 5.0 可以毫无障碍地共存。同时支持两者的代理服务器会在一个端口上同时服务,3.1.1 的客户端也能顺利收到 5.0 客户端发布的消息。不必一次性全部迁移——随着固件更新,一台台迁过去即可。

MQTT 5.0 实际带来了什么

版本 5 源自实践中积攒的种种不便。影响最大的几点是:

  • 原因码。在 3.1.1 里,一次拒绝看上去就是套接字被悄悄关掉,剩下的只能靠猜。5.0 里服务器会说明缘由:密码不对、该主题没有权限、报文太大。
  • 报文属性。消息可以携带元数据:Content TypeCorrelation Data、任意的键值对。以前这些只能塞进载荷或主题名里。
  • 请求与响应。Response TopicCorrelation Data 两个字段,把普通的 RPC 变成一等公民,而不再是各自约定的土办法。
  • 共享订阅$share/组/过滤器):同一个消费方的多个实例分摊同一条数据流。这就是协议层面的负载均衡与故障接管。
  • 会话与消息的存活时长。Session ExpiryMessage Expiry 让你可以说「留一天,然后忘掉」,而不是只能在「清理」与「不清理」之间二选一。
  • 流量控制。Receive Maximum 限制在途未确认消息的数量,Maximum Packet Size 保护小设备不被自己消化不了的报文噎住。
  • 主题别名。长名字只发一次,之后用两个字节的编号代替——层级越深,省得越多。
  • 订阅标识符、No Local、Retain As Published、Retain Handling。这些细节消除了由来已久的坑:听见自己消息的回声、每次重连都被保留消息淹没、不知道消息是从哪条订阅来的。
  • 延迟的遗嘱。Will Delay Interval 让一次瞬时抖动不至于触发告警:只有客户端未能及时回来,遗嘱才会被发布。

实践结论是:新项目请从 5.0 开始。共享订阅和易懂的错误码能省下好几周的调试时间,而且与老设备的兼容性并不会因此丢失。

MQTT 与 HTTP 及其他选择

「已经有 REST 了,为什么还要 MQTT?」这是个合理的问题。差别不在时髦与否,而在于对话的方向和代价。

MQTTHTTP / REST
模型发布与订阅,多对多请求与响应,一对一
谁先开口两边都行:服务器随时可以推送只有客户端;没人问,服务器就不出声
连接一条,长久保持通常每个请求一条新连接
额外开销从 2 个字节起数百字节的请求头
得知变化的方式它自己送上门按固定间隔轮询
断线时的表现队列、重发、retain、遗嘱请求直接失败
最擅长遥测、命令、事件文档、文件、系统对接、Web

轮询最能说明问题。要在一秒内通过 HTTP 得知某个事件,一千台设备就得每秒问上一千次,而几乎每次的回答都是「没有新东西」。在 MQTT 里,事件发生的那一刻就自己送到,其间不消耗任何流量。

CoAP、AMQP 和 WebSocket 又如何

  • CoAP 是面向极小节点的、跑在 UDP 上的 REST。比 MQTT 更轻,但不下功夫的话,既没有队列也没有可靠投递。
  • AMQP 更重也更丰富:复杂的路由、事务、企业级队列。在服务器之间用很合理,用在传感器上则过头了。
  • WebSocket 是一种传输方式,不是替代品:MQTT 自己就跑在 WebSocket 之上,好在浏览器里工作。
  • Sparkplug B 不是对手,而是 MQTT 之上的一层:它规定工业系统中主题如何命名、载荷如何编码,好让不同厂商的 SCADA 软件读懂同一批数据。
MQTT 与 HTTP 并不互相取代。健康的系统通常两者兼用:读数和命令的数据流走 MQTT,报表、导出和对接外部服务走 HTTP。

传输、端口与载荷

MQTT 跑在 TCP 之上,只要求可靠、有序的投递。惯用的端口如下:

端口用途
1883TCP 上的 MQTT,不加密
8883TLS 上的 MQTT —— 加密连接的标准端口
80 / 443WebSocket 上的 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,或者把账号放在外部数据库里。
  • 重启后能否幸存?保留消息、持久会话和队列必须落盘,否则一次计划内的重启就意味着丢数据。
  • 提供哪些限制手段?一台失控的设备不该拖垮整个系统:频率和流量的限制很重要。
  • 价格多少、跑在哪里?云服务在开始按消息计费之前都很省心;自建服务器需要人来运维,但数据留在自己手里。

对于中小规模的部署——从智能家居到几千个端点的工厂——一台普通机器上的单个代理服务器通常就够了。集群的必要性远低于设计阶段的想象;更常见的做法是用一条桥接把两台独立的代理服务器连起来,只转发真正重要的主题。

ELX-MQTT Broker 覆盖了上面这份清单:两个协议版本的完整支持、带实时曲线和消息流的 Web 控制台、主题 ACL、用于开通设备的 REST API、保存在 SQLite 中的状态、频率限制以及桥接。在 Debian 和 Ubuntu 上一条命令即可安装,Windows 上有安装程序;免费,且不限设备数量。

五分钟上手

理解上述内容最快的办法,是自己跑起一台代理服务器,亲眼看看消息。在 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 仍然是更好的工具。