MQTT主题树设计的最佳实践、技巧和实例

通用的MQTT背景

通过MQTT,发送方和接收方不知道对方的存在--经纪人处理消息传递。这允许消息在空间、时间和强度上分开。发送方可以以它想要的速度和时间发送。接收者可以以它想要的速度和时间来接收信息。发送方和接收方不需要认识对方,也不需要在他们之间建立直接的路线(如电话线)。MQTT允许轻松设计多播设置--一个发送方可以向许多订阅的接收方发送。

  • 客户端不知道最初是谁发布的消息--除非从主题(可能通过ACL强制执行),或从消息内容(可能用HMAC签名)中可以看出。
  • MQTT是为低功耗、低延迟的通信而设计的(HMAC等可能很贵)。
  • 你不可能从经纪人那里得到所有主题的列表。经纪人会丢弃没有人订阅的消息(和主题)(可能只有在没有设置保留标志的情况下才完全正确)
  • 你不知道谁订阅了某个主题(使用ACL来限制未授权客户的订阅)。

遗愿和遗嘱/保持生命

客户端可以连接,发布消息并断开连接,或者与服务器保持持久连接。在持久连接的情况下,将发送keep-alives。如果客户端没有在连接建立过程中约定的时间范围内发送keep-alives,服务器(经纪人)可以发送客户端在连接建立过程中提交的所谓最后的意愿和遗嘱消息。这个消息可以是任何东西,可以被发布到 每个连接有一个单一的主题.经纪人将把它发送给所有订阅这个主题的客户。

如果客户端优雅地断开连接,最后的意愿和遗嘱将不会被公布。

保活功能对移动客户端特别有用,因为连接可能会中断(例如到坏的网络,隧道等)。

另见。

http://www.steves-internet-guide.com/checking-active-mqtt-client-connections/

http://www.steves-internet-guide.com/mqtt-last-will-example/

https://owntracks.org/booklet/tech/mqtt/

客户ID/客户接管

每个连接到MQTT代理的客户都应该有一个唯一的客户ID。以相同的客户端ID连接的客户端会杀死之前的连接并接管新的连接。如果你有两个客户端具有相同的客户端ID,这可能会导致连接断开的乒乓现象。

保留的信息

ǞǞǞ 最后一次 保留给一个主题的信息被保存并传递给这个主题的所有新用户。这可用于交流设备状态/配置/等。

你把保留状态设置为每条信息的标志。

保留的信息可以用于许多有趣的事情,例如,缓解自动发现(见下文建议4)。

服务质量/QoS

服务质量有三个级别,它们决定了在处理信息过程中遇到的开销有多少。级别越高,开销越大。

  • 0:没有任何保证
  • 1:信息将至少被发送一次(可能出现重复)。
  • 2:信息将被准确发送一次(不可能有重复的信息)。

QoS可以在发送给经纪人的每条消息和订阅期间设置。这两个值中较低的值将被用于实际的消息传递。

请看一下这个,了解更多细节。

https://www.eclipse.org/paho/files/mqttdoc/MQTTClient/html/qos.html

课题

关于主题的事实

主题名称是区分大小写的,并且是UTF-8字符串。建议将自己限制在可打印的ASCII字符上,以方便调试。

主题名称必须至少由一个字符组成才有效。

/

已经是一个主题名称

ǞǞǞ $SYS 该主题是用于经纪人的使用/统计。

你可以选择将信息放到主题结构中,或者将其放到消息中。消息的JSON结构将帮助你能够用进一步的数据来扩展它。

考虑到在每次对经纪人的调用中,每次发布的主题都会在MQTT数据包的有效载荷中一起发送--因此要尽量避免不必要的长度。

考虑使用通配符

  • # 匹配从当前级别开始的任意级别深度的所有内容 (多级通配符,只能在最后插入)
  • + 匹配当前水平的一切 (单级通配符)

例如,如果你有多个温度传感器,如果你正确设计你的主题树,你可以很容易地订阅所有的信息。

