前言

前段时间吐槽说:

最近在重写CodeLab Adapter(为2.0版本设计新的架构),希望成为智能/可编程空间的通用hub,也试着通过更开放的架构设计,允许任何AI系统作为可插拔组件随时接入系统。 “everything is a message"几乎可以断定是此类系统的最佳设计原则(Smalltalk 和Dynamicland都基于消息),由于问题域比较独特(支持最终用户编程的开放生态),可供参考的资料很少,翻了一圈物联网领域的相关协议,发现落地的协议里对名词和动词的使用、命名空间的使用,一片嘈杂(症状就是今天这类系统兼容性非常糟糕),困难都不在硬的技术问题上,而出现在语义上,以及领域词汇的组织形式上,这类问题强迫技术人员用词汇去描述现实世界。可以看出,如果不出现一个维特根斯坦,消息系统永远会混乱下去,它太过于灵活而让人无所适从了。
《SICP》序言里有一段很有意思的表述:“计算机科学并不是一门科学,而且其重要性和计算机也并无太大的关系。计算机革命是有关我们如何去思考的方式以及我们如何去表达自己的思考的一个革命”。 第一次读这段话的时候,觉得这不是装x吗,说得跟玄学似的。设计消息系统的时候,由于消息可以构建出任何语义,于是设计者就被抛到空无一物的境地,这时候就像《SICP》表述的:被迫去表达自己所理解的那个目前还不存在的逻辑系统。而这件事本质上与计算机并没有什么关系。


本文关注MQTT topic与payload的设计相关问题。

对任何使用topic/payload的消息系统(基于发布/订阅模式)而言,消息结构的设计都是极为重要的。尽管我们可以选择任何一个消息协议来关注,但我偏向选择MQTT来讨论。之所以选择MQTT协议来讨论,是因为物联网领域高速发展,有许多基于MQTT的优秀的开源项目/落地项目。它们实际运行在现实世界里,已经经过多次迭代,优化后的消息结构,已经是比较好的应对现实问题的模型。

在此并不讨论MQTT协议本身,而倾向于关注具体行业里的应用所对应的消息结构的设计。

消息设计的难题

MQTT(Message Queuing Telemetry Transport)是基于发布/订阅(sub/pub)范式的消息协议。

MQTT 客户端(client)可以订阅(sub)自己关注的topic,之后将接收到属于这个topic的消息,消息内容为payload。

听起来很简单啊。topic是消息的主题(title),订阅者选择自己感兴趣的主题进行订阅,payload是消息体(body), 订阅者收到感兴趣的消息后,查看payload可以得知细节信息。

困难在于,这近乎是一个万能的结构,一张可以绘制任何图案的白纸。近乎无限的自由度带来的困难就是你需要利用topic/payload这两块积木,设计出合理的消息结构,用作解决领域问题的模型。大多数的技术工作要比这个简单,它们要求你在一个既定框架里工作,只要好好填空就行了。

与此类似的困境,在web领域也有: REST API的设计问题, 关于如何使用动词和名词,关于POST json数据的结构设计。技术人员在此被迫面对设计问题,这些恰恰是我们不擅长的。

REST API已经有了很多好的最佳实践,来自许多优秀的落地项目(Github API/heroku API/…), 但在MQTT许多领域(诸如智能家居),依然一片混乱。而且相关的讨论也远不如REST API多。

问题举例

MQTT multiple topics vs. bigger payload这个问题试图询问 mqtt消息 topic和payload设计的最佳实践。

提问者对此表示疑惑: 在topic中尽可能多携带信息好呢?还是在payload里尽可能多携带信息好?以下两种结构携带的信息是一样的,但结构全然不同。

1
2
3
plant1/machineA/sensorX/temperature/value 20
plant1/machineA/sensorX/temperature/unit C
plant1/machineA/sensorX/temperature/timestamp 2018-08-01T12:00:30.123Z

1
2
3
4
5
6
7
8
plant1/machineA/
{
  ["sensorX": {
   "value": 20,
   "unit": "C",
   "timestamp": "2018-08-01T12:00:30.123Z"
  }]
}

当然还有更多组合的可能性, 但是有一般的方法吗?

更多讨论

我对这个问题也感到疑惑,试着去寻找最佳实践,结果读到的东西越来越多,疑惑却并不减少。所以本文更多是呈现出问题和疑惑所在,也试着梳理一些优质讨论,给出参考资料。

Steve的博客

Steve围绕物联网写了很多优质文章。他的博客里写过几篇精彩的文章,来讨论topic与payload相关话题。这些理性的讨论让我对这个问题有了更清晰的认识,我将其视为入门引导:

下边来梳理下MQTT Topic 与 Payload设计笔记里的精彩讨论:

MQTT Topic 与 Payload设计笔记

MQTT网络由传感器之类的设备构成。它们可以:

  1. 主动发送数据
  2. 响应请求
  3. 接受指令

将简单的灯开关作为示例设备,那么至少它需要:

  1. 发布它的当前状态: 打开 或 关闭。
  2. 响应命令(更改状态)。

可以使用topic和payload来实现上述目标。

