5月27日 18:12

MQTT 主题通配符有哪些?怎么用才不出错?

MQTT 主题通配符是订阅机制中绕不过去的核心概念。当你只需要监听某一类设备的数据,而不是逐个订阅每一个具体主题时,通配符就能派上用场。MQTT 规范定义了两种通配符:单级通配符 + 和多级通配符 #,它们各自的匹配规则和使用限制完全不同,混用或用错都会导致订阅失败或收到意料之外的消息。

单级通配符 +

单级通配符用 + 表示,它的作用很明确:只匹配主题层级中的一个层级,且该层级不能为空

所谓"一个层级",指的是两个 / 之间的一段内容。比如主题 home/livingroom/temperature 包含三个层级:homelivingroomtemperature。如果你订阅 home/+/temperature,那么第二层级无论是什么值都会被匹配到——home/bedroom/temperaturehome/kitchen/temperature 都能命中,但 home/temperature 不行(缺少一个层级),home/livingroom/kitchen/temperature 也不行(多了一个层级)。

几个需要特别注意的点:

  • + 可以在同一个主题过滤器中出现多次。比如 sensor/+/data/+ 是合法的,它会匹配 sensor/001/data/temperaturesensor/002/data/humidity,但不匹配 sensor/001/data(层级不够)或 sensor/001/data/temperature/value(层级超出)。
  • + 不能匹配空层级。订阅 home/+/temperature 不会匹配 home//temperature,因为中间那个层级是空的,而 + 要求至少有一个字符。
  • + 必须占据整个层级。home/room+/temperature 是无效写法,它不会匹配 home/room1/temperature——+ 不是一个正则里的"一个任意字符",而是代表整个层级的通配。

多级通配符

多级通配符用 # 表示,它的匹配范围比 + 大得多:匹配当前层级及其后面的所有层级,包括零个层级

比如订阅 home/#,以下主题全部能命中:home/livingroomhome/livingroom/temperaturehome/bedroom/humidity/value。甚至 home/ 本身也能匹配(# 匹配了零个层级,但注意 home 不行,因为 # 前面必须有 / 或者 # 本身就是整个过滤器的第一个字符)。