野生卡只是用来订阅的。 他们是不允许发表的。

你可以用以下方式订阅所有主题 #

除了$SYS主题。使用$SYS/#来订阅$SYS的所有主题

可能,在设备标识符之前使用 "功能 "部分,可以让你更容易订阅某些频道。

例如,如果你将你的路径定义为 电话/房间1/传感器名称1 电话/房间2/传感器名称2 等和 stat/room1/sensorname1 stat/room1/sensorname2 订阅是可能的。

tele/#

stat/#

如果你反其道而行之,用 传感器1/stat 传感器2/stat,等等,你可以使用

+/stat

来订阅统计资料,但不会像前面的例子那样在任意的深度上进行匹配。也就是说,通往你的传感器的路径必须精确地由一个组件组成。

这样一来,如果你的行动前缀,你的路径将更加一致。

例如见 https://stevessmarthomeguide.com/setting-up-the-sonoff-tasmota-mqtt-switch/

请勿

- 不要使用前导斜线(如/mytopics)来开始你的主题树--它增加了开销,没有价值。

- 请勿使用 $SYS 作为起始主题,这是为经纪人保留的。

- 请勿在主题中使用空格

- 请勿在主题中使用不可打印的字符

- 对具有不同意义的消息使用一个MQTT主题 - 为它们创建不同的主题

建议/做法

- 将客户ID嵌入主题树中

- 为单个寻址(如传感器)创建特定的主题,而不是通过一个主题发送所有的值------。 这将避免向不需要的收件人发送邮件。

- 尽量保持简短的主题名称(因为它们将被发送到每一个发布和信息中)。

- 考虑哪些数据需要多长时间发送一次

o 例如,考虑将元数据分离到它自己的主题中,并以 "保留 "标志发布。

- 分离出设备的命令/cmd和状态主题--这样就可以进行双向通信,而设备也不必过滤它自己的信息。

- 使用最后的意志和遗嘱信息来表示一个意外的客户断开连接

考虑多租户和桥接

你是否需要支持多个客户/组织/独立的应用程序?有可能使用,例如VerneMQ,不同的装载点。另一个选择是在你的主题树中包括多租户(通过在主题树的开头使用一个适当的前缀)。

我更喜欢前者,因为它提供了一个额外的ACL分离层--并且减少了 "搞砸事情 "的机会。

此外,你可能想把几个MQTT经纪商连接在一起,共享他们的部分主题树(由他们的ACL定义)。

关于桥接的更多信息,请看这个https://owntracks.org/booklet/guide/bridge/。

考虑加密

MQTT可以在加密的网络上运行(例如通过websockets/TLS加密),也可以在未加密的通道上运行。此外,你可以考虑对数据的有效载荷进行加密和/或签名。

考虑可发现性

考虑建立有助于发现你的设备的服务和能力的主题,例如,通过使用保留信息。

考虑关于命令的反馈

当你发布一个命令时,你没有一个即时的回馈渠道。因此,建立一个关于特定命令的具体反馈的层次结构可能是有用的--设备是否收到并能够执行命令,以便能够向用户提供反馈。

我想在这里应用的一种模式是将独特的命令ID作为消息有效载荷的一部分,在反馈通道中用状态代码进行引用。

考虑组播情况

你想让几个设备对你发布的一条信息作出反应吗?记住,你不能向通配符主题发布--只能订阅它们。在这种情况下,应该创建额外的主题,允许客户端对多播情况做出反应。所有客户端/某个特定组的所有客户端都将订阅组播主题并对消息作出反应。

如果你把这和我上面的反馈/参考命令ID的建议结合起来,在(通用)反馈通道上就会引入客户ID作为有效载荷的新要求。另一种可能性是让设备在它们各自的反馈通道上做出反应--设备ID已经是路径的一部分。(也就是说,它们在组播路径上收到命令,但在它们自己独有的消息主题层次中进行回复)。

考虑流量和隐私,传感器类型