topic很灵活, 有很多可能,例如:

  • sensor1 – Topic 仅包含传感器名称
  • sensor1/status - Topic同时包含传感器名称和有关数据类型的信息,例如状态信息。

下边以电灯开关为例来具体说明。

电灯可以将它的状态信息发布到light-switch/status topic 或者 light-switch topic.

当我们选择sensor1/status风格时,payload为ON 或者 OFF.

在这种风格下,它接受light-switch/set topic的控制消息,payload也为ON 或者 OFF.

我们也可以采用另一种风格:sensor1,在payload中放置额外的信息。我们使用light-switch作为topic,

payload 为:

1
2
3
{"status":"ON"} or {"status":"OFF"}  – Data

{"set":"ON"} or {"set":"OFF"}   – Commands

需要注意的重要一点是,你始终可以选择在topic或payload中放置信息(故而你也被迫去做设计决策)。

例子: 智能家居传感器

想象屋子里有10个设备:例如温湿度传感器、电灯开关、门锁等

可以采用的topic风格如:

house/sensor1, house/sensor2 … house/sensor10

如果传感器仅发布单个值(ON/OFF,Open/Closed,Temperature等),那么我们的payload可能只包含该值。

因此,房间里电灯开关将发布:

1
2
3
topic – house/living-room-light

payload - ON or OFF

我们还可以选择发布简单的topic,并将传感器ID包含在payload中,如下所示:

1
2
3
topic – house

payload – {"living-room-light":"ON"} or {"living-room-light":"OFF"}

如何控制电灯开关?电灯开关需要接收命令。创建一个主题来接收命令。可以使用以下topic结构:

1
2
topic - house/living-room-light/cmd
payload - {"SET":"ON"} or {"SET":"OFF"}`

也可以是

1
2
3
topic – house 

payload - {"living-room-light":{"SET":"ON"}} or  {"living-room-light":{"SET":"OFF"}}

topic命名方案

考虑信息放在哪儿时,

  1. 在topic中输入尽可能多的信息
  2. 在payload中添加尽可能多的信息

topic的结构可以包括:

  1. topic 分组设备. - 可选
  2. 传感器名称 – 可选
  3. 功能,例如status,set,get,cmd - 可选

payload可以包括:

  1. Payload 数据
  2. Sensor ID - 可选
  3. 功能,例如status,set,get,cmd – 可选

需要注意的是,目前没有标准的topic命名方案或消息格式, 也不存在正确或错误的方式。

Github上有一个很有用的提议,推荐阅读。

不同的影响

尽管你可以按照自己的喜好设计topic,应当注意的事,不同的设计会到来不同的影响,除了影响语义的清晰性,也可能影响系统性能。

回到前头屋子里有10个传感器的例子。

在风格house/sensor01... house/sensor1中,house/sensor01消息只发给sensor01(只有一个订阅者),

在风格house中,10个传感器都订阅它, payload为`{“sensor01”:“on”}的消息将发给10个传感器。每个传感器都需要检查消息以查看它是否适合自己。

因此steve提出以下建议:

建议

  • 为单个设备或一小组设备使用相同的topic。
  • 为data和commands使用单独的topic。
  • 消息payload中的数据应该是特定于设备的。
  • 消息payload中与设备的多个属性相关的数据是JSON编码的。

打包Topic数据

有时我们需要选择是在多个topic上发布,还是将数据打包放在单个topic上。正如开头那个问题里问的。

看个例子

1
2
3
4
5
Test/AppSpin/read false
Test/Position3/read false
Test/TankEmpty/read true
Test/AppOpen/read false
Test/Position7/read false

等效于单个topic下的:

1
{AppSpin_read:False,Position3/read:False,TankEmpty_read:True,AppOpen_read:False, Position7_read:False}

steve根据打包数据对网络流量进行了一些测试,参考MQTT传感器和网络流量观测

应该注意,当前的IOT / MQTT仪表板通常将数据作为JSON编码数据获取,并且很可能成为标准数据。

请参考使用Python发布和接收JSON数据的视频。

加密

使用简易topic的一个优点是可以容易地加密paylaod数据,这将保护topic信息,因为它不是公共的。请参阅加密payload

Homie公约

这是一个Github提议,试图标准化MQTT设备,使他们在MQTT网络上可被发现。

它包含设备在连接到MQTT代理时应发布的数据的详细描述。

本文档应作为设计的良好指南。

公共topic

目前,MQTT实现主要是私有的,topic命名方案由实现团队选择。

在未来,MQTT可能会部署在公共环境中,例如:

  • 发布交通信息和更新
  • 机场,火车站等交通工具的到达和离开信息
  • 商店里的信标

在这些情况下,需要在命名约定上使用某种规范,以便人们去订阅相关信息。目前尚没有提案。

命名方案示例

Owntracks是一款报告地理位置的手机应用程序,topic层次的设计在这里,消息格式的设计在这儿

todo

进一步需要做的工作包括:

  • 梳理相关的讨论和提案
  • 阅读已有的优秀项目的设计
  • 研究一个成熟的产品/生态
    • 米家
    • homekit
    • 飞比

参考