主题被用来标识和区分不同的消息,它是 MQTT 消息路由的基础。发布者可以在发布时指定消息的主题,订阅者则可以选择订阅自己感兴趣的主题来接收相关的消息。
一、什么是主题?
MQTT 主题本质上是一个 UTF-8 编码的字符串,是 MQTT 协议进行消息路由的基础。MQTT 主题类似 URL 路径,使用斜杠 / 进行分层:
下面以设备
//设备端
product/deviceId/event/property/post // 发布 设备属性上报
product/deviceId/service/property/set // 订阅 设备属性设置
product/deviceId/service/property/set_reply // 发布 设备属性设置_响应
//服务端
product/+/event/# // 订阅 设备属性上报
product/+/service/# // 订阅 设备属性上报
product/deviceId/service/property/set // 发布 设备属性设置
MQTT 主题不需要提前创建。MQTT 客户端在订阅或发布时即自动的创建了主题,开发者无需再关心主题的创建,并且也不需要手动删除主题。
下图是一个简单的 MQTT 订阅与发布流程,服务器订阅【djakmc/+/event/# 】主题后,将能接收到设备85236985 发布了【djakmc/85236985/event/property/post】 主题消息,而设备85236986订阅【djakmc/85236986/service/property/set】主题后,将能接收到服务器发布了【djakmc/85236986/service/property/set 】主题消息。
二、主题通配符
在MQTT实质运用场景中,当服务器需要订阅来自无数设备的消息,且每个设备都发布到一个唯一标识的topic时,确实会导致服务器需要订阅大量的topic。然而,这种直接的一对一订阅模式在设备数量庞大时可能不是最高效或最可维护的解决方案。
当设备的topic遵循一定的模式时,可以利用MQTT的通配符+(表示单个层级)和#(表示多个层级)来订阅一组topic。例如,如果所有设备的topic都以{product}/{deviceId}/event/property/post的形式发布,那么服务器可以订阅djakmc/+/event/# 【注意:djakmc 系统分配的product key标识设备品类】来接收所有设备的状态更新。
通配符
MQTT 主题通配符包含单层通配符 + 及多层通配符 #,主要用于客户端一次订阅多个主题。【注意:通配符只能用于订阅,不能用于发布】
单层通配符
加号【+】是用于单个主题层级匹配的通配符。在使用单层通配符时,单层通配符必须占据整个层级,例如:
+ 有效
djakmc/85236985/event/property/+ 有效
djakmc/+/event/property/post 有效
djakmc/85236985+/event/property/post 无效(没有占据整个层级)
如果服务端订阅了主题 【djakmc/+/event/property/post 】,将会收到以下主题的消息:
djakmc/85236985/event/property/post
djakmc/85236986/event/property/post
djakmc/85236987/event/property/post
djakmc/.../event/property/post
但是不会匹配以下主题:
djakmc/85236985
sys/djakmc/85236986/event/property/post
多层通配符
井字符号【#】是用于匹配主题中任意层级的通配符。多层通配符表示它的父级和任意数量的子层级,在使用多层通配符时,它必须占据整个层级并且必须是主题的最后一个字符,例如:
# 有效
djakmc/# 有效
djakmc/#/event/property/post 无效(不是主题最后一个字符)
djakmc/852# 无效(没有占据整个层级)
如果客户端订阅主题 djakmc/# ,它将会收到以下主题的消息:
djakmc
djakmc/event/property/post
djakmc/85236987/event/property/post
想了解更多EMQX主题的信息请查阅官方文档【EMQX主题】
三、共享订阅
在MQTT实质运用场景中,当系统采用分布式架构时,服务器需要能够处理来自无数设备的消息,但同时需要确保同一个服务的多个实例不会重复接收同一个设备发送的同一条消息。
如果没有采用共享订阅方式,确保同一个服务的多个实例不会重复接收同一个设备发送的同一条消息,则需要在服务端做分布式消息去重(可以通过redis或者MySQL)。但是会增加服务器资源、网络传输开销,增加数据处理逻辑。
如果采用共享订阅模式,服务端需要订阅【$share/deviceService/djakmc/+/event/#】。减少网络传输、服务器资源开销,提升可扩展性和可用性。
共享订阅
共享订阅是 MQTT 5.0 引入的新特性,用于在多个订阅者之间实现订阅的负载均衡,MQTT 5.0 规定的共享订阅主题以 $share 开头。【注意:虽然 MQTT 协议在 5.0 版本才引入共享订阅,但是 EMQX 从 MQTT 3.1.1 版本开始就支持共享订阅。】
共享订阅能够解决以下问题:
集群模式下,如果订阅者所在的节点发生故障,则发布者的消息会丢失(QoS 0)或者堆积在节点中(QoS 1, 2)。可以通过增加订阅节点的方式解决这一问题,但这样又产生了大量的重复消息浪费了性能,并增加了业务的复杂度。
当发布者的生产能力较强时,可能会出现订阅者的消费能力无法及时跟上的情况,此时只能由订阅者自行实现负载均衡来解决,又一次增加了用户的开发成本。
机制介绍
在原有主题的基础上,添加 $ share 前缀即可为一组订阅端启用共享订阅。
上图中,共享 3 个 subscriber 用共享订阅的方式订阅了同一个主题 【$ share/deviceService/djakmc/+/event/#】,其中topic 是它们订阅的真实主题名,而 【$ share/deviceService/】是共享订阅前缀。EMQX 支持两种格式的共享订阅前缀:
示例 | 前缀 | 真实主题名 |
---|---|---|
$share/deviceService/ | ||
djakmc/85236985/event/property/post | $share/deviceService/ | djakmc/85236985/event/property/post |
想了解更多共享订阅信息查看官方文档(EMQX共享订阅)
带群组的共享订阅
以 $ share/ 为前缀的共享订阅是带群组的共享订阅:
group-name 可以为任意字符串,属于同一个群组内部的订阅者将以负载均衡接收消息,但 EMQX 会向不同群组广播消息。
负载均衡策略与派发 ACK 配置
平衡策略可以在全局或每组中指定。
- 全局策略可以在 broker.shared_subscription_strategy 配置中设置。
- 配置 broker.shared_subscription_group.$ group_name.strategy 为每组策略。
# etc/emqx.conf
# 均衡策略
broker.shared_subscription_strategy = random
# 当设备离线,或者消息等级为 QoS1、QoS2,因各种各样原因设备没有回复 ACK 确认,消息会被重新派发至群组内其他的设备。
broker.shared_dispatch_ack_enabled = false
均衡策略 | 描述 |
---|---|
random | 在所有订阅者中随机选择 |
round_robin | 按照订阅顺序选择 |
round_robin_per_group | 在每个共享订阅组中按照订阅顺序进行选择 |
local | 随机在本地订阅中进行选择,如无法找到,则在集群范围内随机选择 |
sticky | 选定订阅者后,始终向其进行发送,直到该订阅者断开连接 |
hash_clientid | 通过对发送者的客户端 ID 进行 Hash 处理来选择订阅者 |
hash_topic | 通过对源主题进行 Hash 处理来选择订阅者 |
四、EMQX系统主题
EMQX 周期性发布自身运行状态、消息统计、客户端上下线事件到以 $ SYS/ 开头系统主题。
$ SYS 主题路径以 $ SYS/brokers/{node}/ 开头。{node} 是指产生该 事件 / 消息 所在的节点名称,例如:
$SYS/brokers/emqx@127.0.0.1/version
$SYS/brokers/emqx@127.0.0.1/uptime
$ SYS 系统消息发布周期配置项:
broker.sys_interval = 1m
集群状态信息
主题 | 说明 |
---|---|
$ SYS/brokers | 集群节点列表 |
$ SYS/brokers/$ {node}/version | EMQX 版本 |
$ SYS/brokers/$ {node}/uptime | EMQX 运行时间 |
$ SYS/brokers/$ {node}/datetime | EMQX 系统时间 |
$ SYS/brokers/$ {node}/sysdescr | EMQX 系统信息 |
客户端上下线事件
S Y S 主题前缀: SYS 主题前缀: SYS主题前缀:SYS/brokers/${node}/clients/
主题 (Topic) | 说明 |
---|---|
$ {clientid}/connected | 上线事件。当任意客户端上线时,EMQX 就会发布该主题的消息 |
${clientid}/disconnected | 下线事件。当任意客户端下线时,EMQX 就会发布该主题的消息 |
connected 事件消息的 Payload 解析成 JSON 格式如下:
{
"username": "foo",
"ts": 1625572213873,
"sockport": 1883,
"proto_ver": 4,
"proto_name": "MQTT",
"keepalive": 60,
"ipaddress": "127.0.0.1",
"expiry_interval": 0,
"connected_at": 1625572213873,
"connack": 0,
"clientid": "emqtt-8348fe27a87976ad4db3",
"clean_start": true
}
disconnected 事件消息的 Payload 解析成 JSON 格式如下:
{
"username": "foo",
"ts": 1625572213873,
"sockport": 1883,
"reason": "tcp_closed",
"proto_ver": 4,
"proto_name": "MQTT",
"ipaddress": "127.0.0.1",
"disconnected_at": 1625572213873,
"clientid": "emqtt-8348fe27a87976ad4db3"
}
客户端订阅与取消订阅事件
本事件默认为关闭状态,开启请参照sys_topics.sys_event_messages。
$ SYS 主题前缀:$ SYS/brokers/$ {node}/clients/
主题 (Topic) | 说明 |
---|---|
${clientid}/subscribed | 订阅事件。当任意客户端订阅主题时,EMQX 就会发布该主题的消息 |
${clientid}/unsubscribed | 取消订阅事件。当任意客户端取消订阅主题时,EMQX 就会发布该主题的消息 |
subscribed 事件消息的 Payload 解析成 JSON 格式如下:
{
"username":"foo",
"ts":1625572213878,
"topic":"/the/best/mqtt/broker/is/emqx",
"subopts":{
"sub_props":{},
"rh":0,
"rap":0,
"qos":0,
"nl":0,
"is_new":true
},
"protocol":"mqtt",
"clientid":"emqtt-8348fe27a87976ad4db3"
}
unsubscribed 事件消息的 Payload 解析成 JSON 格式如下:
{
"username":"foo",
"ts":1625572213899,
"topic":"/the/best/mqtt/broker/is/emqx",
"protocol":"mqtt",
"clientid":"emqtt-8348fe27a87976ad4db3"
}
客户端统计
主题 (Topic) | 说明 |
---|---|
connections/count | 当前客户端总数 |
connections/max | 客户端数量历史最大值 |
系统告警
系统主题前缀:$ SYS/brokers/$ {node}/alarms/
主题 | 说明 |
---|---|
alert | 新产生的告警 |
clear | 被清除的告警 |
系统监控
系统主题前缀:$ SYS/brokers/$ {node}/sysmon/
主题 | 说明 |
---|---|
long_gc | 垃圾回收耗时过长 |
long_schedule | 进程调度时间过长,占用调度器过多的时间片 |
large_heap | 进程内存占用过高 |
busy_port | 进程向某个繁忙的端口发送消息,进程被挂起 |
busy_dist_port | 节点间通讯使用的分布式通讯端口繁忙,进程被挂起 |
更多的系统主题可以查阅官方文档(EMQX系统主题)
五、其它
参考文档:
通过案例理解 MQTT 主题与通配符
共享订阅 | EMQX 5.0 文档
系统主题 | EMQX 5.0 文档
关注公众号【 java程序猿技术】获取EMQX实践系列文章