如果你正在运行一个商业系统,例如为用户收集传感器数据,他们可能希望能够在一定程度上控制哪些传感器的值被公布。

此外,自动简单地发布所有传感器读数将增加你的服务器的流量和负载。

因此,简单地将所有可用的传感器值推送给你的MQTT代理可能不是最佳选择。一种可能性是创建一个保留的主题,有一个客户端可以订阅的配置。当客户端连接时,它将自动收到保留主题上的设置,并可以根据设置调整其发布的值。此外,它还可以对配置的变化作出反应,对到达这个配置主题的任何额外消息作出反应。

此外,重要的是要考虑到存在不同类型的传感器。例如,对于一个门的传感器,你会对状态变化(关闭与打开)感兴趣,而对于一个温度传感器,你可能想接收连续的读数。对于你的设计来说,重要的是要考虑当负责传感器读数的客户端断开连接时,订阅者将收到什么样的值。记住,你只能在一个主题上设置最后的意愿和遗嘱(对于多传感器设备)。在断开连接的情况下,保留的信息可能会给人一种错误的印象(例如,门是开着的,而实际上是关着的,但没有被更新,等等)。

另一方面,例如,如果你的控制客户端的网络界面是在门-传感器客户端连接了一段时间后才连接的,而你使用了非保留的消息,那么最后一个门状态更新的消息将被错过。

这里有两种可能性。

  • 主动要求更新门的状态(例如,通过使用你自己的get topic)。
  • 使用保留的信息,并使用webinterface客户端逻辑,在客户端本身没有连接的情况下,丢弃最后一个值。 标记为陈旧的

在任何情况下,都应该让用户知道传感器的读数是否可以信任,或者是否由于传感器-客户端断开连接而可能不正确。

另见 http://www.steves-internet-guide.com/mqtt-sensors-traffic-observations/

  • 史蒂夫指出,在一个JSON编码的有效载荷中结合来自几个传感器的数据并不会减少流量,甚至会稍微增加流量(与非编码的有效载荷相比!)。

语义与物理主题路径

对人类有意义的事物创造了主题结构。("语义法")

家庭/浴室/湿度

设备路径(传感器连接到哪些设备上,如何寻址?)创建主题结构。("物理方法")

pi/00000234898324734/i2c/10/pressure

(这些数字分别象征着BCM序列和I2C地址)。

再版

在这个想法中,你将结合两种方法,并创建一个额外的服务,在两个世界之间进行转换。

见。 https://tinkerman.cat/post/mqtt-topic-naming-convention

在线设计建议概述

建议1:覆盆子谷地

从。 https://raspberry-valley.azurewebsites.net/MQTT-Topic-Trees/

设备类别/设备ID/支付宝语境/支付宝-区分器

  • 设备类别:例如,"pi"、"arduino "等。
  • 设备ID:如BCM的序列号
  • 有效载荷-背景:如温度
  • 有效载荷-区分器:例如,前面/后面(为特定的环境增加一个测量级别)

注意设备类别/设备ID和有效载荷上下文/有效载荷区分器之间的对称性(通用->具体;通用->具体)。

建议2:史蒂夫

(作者:Steve)。 http://www.steves-internet-guide.com/mqtt-topic-payload-design-notes/

你可以在你的主题树中包括以下项目。

  • 高水平的主题分组装置
  • 指定的传感器名称
  • 功能(例如:设置/状态/获取/cmd )

史蒂夫的做法。

  • 用主题名称表示个别设备(如传感器)。
  • 使用有效载荷/JSON数据作为该传感器的属性
  • 为数据和命令使用单独的主题

建议3:MQTT-Smarthome

MQTT智能家居建议

https://github.com/mqtt-smarthome/mqtt-smarthome

在这个建议中,路径看起来是这样的。

toplevelname/function/item

knxgateway1/status/Kitchen/Lights/Front Left

  • 第一层(knxgateway1)= toplevelname,被寻址的具体网关
    • 对于多个类似的网关,这个名字必须可以设置,以避免命名空间的碰撞。
  • 第二层(状态)=功能
  • third level, and further levels -> individual address hierarchy of the particular gateway (item)

