MQTT-第一节(协议)
日期 2021-07-09 08:50:43
阅读 12

1. 定义

  MQTT是机器对机器(M2M)/物联网(IoT)连接协议。它被设计为一个极其轻量级的发布/订阅消息传输协议。对于需要较小代码占用空间和/或网络带宽非常宝贵的远程连接非常有用,是专为受限设备和低带宽、高延迟或不可靠的网络而设计。这些原则也使该协议成为新兴的“机器到机器”(M2M)或物联网(IoT)世界的连接设备,以及带宽和电池功率非常高的移动应用的理想选择。例如,它已被用于通过卫星链路与代理通信的传感器、与医疗服务提供者的拨号连接,以及一系列家庭自动化和小型设备场景。它也是移动应用的理想选择,因为它体积小,功耗低,数据包最小,并且可以有效地将信息分配给一个或多个接收器。

2. 特点

  • 开放消息协议,简单易实现
  • 发布订阅模式,一对多消息发布
  • 基于TCP/IP网络连接,提供有序,无损,双向连接。
  • 1字节固定报头,2字节心跳报文,最小化传输开销和协议交换,有效减少网络流量。
  • 消息QoS支持,可靠传输保证

3. 应用

  MQTT协议广泛应用于物联网、移动互联网、智能硬件、车联网、电力能源等领域。

  • 物联网M2M通信,物联网大数据采集
  • Android消息推送,WEB消息推送
  • 移动即时消息,例如Facebook Messenger
  • 智能硬件、智能家具、智能电器
  • 车联网通信,电动车站桩采集
  • 智慧城市、远程医疗、远程教育
  • 电力、石油与能源等行业市场

4. 说明

  MQTT协议主要应用于物联网,智能设备等场景,目前有两个主要版本的协议,MQTT3.1.1和MQTT5.0,本篇章节主要以MQTT3.1.1协议进行分析,对MQTT3.1.1协议的概念进行梳理,并着重分析MQTT协议编码和解码的实现原理和具体算法,MQTT5.0协议和MQTT3.1.1协议的详细细节,请参考MQTT中文网。针对编码和解码的实现原理和具体算法,这里用PHP语言作为测试语言,使用SWOOLE自行搭建MQTT服务器。

5. MQTT3.1.1

5.1 数据

  • 一个字节有8个位,从0到7。位7是最高有效位,位0是最低有效位。
  • 整型数据值是16位大端序列:高阶字节在低阶字节之前。这意味着一个16位的字被放到网络上的时候,前面是最高有效位,后面是最低有效位。
  • 控制包中的文本字段被编码为UTF-8字符串。UTF-8[RFC3629]是一种高效的Unicode[Unicode]编码方式,它优化了ASCII字符的编码,来支持基于文本的通信。
  • 每个字符串都有一个两个字节的字段作为前缀,给出UTF-8编码字符串的长度。因此这种UTF-8编码的字符的大小有一定的限制,编码后不能超过65535个字节。
  • 除非特别说明,所有的UTF-8编码字符串的长度都可以是0到65535个字节。

5.2 控制包

  5.1中提到控制包中的文本字段被编码为UTF-8字符串,这里说明一下控制包的概念。通俗来讲,MQTT协议定义了信息交换的格式,这些具有特定格式的信息统称为控制包,一个MQTT控制包包含三部分,分别是固定包头、可变包头以及载荷。

Figure 2.1 - Structure of an MQTT Control Packet

|固定包头,存在于所有MQTT控制包
|可变包头,存在于某些MQTT控制包
|载荷,存在于某些MQTT控制包

固定包头

  固定包头占用1个字节,且位于传输数据的字节1的位置,我们都知道一个自己有8个位,从左到右依次是7,6,5,4,3,2,1,0位。固定包头的格式如下:

Figure 2.2 - Fixed header format

|Bit         |7       |6       |5       |4       |3       |2       |1           |0
|byte 1      |MQTT Control Packet type           |Flags specific to each MQTT Control Packet type

  其中的7-4位定义了14种MQTT控制包的类型,3-0位定义了各种控制包类型的标识,每种控制包类型对应的3-0的值不同。

类型
Table 2.1 - Control packet types

