MQTT v5.0 协议 Swift语言实现与分析

引言

MQTT协议是IBM提出的一种通信协议。主要应用场景:Internet of Things、Instant Messaging

传输层协议要求

MQTT协议要求底层的传输层协议提供一个有序、无损的字节流。
典型支持的协议有:

  • TCP
  • TCP over TLS
  • WebSocket
    很显然不支持的协议有:
  • UDP

固定头部 Fixed Header

数据包类型

首字节被拆分为高4位和低4位。
高4位一共16种可能性,分配给了全部16种数据包。
第4位除了发送消息的PUBLISH数据包外,都是固定数值。

MQTT数据包长度

MQTT数据包的长度是不固定的,因此你无法通过读取固定字节的方式获取到完整的数据包。
每个数据包的(1...x)个字节开始为固定头部
其中,第1个字节用于标记数据包类型和特殊标记,我们可以设计一个分类器,仅读取第1个字节就将数据包发往对应的分类做进一步处理。
(2...x)字节,就是数据包的剩余长度。它的计算不包括固定头部本身,如果这个字节的内容是n,那么数据包的总长度就是固定头部.count + n

你应该已经留意到(2...x)这种奇怪的表述。这是因为剩余长度占用的字节数也是不·固·定的!为此,MQTT规范在第一章专门引入了一系列特殊的数据结构,用于让数据本身自包含它的长度信息。
举例来说,剩余长度的类型是变长整型。相比普通的Int,它只用每个字节的低7位存储数值。最高位若为1则表述下一个字节也是它的一部分,若为0则表示已经到达数值末尾。大概效果是这样的:
0b1xxx_xxxx
0b1xxx_xxxx
0b1xxx_xxxx
0b0xxx_xxxx
上面这个变长整型的值就是7x4=28个bit拼成的整型。最高位的标记和String的\0标记异曲同工。

实现与分析

在封包时,我们应该先完成首字节和剩余部分的Data数据构建,然后计算出剩余部分的长度,最后将这3部分拼接为一个完整的数据包。
在解包时,我们可以读取首字节获取数据包类型和特殊标记,然后根据第2个字节读取剩余部分,再交给对应的解码器生成对应的Object。

剩余部分

剩余部分自成一体。它也有自己头部可变头部,以及身体负载。从某种意义来说,它也可以被理解为一种数据包。纵观计算机网络协议俄罗斯套娃式的嵌套,这种做法很常见。

可变头部 Variable Header

可变头部分为两部分。

  • 数据包标识符Packet Identifier只在PUBLISH (where QoS > 0), PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK这9种数据包中出现。
  • 属性Properties只在CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBACK, DISCONNECT, AUTH这12种数据包中出现。

数据包标识符

数据包标识符只是一个双字节的整数,没什么要说的。在MQTT中,固定字节的整数就是普通的整数,你可以用UInt8、UInt16、UInt32来表示它。唯一需要注意的是,网络数据包都是"大头党",拼接数据是稍作留意就好。(豆腐脑我吃咸的)

属性数组Properties

一言以弊之:var properties: [Property] = []
遗憾的是,实际上你并不能这么写。

属性数组的长度

你应该已经猜到了,像数组这种不固定长度的数据结构,当然是计算数据长度以后再拼接的。

单个属性Property

每个属性分为2部分,

  1. 变长整型的identifier
  2. 具体数据
    先说说这个identifier。目前一共有42种属性,从0x01一直排到0x2A,距离用满单个字节的低7位0x7F都还早着呢。因此目前实际上这个identifier和UInt7是一样的,不过为了防止今后MQTT v10 Plus突然加了几百种属性,代码上还是保持向后兼容为好。
    然后再说这个具体数据。这里的具体数据就相当于Property的Payload了,它是什么类型、有多长可就要了命了。你必须对照MQTT规范逐个查表去看。对于Swift这种强类型的语言来说,不要指望弄个enum就能让编译器自己查。如果我遗漏了某个Swift高级语法特性,请务必告诉我。
    由于这个原因,我的代码是这个样子的:
propertiesData += MQTTProperty<UInt32>(.sessionExpiryInterval, value: sessionExpiryInterval).mqttData

它的丑陋,让我这个作者都感到难堪,有机会一定要优化它。

负载 Payload

只有部分数据包会有Payload。在CONNECT, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK这5种数据包中是必选的。而PUBLISH中,如果要发送的数据为空,则Payload可以没有。

不同种类的数据包,负载差异很大

MQTT v5.0 16种数据包封包和解包

CONNECT数据包