有趣的是函数,以及它们的位置 之前 更深层次的路径(考虑用通配符订阅--这样你就可以轻松订阅所有的设备状态报告!)。

本提案中的可用/定义功能。

  • status - 用于获取状态报告
  • 设置 - 用于请求改变状态
  • get - (可选)用于主动请求状态更新(对于支持该功能的网关)。
    • 读取的结果将被发布到 身份 等级制度。

各个动词的后续层次应该相互匹配,这样你就会对同一个设备/节点/参数进行寻址。

每个网关有一个特殊的主题 "连接",它允许客户设置一个最后的遗嘱信息

顶级名称/连接的

为这个主题定义了简单的值。

  • 0 = 与经纪商断开连接(设置为最后意愿)
  • 1 = 连接到MQTT,与硬件断开连接
  • 2 = 连接到MQTT和硬件(完全运行)。

请注意,数值0并不区分自愿断开连接或失去连接(keep-alives超时)的情况。

对有效载荷也有一些建议。

有效载荷的JSON编码是可选的。

在JSON编码的情况下,值将总是在键 "val "中。此外。

ts : 时间戳(时间戳,自大纪元以来的毫秒数)。

lc:数值的最后变化(时间戳,自Epoch以来的毫秒数)。

clip_image001

建议4:Tinkerman

https://tinkerman.cat/post/mqtt-topic-naming-convention

考虑你是想要一个 "语义 "方法(例如对人类有意义的东西)还是一个 "物理 "方法(例如你的系统所连接的实际设备路径)。

物理方法对机器更友好。

(请注意,Tinkerman和我对挂载点及其用法的理解有偏差--对我来说,挂载点是完全分离的主题树,例如用于多租户的。)

元数据可以被编码为后缀路径。

比如说

/home/bedroom/temperature -> 21

/home/bedroom/temperature/units -> °C

/home/bedroom/temperature/timestamp -> 2012-12-10T12:47:00+01:00

注意:由于单位和其他一些元数据不太可能改变,它们可以作为保留主题发送。

这个建议的一个有趣部分也是聚合(设备方面)。

/home/bedroom/temperature/last -> 最后一个温度值

/home/bedroom/temperature/last/timestamp

/home/bedroom/temperature/last24h/max -- 过去24小时内的最高值

/home/bedroom/temperature/last24h/max/timestamp -> 温度达到这一高度的时间戳。

/home/bedroom/temperature/last24h/min

/home/bedroom/temperature/ever/max -- 有史以来测量的绝对最高温度

同样,这些值可以被设置为保留的主题(特别是 "曾经"),以节省网络流量--接收系统可以丢弃旧的保留值(如果传感器断开连接,应该由另一个主题来证明--可能由最后的遗嘱和意志来设置),或者系统可以将它们显示为 "过时 "的值。

这是一个有趣的设计(对于聚合),可以作为允许这种灵活性的一个灵感。

建议5:Homie IoT公约

https://homieiot.github.io/

Home IoT专注于物联网家庭自动化设置中节点的可发现性。Homie不使用JSON编码的信息,它使用每个节点的有效载荷的直接表示。请看这个例子。

clip_image003

Homie定义了一个严格的字符子集,你可以用它来命名你的节点,因为特殊字符(如$和下划线_)是用于特殊用途的。

它定义了数据类型,这些数据类型作为(保留的)元数据("属性")发布给单个属性的$datatype(关于属性的定义见下文)。

  • 字符串
  • 整数
  • 浮动
  • 布尔型
  • 枚举
  • 颜色

(注意$的使用,以标记特殊主题)。

最后的意愿是用来将设备标记为丢失。

乡亲/deviceID/$state -> lost