|Name           |Value          |Direction of flow                   	|Description
|Reserved       |0              |Forbidden                   		|Reserved
|CONNECT        |1              |Client to Server                       |
|CONNACK        |2              |Server to Client                       |Connect acknowledgment
|PUBLISH        |3              |Client to Server or Server to Client   |Publish message
|PUBACK         |4              |Client to Server or Server to Client   |Publish acknowledgment
|PUBREC         |5              |Client to Server or Server to Client   |Publish received (assured delivery part 1)
|PUBREL         |6              |Client to Server or Server to Client   |Publish release (assured delivery part 2)
|PUBCOMP        |7              |Client to Server or Server to Client   |Publish complete (assured delivery part 3)
|SUBSCRIBE      |8              |Client to Server                       |Client subscribe request
|SUBACK         |9              |Server to Client                       |Subscribe acknowledgment
|UNSUBSCRIBE    |10             |Client to Server                       |Unsubscribe request
|UNSUBACK       |11             |Server to Client                       |Unsubscribe acknowledgment
|PINGREQ        |12             |Client to Server                       |PING request
|PINGRESP       |13             |Server to Client                       |PING response
|DISCONNECT     |14             |Client to Server                       |Client is disconnecting
|Reserved       |15             |Forbidden                              |Reserved

  从上面可知,7-4位定义了控制包的类型,一位有两种可能,24=16,所以总共可以定义16种类型,其中0000和1111转换成十进制分别为0和15,所以0和15作为保留值,不启用,其他1-14值代表了14种不同的控制包类型。

标识

  固定包头字节1中剩下的位[3-0]包含了每个MQTT控制包类型的特殊标识,如下表Table 2,2 - Flag Bits。表中被标识为“预留”的标识位也必须赋值[MQTT-2.2.2-1]。如果收到不可用的标识,接收方必须关闭网络连接。

Table 2.2 -Flag Bits

|Control Package    |Fixed header flags     |bit3           |bit2           |bit1           |bit0
|CONNECT            |Reserved               |0              |0              |0              |0
|CONNACK            |Reserved               |0              |0              |0              |0
|PUBLISH            |Used in MQTT 3.1.1     |DUP1           |QoS2           |QoS2           |RETAIN3
|PUBACK             |Reserved               |0              |0              |0              |0
|PUBREC             |Reserved               |0              |0              |0              |0
|PUBREL             |Reserved               |0              |0              |1              |0
|PUBCOMP            |Reserved               |0              |0              |0              |0
|SUBSCRIBE          |Reserved               |0              |0              |1              |0
|SUBACK             |Reserved               |0              |0              |0              |0
|UNSUBSCRIBE        |Reserved               |0              |0              |1              |0
|UNSUBACK           |Reserved               |0              |0              |0              |0
|PINGREQ            |Reserved               |0              |0              |0              |0
|PINGRESP           |Reserved               |0              |0              |0              |0
|DISCONNECT         |Reserved               |0              |0              |0              |0

固定包头后面就是可变包头和载荷,但是在固定包头和可变包头之间有1-4个字节的大小来定义剩余长度

剩余长度

  位置:从第二个字节开始,最多占用4个字节,最少占用1个字节。

  剩余长度是指当前包中的剩余字节,包括可变包头的数据以及载荷。剩余长度不包含用来编码剩余长度的字节。

  剩余长度使用了一种可变长度的结构来编码,这种结构使用单一字节表示0-127的值。大于127的值如下处理。每个字节的低7位用来编码数据,最高位用来表示是否还有后续字节。因此每个字节可以编码128个值,再加上一个标识位。也就是说剩余长度是128进制进行编码的。前面的说明不是很明确,并没有体现MQTT剩余长度到底是如何编码的,下面先列出来官方的编码和解码的程式。

编码
do
    encodedByte = X MOD 128
    X = X DIV 128
    // if there are more data to encode, set the top bit of this byte
    if ( X > 0 )
        encodedByte = encodedByte OR 128
    endif
        'output' encodedByte
while ( X > 0 )

  其中MOD是取模操作(C语言的%),DIV是整除操作(C语言的/),OR是按位或操作(C语言的|)

解码
multiplier = 1
value = 0
do
    encodedByte = 'next byte from stream'
    value += (encodedByte AND 127) * multiplier
    multiplier *= 128
    if (multiplier > 128*128*128)
        throw Error(Malformed Remaining Length)
while ((encodedByte AND 128) != 0)

  AND是按位与操作(C语言的&)。算法运行完之后,变量value就是剩余长度的值。

