MQTT协议 -- 消息报文格式

虽然学习协议是枯燥的,但是熟悉协议本身却是很重要的事情。如果能把其细节弄清楚,并且配合一些实验来学习,就不会那么枯燥了。

消息报文格式

MQTT协议是应用层协议,需要借助TCP/IP协议进行传输,类似HTTP协议。MQTT协议也有自己的格式,如下表:

[ Fixed Header | Variable Header | Payload]

Fixed Header: 固定头部,MQTT协议分很多种类型,如连接,发布,订阅,心跳等。其中固定头是必须的,所有类型的MQTT协议中,都必须包含固定头。

Variable Header:可变头部,可变头部不是可选的意思,而是指这部分在有些协议类型中存在,在有些协议中不存在。

Payload:消息载体,就是消息内容。与可变头一样,在有些协议类型中有消息内容,有些协议类型中没有消息内容。


固定头

固定头包含两部分内容,首字节(字节1)和剩余消息报文长度(1-4字节)。

Bit 7 6 5 4 3 2 1 0
Byte 1 MQTT Control Packet type Flags specific to each MQTT Control Packet type
Byte 2... Remaining Length

为了避免翻译不准确,这里都使用官方的原始术语。其中MQTT Control Packet type可以简单理解为字节位Bit[7-4]用于确定报文类型。Flags specific to each MQTT Control Packet type意思是字节位Bit[3-0]用作某些报文的特殊标记。

首字节

首字节用于表示MQTT消息的报文类型以及某些类型的控制标记,如上图。高4位(bit7~bit4)表示协议类型,总共可以表示16种协议类型,其中0000和1111是保留字段。MQTT消息报文类型如下。

报文类型 字段值 数据方向 描述
保留 0 禁用 保留
CONNECT 1 Client ---> Server 客户端连接到服务器
CONNACK 2 Server ---> Client 连接确认
PUBLISH 3 Client <--> Server 发布消息
PUBACK 4 Client <--> Server 发不确认
PUBREC 5 Client <--> Server 消息已接收(QoS2第一阶段)
PUBREL 6 Client <--> Server 消息释放(QoS2第二阶段)
PUBCOMP 7 Client <--> Server 发布结束(QoS2第三阶段)
SUBSCRIBE 8 Client ---> Server 客户端订阅请求
SUBACK 9 Server ---> Client 服务端订阅确认
UNSUBACRIBE 10 Client ---> Server 客户端取消订阅
UNSUBACK 11 Server ---> Client 服务端取消订阅确认
PINGREQ 12 Client ---> Server 客户端发送心跳
PINGRESP 13 Server ---> Client 服务端回复心跳
DISCONNECT 14 Client ---> Server 客户端断开连接请求
保留 15 禁用 保留

首字节的低4位(bit3~bit0)用来表示某些报文类型的控制字段,实际上只有少数报文类型有控制位,如下图。

报文类型 固定头标记 Bit 3 Bit 2 Bit 1 Bit 0
CONNECT 保留 0 0 0 0
CONNACK 保留 0 0 0 0
PUBLISH Used in MQTT 3.1.1 DUP QoS QoS RETAIN
PUBACK 保留 0 0 0 0
PUBREC 保留 0 0 0 0
PUBREL 保留 0 0 1 0
PUBCOMP 保留 0 0 0 0
SUBSCRIBE 保留 0 0 1 0
SUBACK 保留 0 0 0 0
UNSUBACRIBE 保留 0 0 1 0
UNSUBACK 保留 0 0 0 0
PINGREQ 保留 0 0 0 0
PINGRESP 保留 0 0 0 0
DISCONNECT 保留 0 0 0 0

当发布PUBLISH消息时,如果DUP字段(bit 3)设置为1,表明这是一条重复消息,否则是第一次发布消息。为了保证消息的可靠性传递,当QoS设置为1时,客户端或服务器发布消息时,需要得到对方的确认(PUBACK),如果一段时间后没收到PUBACK,那么会再次发送当前消息,并将DUP字段标记为1。

QoS用来表明QoS等级,如果Bit 1和Bit 2都为0,表示QoS 0。如果Bit 1为1,表示QoS 1。如果Bit 2为1,表示QoS 2。如果同时将Bit 1和Bit 2都设置成1,那么客户端或服务器认为这是一条非法的消息,会关闭当前连接。