Homie定义了6种可能 国家 为该设备。

  • init -> 设备已连接到MQTT,但还没有发布所有必要的Homie消息
  • 准备好了 -- -- 设备已连接,并已完成设置 -- -- 所有必要的信息都已发送。
  • 断开 -> 设备以一种干净的方式断开连接
  • 睡觉->自述
  • 丢失 -> 设备意外断开(由最后的意愿和遗嘱设定)。
  • 警报 -> 设备已连接,但有问题,需要人工干预

Homie建议使用 服务质量1 (因为Homie的性质使得它对重复的信息很安全)。

一项关于 扩展 是使用反向域名语法,例如org.mycompany.homie-extension

节点和设备

在Homie-parlance,一个 设备 是基本的硬件单位。例如,Raspberry Pi、咖啡机、汽车。

A 结点 是设备的一个逻辑上的、独立的单元。例如,一辆汽车可能有一个车轮节点、一个发动机节点和一个车灯节点。

财产 代表节点/设备的基本特征。例如,一辆汽车的发动机节点可以有一个 "速度 "和一个 "温度 "属性。属性可以被保留和/或设置。

  • 保留+不可设置:如温度传感器
  • 保留+可设置:节点可以发布一个属性并接收该属性的命令(如灯的功率)。
  • 非保留的+不可设定的:如门铃(瞬间事件)。
  • 非保留+可设置:节点可以接收属性的命令,例如,冲泡咖啡。

属性。

这些属性对Homie的自动发现很重要,它们被指定,并以$开始,以表明它们的特殊意义/避免碰撞。它们被用来存储和更新元数据。

例如,设备在下列地点发布保留信息

homie/设备ID/$nodes

用逗号分隔的节点列表。为了表示一个节点是一个数组(可以用来分组,例如汽车的前灯和后灯),名称在最后用矩形括号指定,例如

灯光[]

一个关于设备ID子树的例子。

clip_image004

Homie为设备指定了不同的统计数据,其中大部分是可选的(除了正常运行时间)。比如说。

clip_image005

对于节点来说,通过在$properties属性中指定属性,同样可以实现属性的自动发现。

clip_image006

这样,作为订阅者,你不必猜测/订阅所有的事件--你可以选择性地订阅你感兴趣的事件。

在Homie中,元数据(属性)是作为路径的后缀发布的。例如,对于一个特定的属性。

clip_image007

控制在Homie中

Homie是基于状态的。这意味着,你不是 "把灯打开",而是把一个设备的电源状态设置为开启。

clip_image008

那么设备就会更新它的电源状态,以表明该命令已经被执行。

clip_image009

广播

clip_image010

警报是一个任意的选择--广播可以是任何其他主题,设备可以过滤。消息被广播给所有的设备--如果设备愿意,它们可以做出反应。

这是我在网上研究中能够找到的最完整的规格。我个人觉得它的设计非常优雅。

阅读更多。

https://homieiot.github.io/specification/

建议6:Owntracks

https://owntracks.org/booklet/tech/json/

http://owntracks.org/booklet/guide/topics/

clip_image012

Owntracks是一个开源的应用程序,允许你跟踪自己的物理位置,与朋友分享等。位置是通过HTTP或MQTT发布的。 最好是对自己的经纪人.

"它还可以检测到你何时进入或离开一个特定的区域,你为该区域设置一个所谓的航点。人们利用这一点,比如说,控制他们家庭自动化系统的某些方面。(大家都离开家了吗?我们可以把灯关掉。)"

如果你想了解更多关于OwnTracks的信息,请访问这个网站:https://owntracks.org/booklet/

关于owntracks的主题设计。

https://owntracks.org/booklet/guide/topics/

"设计OwnTracks主题命名方案时的原则是

  • 人类的可读性
  • 流量最小化
  • 细致的访问控制

基本主题名称(其他也可以)。

owntracks/peter/iPhone

这里owntracks是一个前缀,以允许其他应用程序在MQTT代理上没有碰撞。

peter是追踪设备的所有者,而iPhone是他的设备之一。

对设备的命令是在:owntracks/peter/iPhone/cmd上发送的。

命令的输出被发布,例如,发布到相对的主题名称上。 阶梯, 倾倒 等。