举例

  上面的编码和解码的程式还是不理解的话,这里举几个示例,来演示编码和解码的原理。这里先说明一下符号的含义,下面的"/“表示整除,而非除以,”%“表示取模,”&“表示C语言的&,”|“表示C语言的|,”="表示等于。

  1. 假设X=10,意思是X的十进制值为10,剩余长度为10,那么编码成二进制该如何编码呢?首先,我们知道剩余长度是128进制的,那么10/128=0,10%128=10。这两个公式表示的含义是:10/128=0说明,1个字节完全可以满足存储10的需求,10%128=10说明,这个字节存储的值为10,所以十进制10转换成二进制为0000 1010,转换成16进制表示为0x0A。这里的一个字节体现不出来编码方式,不过却说明了需要多少个字节编码是根据128进制来决定的,并且一个字节能表示十进制的范围是0~127。
  2. 假设X=1000,意思是X的十进制值为1000,剩余长度为1000,那么编码成二进制方式类同1中的方法。首先,我们知道剩余长度是1000,那么1000/128=7,1000%128=104。这两个公式表示的含义是:1000/128=7>0说明,1个字节满足不了存储1000的需求,需要第二个字节,1000%128=104说明,第一个字节可以存储的值为104,所以十进制104转换成二进制为0110 1000,根据前面规则所述的一个字节的第7位表示是否还有后续字节,1表示有,0表示没有,那么0110 1000的7位需要设置为1,所以0110 1000 => 1110 1000,转换成10进制为232,十六进制为0xE8。由于需要第二个字节存储7,该7是由1000/128=7得出的,所以依照前面的方法,计算第二个字节。7/128=0,7%128=7,7/128=0表示不需要第三个字节来存储,二个字节已经满足要求,7%128=7说明,第二个字节存储的值是7,转换成二进制为0000 0111,由于不需要第三个字节,所以最高位7位的值仍然是0,转换成10进制是7,转换成16进制是0x07。所以最后计算出十六进制编码结果为0xE8 0x07
  3. 假设X=100000,意思是X的十进制是100000,剩余长度为100000,那么下面进行编码。首先计算出100000/128=781,100000%128=32。这连个公式表示的含义是:100000/128=781>0说明,1个字节满足不了存储100000的需求,需要第二个字节,100000%128=32说明,第一个字节可以存储32,那么32转换成二进制为0010 0000,由于需要后续字节,所以最高位设置位1,0010 0000=>1010 0000,转换成10进制为160,十六进制为0xA0。继续计算第二个字节,781/128=6>0,781%128=13,由于781/128=6大于0,所以说明需要第三个字节。781%128=13表示第二个字节存储的值为13,转换成二进制为0000 1101,由于需要第三个字节,所以最高位7位设置位1,所以0000 1101=>1000 1101,转换成10进制为141,16进制为0x8D。继续计算第三个字节,6/128=0,6%128=6,所以第三个字节存储的值为6,转换成二进制为0000 0110,由于不需要后续字节,所以最高位仍然为0,所以转换成10进制为6,十六进制为0x06。最后计算出十六进制编码结果为0xA0 0x8D 0x06

  上面举例说明了MQTT是如何编码的,总结以上规律可以得出一下结论:

  • 由于MQTT剩余长度最多4个字节,所以最大长度可以表示为2,6843,5455。
  • 字节从左到右依次字节依次升高,也就是说,左侧表示低阶字节,右侧表示高阶字节。
  • 字节本身代表的值,依次是(n-128)*128y,其中n表示编码字节转换为10进制的数,y表示第几个字节-1,比如第一个字节的y=0,第二个字节的y=1。
  • 各个字节本身代表的值的和就是剩余长度的值。也就是解码的原理了。

  例如100000的解码如下:100000的十进制编码为160 141 6,所以这三个字节表示的长度为

   (160-128)*1280 + (141-128)*1281 + 6*1282
  = 32 * 1 + 13 * 128 + 6 * 1282
  = 32 + 1664 + 98304
  = 100000

  从上面计算公式可以看出,32、13、6都是编码时的相应值,但是我们可以发现,计算过程中,160,141都大于128,可以减去128,而6小于128不需要减去,之所以这里强调一下,是为了容易理解为什么后面的代码中为何用&来实现这样的效果。

PHP代码进行编码和解码
  1. 进行运算之前,先了解一下PHP的位运算,这里暂时只用到了按位与(&)和按位或(|),参考地址:https://www.php.net/manual/zh/language.operators.bitwise.php

  2. 特定规律总结:

假设十进制正整数$n为位运算数,$x为需要编码的值,$y = %x % 128
1. 128的二进制编码为1000 0000
2. 127的二进制编码为0111 1111
3. 由于编码过程中,$x % 128 < 128,所以$x % 128二进制的最高位都是0(这里不考虑后续字节问题)
4. 由于编码的某个字节值是 $x % 128 + 128 或者 $x % 128 + 0,判断条件是是否有后续字节,如果有+128,否则+0
5. $y | 128 = $y + 128,这是因为$y < 128,所以最高位是0,128是最高位是1,其他都为0。例如65 | 128
  0100 0001  (65)
  1000 0000  (128)