# 的使用限制比 + 更严格:

  • 必须放在主题过滤器的最后home/#/temperature 是无效的,Broker 会直接拒绝这种订阅。这是 MQTT 规范的硬性要求,没有例外。
  • 每个主题过滤器只能出现一次home/#/# 这种写法同样无效。
  • 前面必须有 / 分隔(除非 # 是整个过滤器的唯一内容,即单独订阅 #,这会匹配所有主题)。比如 home# 是无效的,home/# 才是正确的。

单独订阅 # 是一个特殊用法——它匹配 Broker 上的所有主题。这在调试阶段偶尔有用,但在生产环境中极度危险,不仅会造成性能问题,还可能收到大量不相关的消息。

+ 和 # 的组合使用

两种通配符可以在同一个主题过滤器中组合使用,只要遵守各自的规则。最常见的组合模式是先用 + 固定某个层级,再用 # 捕获剩余部分:

shell
订阅:home/+/sensors/# 匹配: home/livingroom/sensors/temperature ✓ home/livingroom/sensors/temperature/value ✓ home/bedroom/sensors/humidity ✓ 不匹配: home/sensors/temperature ✗(+不能匹配空层级) home/livingroom/sensors ✗(# 前缺少 /)
shell
订阅:sensor/+/# 匹配: sensor/001/data ✓ sensor/001/data/temperature ✓ sensor/002/data/humidity/value ✓ 不匹配: sensor ✗(+至少需要一个层级) office/001/data ✗(第一层级不匹配)

组合使用时,最容易犯的错误是在 # 后面继续加内容。记住一条原则:过滤器里出现了 #,它后面就不能再有任何东西

通配符只能用于订阅,不能用于发布

这是一个初学者常踩的坑。MQTT 规范明确规定:通配符只适用于订阅(SUBSCRIBE)操作,发布(PUBLISH)时必须指定完整的确切主题

这意味着你不能往 home/+/temperature 发布消息,Broker 不会帮你把消息分发到 home/livingroom/temperaturehome/bedroom/temperature。发布时,客户端必须明确指定目标主题,比如 home/livingroom/temperature

这个设计是合理的:如果允许通配符发布,消息的路由方向就变得不可预测,整个发布-订阅模型的确定性会被破坏。

通配符匹配的实际示例

理解规则是一回事,在实际场景中正确使用是另一回事。下面用几个常见场景来演示:

场景一:监控所有温度传感器

假设你的传感器主题结构为 sensors/{device_id}/temperature,你想接收所有设备的温度读数,订阅 sensors/+/temperature 即可。每台设备发布消息到自己的确切主题(如 sensors/001/temperature),而你的客户端只需要一条订阅规则就能全部收到。

场景二:监控某个楼层所有设备

主题结构为 building/floor1/{device_type}/{device_id},你只关心一楼的全部数据,那就订阅 building/floor1/#。不管后面有多少层级、是什么设备类型,一楼的所有消息都会推送到你的客户端。

场景三:订阅特定设备类型的状态

主题结构为 device/{device_id}/status,你想监控所有设备的在线/离线状态。订阅 device/+/status,每台设备状态变化都能收到。

场景四:订阅所有告警

告警消息散布在多个层级中:alert/critical/overheatalert/warning/low_batteryalert/info/maintenance。订阅 alert/# 一条规则覆盖全部告警类型。

场景五:分层指标收集

系统指标主题可能是 system/{service}/metrics/{metric_name}/{instance}。如果你想收集某个服务下所有实例的所有指标,可以订阅 system/payment/metrics/#。如果你需要所有服务的同一个指标名,则用 system/+/metrics/cpu_usage/#

通配符使用中的常见错误

在实际开发中,以下错误反复出现:

错误一:把 + 当成正则的 .

+ 不是正则表达式里的 .,它匹配的是一整个层级,而不是一个字符。home/room+/temperature 不会匹配 home/room1/temperature,这是无效的订阅格式。

错误二:把 # 放在中间

home/#/temperature 看起来像"home 下面任意层级后面跟 temperature",但 MQTT 不支持这种用法。# 只能在最后,没有例外。如果你需要这种模式,只能通过精确订阅多个主题来弥补。

错误三:期望 + 匹配空层级

home/+/temperature 不会匹配 home//temperature。空层级在实际业务中很少出现,但如果你手动拼接主题字符串时不小心产生了连续的 /,就会触发这个问题,而且很难排查。

错误四:在生产环境订阅 #

单独订阅 # 会收到 Broker 上的所有消息。调试时可能方便,但生产环境下这会给 Broker 和客户端同时带来不必要的负担,还可能暴露不该看到的数据。

错误五:忘记 # 前面的 /

home# 是无效的。如果你的主题层级超过一层,# 前面必须有 /。只有当 # 是整个过滤器的唯一字符时才不需要前面的 /

通配符对性能的影响

通配符订阅不是免费的。Broker 收到每一条消息后,都需要遍历所有订阅进行主题匹配。精确订阅的匹配是 O(1) 的哈希查找,而通配符订阅需要逐个做模式匹配,复杂度至少是 O(n),n 是通配符订阅的数量。

实际影响取决于 Broker 的实现和你的订阅规模。EMQX、Mosquitto 等主流 Broker 都对通配符匹配做了优化(比如用 Trie 树),在几千个订阅的场景下,性能差异通常可以忽略。但当订阅数达到十万甚至百万级别时,通配符匹配的开销就会显现。

几个性能方面的建议:

  • 能用精确订阅就不用通配符。如果你只需要三台设备的数据,分别订阅三个主题比 sensor/+/temperature 更高效。
  • 通配符越具体越好building/floor1/#building/# 的匹配范围小,Broker 的过滤效率也更高。
  • 主题层级不要太深。5 层以内的主题结构在匹配性能上不会有问题,超过 10 层的场景需要评估。
  • 避免大量客户端同时订阅宽泛通配符。1000 个客户端都订阅 # 比它们各自订阅不同的精确主题对 Broker 的压力大得多。

通配符与安全控制

通配符订阅天然和权限控制存在张力。一条 # 订阅就能绕过主题层级的隔离,所以 ACL(访问控制列表)的配置必须考虑通配符场景。

主流 Broker 都支持基于主题模式的 ACL 规则。比如 EMQX 可以配置"允许客户端订阅 sensors/+/temperature,但拒绝 sensors/#",从而限制客户端只能读取温度数据,不能读取湿度、压力等其他传感器数据。

安全配置的核心原则是最小权限:只授予客户端完成其工作所需的最小订阅范围。如果某个客户端只需要一楼的数据,就只给它 building/floor1/# 的权限,而不是 building/#

另外,通配符订阅可能带来信息泄露风险。假设你的主题结构是 tenant/{tenant_id}/data,如果某个客户端订阅了 tenant/+/data,它就能收到所有租户的数据。在多租户系统中,这个问题尤其严重,ACL 必须严格限制跨租户的通配符订阅。

代码示例

Python(paho-mqtt)

python
import paho.mqtt.client as mqtt def on_connect(client, userdata, flags, reason_code, properties): print(f"Connected: {reason_code}") # 订阅所有设备的温度数据(单级通配符) client.subscribe("sensors/+/temperature") # 订阅卧室的所有数据(多级通配符) client.subscribe("home/bedroom/#") # 组合使用:所有服务的指标数据 client.subscribe("system/+/metrics/#") def on_message(client, userdata, msg): print(f"Topic: {msg.topic}, Payload: {msg.payload.decode()}") client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) client.on_connect = on_connect client.on_message = on_message client.connect("broker.example.com", 1883, 60) client.loop_forever()

JavaScript(MQTT.js)

javascript
const mqtt = require('mqtt'); const client = mqtt.connect('mqtt://broker.example.com:1883'); client.on('connect', () => { console.log('Connected'); // 单级通配符:所有设备的温度 client.subscribe('sensors/+/temperature'); // 多级通配符:卧室所有数据 client.subscribe('home/bedroom/#'); // 组合通配符:所有服务的指标 client.subscribe('system/+/metrics/#'); }); client.on('message', (topic, message) => { console.log(`Topic: ${topic}, Payload: ${message.toString()}`); });

Java(Eclipse Paho)

java
import org.eclipse.paho.mqttv5.client.MqttClient; import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence; MqttClient client = new MqttClient( "tcp://broker.example.com:1883", "java-client-" + System.currentTimeMillis(), new MemoryPersistence() ); client.setCallback(new MqttCallback() { @Override public void messageArrived(String topic, MqttMessage message) { System.out.println("Topic: " + topic + ", Payload: " + new String(message.getPayload())); } // ... 其他回调方法省略 }); MqttConnectionOptions opts = new MqttConnectionOptions(); opts.setAutomaticReconnect(true); client.connect(opts); // 单级通配符 client.subscribe("sensors/+/temperature", 1); // 多级通配符 client.subscribe("home/bedroom/#", 1); // 组合通配符 client.subscribe("system/+/metrics/#", 1);

主题设计建议

通配符好不好用,很大程度上取决于你的主题结构设计。一个设计良好的主题结构能让通配符发挥最大价值,而一个糟糕的主题结构会让通配符变得鸡肋甚至无法使用。

保持层级语义一致。 主题的每一层都应该有明确的含义。building/floor1/room2/temperature 这种结构中,每一层都是具体的分类维度,通配符可以精确地切入任何维度。如果主题层级含义混乱(比如 data/temperature/floor1/001),通配符就很难发挥筛选作用。

层级不要太深。 3-5 层是最佳范围。层级过深会增加匹配开销,也让订阅规则变得难以阅读和维护。

避免在层级中使用特殊字符。 主题层级中不要包含 +#/ 这些保留字符,也不要使用空格和通配符本身。MQTT 规范虽然没有禁止在主题内容中使用这些字符,但它们会干扰通配符的匹配逻辑。

统一命名风格。 全部用小写、用下划线或连字符连接单词,不要混用 camelCase 和 snake_case。sensor/temperature/living_roomsensor/Temperature/LivingRoom 更不容易出错。

为通配符预留扩展空间。 设计时考虑未来可能新增的层级。比如当前主题是 device/{id}/status,未来可能需要按区域分组,那不如一开始就设计成 region/{region}/device/{id}/status,这样 region/east/# 这样的订阅就有意义了。

MQTT 主题通配符本质上是一种模式匹配机制,+ 匹配单层级,# 匹配多层级,两者都只能用于订阅。理解它们的匹配规则和使用限制是正确使用 MQTT 的前提,而良好的主题结构设计则决定了通配符在实际项目中能发挥多大的价值。

标签:MQTT