其他一些主题被定义(信息、航点、事件)。例如,当用户进入一个定义的区域时,就会发布事件。

这样,订阅owntracks/+/+/event将使你能够看到(对于你授权的用户)他们进入或离开一个定义的区域。

Owntracks以JSON格式发布它的信息。

https://owntracks.org/booklet/tech/json/

clip_image013

你可以看到,这些功能是 之后 该设备。

由于事先知道用户/设备有一个固定的 "深度",所以可以用通配符来订阅多个用户和设备。

clip_image014

在JSON中, 类型 的特定JSON在消息中被传递。

clip_image015

这是一个很好的主意!这样,你可以验证你的应用程序是否正确理解了该事件,可以进行类型检查,等等。

各个元素是以非粗略的方式指定的。这有助于节省流量,特别是对于那些将被频繁发布的信息。

如:

  • acc:报告位置的准确性,以米为单位(无单位)。

许多元素是可选的。

有趣的是,位置类型是为不同的设备定义的,它们支持不同的元素。

一个简单的遗嘱信息被发布。它只包括一个时间戳tst,即设备第一次连接的时间。

clip_image016

tst是一个Unix纪元的时间戳。

clip_image017

关于这个事件类型/有效载荷类型的一个有趣的地方是,指定了几个时间戳。

  • wtst:创建航点的时间戳
  • tst:事件发生的时间戳。

一个航点是一个兴趣点,例如你的车库--如果你进入或离开你的车库,将触发一个过渡事件。

  • 事件。(进入|离开)

clip_image018

一个配置事件。如果启用,应用程序也接受远程配置信息。

这个事件包括很多可能的元素,这些元素比较啰嗦(例如validatecertificatechain)--这是明智的,因为配置可以存储为JSON(!->这可以作为MQTT消息传递和存储!),而且也因为它不经常发送,啰嗦并不是一个大问题,而是对用户的一种优势。

剪辑_图像020

例如,一个带有 "action": "dump "的动作将触发配置信息发布到相对路径dump上。

可以为一个行动提供可选的子参数,例如,要查看的时间范围。

clip_image021

另一个有趣的设计理念是,通过这个cmd信息的某种变化,可以打开一个浏览器到某个URL。例如,如果你进入车库,一个车库控制网站可以被打开,...

clip_image022

一个 阵列 可以/必须在这里作为参数传递不同的消息类型。

clip_image023

这个消息的有趣之处在于,它还有一个Base64编码的PNG图像字段。

clip_image024

clip_image025

这里有趣的想法是,一个可选的 创作人 项目可以被传递,它可以识别创建这些航点的实体。

还请注意,owntracks使用下划线_表示 "特殊 "参数。

clip_image026

可以选择所有这些信息都可以是 加密的 以共享对称密钥进行传输。

加密的有效载荷(原始信息)将在 "数据 "元素中,以Base64编码。

留言

消息的有效载荷是二进制的,可以是任何东西,不要求消息是有效的UTF-8。

因此,你也可以发布数字数据,例如图像,作为某些主题的有效载荷。

目前许多物联网仪表板都采用JSON编码的数据。

一般来说,似乎有两个学派:消息实际上是原始值,元数据被编码在主题树上,以及JSON编码的值,作为有效载荷携带元数据。

建议

  • 使用JSON格式的消息有效载荷
  • 将属性捆绑在信息中,而不是为它们创建单独的主题
  • 包括。
    • 有效载荷数据
    • 传感器/设备ID(如果不是主题树的一部分)。
    • 功能
    • 时间戳

有趣的资源

Awesome MQTT: MQTT相关链接的集合。

  • https://github.com/hobbyquaker/awesome-mqtt

个人项目。

twitter-to-mqtt - 一个使用Twitter Streaming API访问推文并将其重新发布到MQTT主题的python守护程序。

fritz2mqtt - 将FRITZ!Box连接到MQTT。

参考文献。

重新出版。

Marine使用的信号K(作为JSON格式的可能灵感)。