目前Bit[3-0]只在PUBLISH协议中使用有效,并且表中指明了是MQTT 3.1.1版本。对于其它MQTT协议版本,内容可能不同。所有固定头标记为"保留"的协议类型,Bit[3-0]必须保持与表中保持一致,如SUBSCRIBE协议,其Bit 1必须为1。如果接收方接收到非法的消息,会强行关闭当前连接。

Remaining Length

Remaining Length意思是剩余长度,即Variable Header + Payload的长度。剩余长度从Byte 2开始,最长可达4字节。所以剩余长度范围是Byte[2-5]。那么怎样确定其长度到底是1还是4呢,这取决于字节的最高位Bit 7(默认都是高字节在前),如果这个值是1,那么就继续计算字节长度,如果是0,那么就不再计算字节长度。

消息长度可以简单理解为128进制的数据,4位长度最大可以表示128*128*128*128Byte=256MB。但是这个长度的计算有些特别,就是低位在前,高位在后(因为正常的表示方法是高位在前,低位在后),字节最高位Bit7用于标记是否需要继续计算消息长度。以下是消息长度的长度范围:

字节 最小值 最大值
1 0(0x00) 127(0x7F)
2 128 (0x80, 0x01) 16 383 (0xFF, 0x7F)
3 16 384 (0x80, 0x80, 0x01) 2 097 151 (0xFF, 0xFF, 0x7F)
4 2 097 152 (0x80, 0x80, 0x80, 0x01) 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F)

稍微注意一下,0x80=1000 0000,不是 1000。刚开始以为是1000,所以就没明白。

举个例子。

消息假设长度是[0X60],其二进制是01100000,字节最高位Bit7(从左边起第0位)是0,所以不需要继续往后计算。那么消息长度就是0X60,十进制数是96。

如果消息长度是[0XC1, 0XC2, 0X33],那么他们的二进制分别如下,

0xC1=1100 0001

0xC2=1100 0010

0x33=0011 0011,

第一字节最高位是1,那么需要继续向后计算,去掉标记位(0xC1%128),得到100 0001=41

第二字节最高位是1,那么需要继续向后计算,去掉标记位(0xC2%128),得到100 0010=42

第三字节最高位是0,不需要向后计算,其结果就是0x33=51

因为低位在前,高位在后,那么长度计算为Length=41 + 42*128 + 51*128*128=841001 B = 821KB

需要注意的是,消息长度=可变头部长度+消息内容长度。不包括首字节和消息长度本身,如果消息长度为5,那么说明这条消息后边还有5字节,整条消息长度为7(首字节+1位长度字节+5)。

另外如果消息长度为4字节,最后一位不能超过0X7F=127,因为如果超出这个值,其最高位Bit7是1,还需要往后计算,这与消息最大长度为4字节矛盾。所以如果出现[0XFF, 0XFF, 0XFF, 0XFF]这样的消息长度,那么接收方认为这是一条非法的消息。


Variable Header

Variable Header的意思是可变化的消息头部。有些报文类型包含可变头部,如PUBLISH,SUBSCRIBE,CONNECT等等。可变头部在固定头部和消息内容之间,其内容根据报文类型不同而不同。

Packet Identifier(消息ID)是一种常见的可变头部,一个消息ID包含2字节,高字节在前,低字节在后。包含Packet Identifier的协议类型包括:

报文类型 包含可变头
PUBLISH YES(QoS > 0)
PUBACK YES
PUBREC YES
PUBREL YES
PUBCOMP YES
SUBSCRIBE YES
SUBACK YES
UNSUBSCRIBE YES
UNSUBACK YES

消息ID默认是从1开始并自增,如果一个消息ID被用完后,这个消息ID可以被重用。对于PUBLISH (QoS 1)来说,如果发送端接收到PUBACK,那么这个消息ID就用完了。对于PUBLISH(QoS 2),如果接收方收到PUBCOMP,那么这个消息ID就用完了。对于SUBSCRIBE和UNSUBSCRIBE,消息ID使用完成的标记是发送方收到了对应的SUBACK和UNSUBACK。