= 1100 0001  (65+128=193)
6. 由于二进制 1111 1111 表示255,所以一个字节最大值为255,并且如果一个数大于127,说明对应的二进制的值得最高位为1。
7. 如果$n > 127 且 $n <= 255,那么$n & 127 = $n -128,这是因为$n的最高位为1,127的最高位为0,所以按位与的规则,7位为0,相当$n - 128,又因为127的6-0位都为1,所以6-0位等于$n其本身。例如160 & 127
  1010 0000 (160)
  0111 1111 (127)
= 0010 0000 (32)
8. 如果$n >= 0 且 $n <= 127,那么$n & 127 = $n,这是因为$n和127最高位都为0,127的6-0位都为1,根据按位与的规则,两者都为1为1,否则为0,所以$n&127等于$n其本身。例如 100 & 127
  0110 0100 (100)
  0111 1111 (127)
  0110 0100 (100)
9. 如果$n >= 128 且 $n <= 255,那么$n & 128 = 128,这是因为128的6-0位都为0,所以按位与结果6-0位都为0,7位两者都为1,所以7位为1,结果为128。例如160 & 128
  1010 0000 (160)
  1000 0000 (128)
  1000 0000 (128)
10. 如果$n >=0 且 $n < 128,那么$n & 128 = 0,这是因为128的6-0位都为0,所以按位与结果6-0位都为0,$n的7位为0,128的7位为1,所以7位为0,结果为0。例如13 & 128
  0000 1101 (13)
  1000 0000 (128)
  0000 0000 (0) 

  根据以上规律和编码解码示例,用PHP代码进行编码和解码解释

编码

  下面编码代码中,$x为剩余长度的值(10进制),$encodedByte = $x % 128;就是示例3中的第一步取模,得到32,然后$x = (int)($x / 128)就是判断$x是否被128整除,用来判断是否需要其他字节,如果$x>0,说明需要其他字节存储,那么编码第一个字节的值就是32,由于需要其他字节,所以最高位为1,根据特定规律3,4,5,可以得出结论,第一个字节为32+128=160。while进行条件判断是否进行下一个字节的处理。依此循环,最后到第三个字节$x = 0 不满足条件,结束循环。

// 编码规则(打包),比如运行下面的结果
$x = 100000;
do {
    $encodedByte = $x % 128;
    $x = (int)($x / 128);
    if ($x > 0) {
        $encodedByte = $encodedByte | 128;
    }
    echo $encodedByte . '</br>';
} while ($x > 0);

// 结果为
// 160
// 141
// 6
解码

  下面解码代码中,$multiplier相当于前面的128y,y表示第几个字节-1,比如第一个字节的y=0,第二个字节的y=1。$value是最后得剩余长度,$bytes是编码的字节数组,160,141,6分别表示第一,第二,第三个字节,依此类推。$i是$bytes的索引数,从0开始。首先,$encodedByte = $bytes[$i];获取第一个字节160,根据特定规律7,8可以得出$encodedByte & 127 = 160-128 = 32,这里$encodedByte & 127就解释了为什么前面强调的为何160,141减去128,而6不需要减去的实现方式。($encodedByte & 127) * $multiplier就等于第一个字节表示的值,$multiplier *= 128是为了得到下一个字节的128y值,下面$multiplier > 128 * 128 * 128是为了判断是否超过4个字节,因为第4个字节的multiplier等于1283,while中根据特定规律9,10判断$encodedByte是否小于128,因为do开始就执行1次,while判断到6,是判断了三次,6小于128说明没有后续字节了,循环结束。

$multiplier = 1;
$value = 0;
$bytes = [160,141,6];
$i = 0;
do {
    $encodedByte = $bytes[$i];
    $value += ($encodedByte & 127) * $multiplier;
    $multiplier *= 128;
    $i++;
    if ($multiplier > 128 * 128 * 128) { //这里说明超过了4个字节,错误
        echo 'Malformed Remaining Length';
    }
} while (($encodedByte & 128) != 0);

echo $value;

可变包头

  某些类型的MQTT控制包包含一个可变包头结构。位于固定包头和载荷之间。可变包头的内容取决于包的类型。所以这里不做过多说明,具体细节,请参考文章开头文档。

载荷

  一些MQTT控制包的最后一部分包含载荷,例如PUBLISH包相当于应用消息。载荷的内容取决于包的类型。所以这里不做过多说明,具体细节,请参考文章开头文档。

总结

  本篇文章重点说明了控制包的结构,以及剩余长度的解码和编码问题。下一篇文章会重点阐述SWOOLE和PHP下得控制包的解析问题。

更多:

MQTT-第一节(协议)

MQTT-第二节(SWOOLE和PHP下控制包的解析)