另外客户端和服务端的消息ID是独立分配的,客户端和服务端可以同时使用同一个消息ID。比如

    Client                     Server

   PUBLISH Packet Identifier=0x1234--->

   <--PUBLISH Packet Identifier=0x1234

   PUBACK Packet Identifier=0x1234--->

   <--PUBACK Packet Identifier=0x1234

上边消息客户端给服务端发送一条消息,使用的消息ID是0x1234,同时服务端给客户端发送了一条消息,也使用了消息ID 0x1234。然后客户端回复服务端,发送PUBACK,最后是客户端收到服务端的回复PUBACK。

另外其它协议如CONNECT和CONNACK也有可变头部,具体请参见MQTT-Packet CONNECT Variable Header


Payload

有些报文类型是包含Payload的,Payload意思是消息载体的意思,如PUBLISH的Payload就是指消息内容。而CONNECT的Payload则包含Client Identifier,Will Topic,Will Message,Username,Password等信息。具体请参见MQTT-Packet CONNECT Payload

包含Payload的报文类型如下:

报文类型 是否包含Payload
CONNECT YES
PUBLISH 可选
SUBSCRIBE YES
SUBACK YES
UNSUBSCRIBE YES

除了上面列出的报文类型,其它的报文类型都没有Payload。

抓包测试

我们使用Wire Shark抓包,来探测一下MQTT消息内容。

1. 打开Wireshark,选择你的网卡,添加以下过滤条件,并点击开始捕获。

tcp.port==1883

2. 打开终端,输入以下命令,发布一条消息。如果你不理解一下命令,请参看我的前一篇文章MQTT快速入门

$ mosquitto_pub -d -p 1883 -h 10.69.94.176 -q 1 -t topic1 -m "Hello MQTT"
Client mosqpub|2052-SCNWCL0121 sending CONNECT
Client mosqpub|2052-SCNWCL0121 received CONNACK (0)
Client mosqpub|2052-SCNWCL0121 sending PUBLISH (d0, q1, r0, m1, 'topic1', ... (10 bytes))
Client mosqpub|2052-SCNWCL0121 received PUBACK (Mid: 1)
Client mosqpub|2052-SCNWCL0121 sending DISCONNECT

3. 进入Wireshark捕获窗口,发现捕获到了一些TCP和MQTT协议,如下:

MQTT协议 -- 消息报文格式_第1张图片
mqtt-control-packet-format.png

4. 查看其中一条MQTT消息,比如Publish Message,点击这一行。查看MQTT消息内容。其字节码为

32 14 00 06 74 6f 70 69 63 31 00 01 48 65 6c 6c 6f 20 4d 51 54 54

5. 来具体看一下消息内容

  • 首字节 0x32=0011 0010,对照首字节中的表,4位高字节为0011=3,表示PUBLISH,4位低字节0010,分别表示DUP 0,QoS 1(占两位),Retain 0。

  • Remaining Length 0x14=20,表示剩余消息长度为20。

  • PUBLISH (QoS>0)报文消息包含可变头部,其可变头部包含topic name和Packet Identifier。其格式为:

  • 00 06表示topic name的长度,所以topic name长度是6

  • 74 6f 70 69 63 31表示topic name的UTF8字符串,其值为"topic1"。

  • 00 01是Packet Identifier,所以消息ID为1。

  • 48 65 6c 6c 6f 20 4d 51 54 54是Payload,表示“Hello MQTT"的UTF8字节码。


总结

我们介绍了MQTT协议的消息格式,MQTT消息格式包含Fixed Header, Variable Header和Payload。因为MQTT消息格式非常精简,所以可以高效的传输数据。

Fixed Header中包含首字节,高4位用来表示报文类型,低4位用于类型控制。目前只有PUBLISH使用了类型控制字段。其它控制字段被保留并且必须与协议定义保持一致。

Fixed Header同时包含Remaining Length,这是剩余消息长度,最大长度为4字节,理论上一条MQTT最大可以传输256MB数据。Remaining Length=Variable Header+Payload长度。

Variable Header是可变头部,有些报文类型中需要包含可变头部,可变头部根据报文类型不同而不同。比如Packet Identifier在发布,订阅/取消订阅等报文中都使用到。

Payload是消息内容,也只在某些报文类型中出现,其内容和格式也根据报文类型不同而不同。


所有文章在Github上同步,你也可以访问我的个人博客点击查看

你可能感兴趣的:(MQTT协议 -- 消息报文格式)