1 MQTT介绍
MQTT是一种基于客户端/服务器架构(CS架构)的发布/订阅模式的消息传输协议。它最初由IBM开发,并被设计成为轻量、开放、简单以及易于实现的消息传输协议。MQTT很适合应用于环境受限(窄带、高延迟、不可靠的网络环境)的机器与机器(M2M)、物联网(IoT)设备之间的消息通信。
2 MQTT控制报文的结构
MQTT协议一包完整的控制报文包含三部分:固定报文头部(Fixed Header)、可变报文头部(Variable Header)以及有效载荷(Payload),如图1所示:
图2.1:MQTT控制报文结构
固定报文头部(Fixed Header): 每一包MQTT控制报文都会包含此部分,由1byte的控制报文类型 + 1byte~4bytes的剩余字段(Remaining Length)。其长度为2bytes ~ 5bytes,最小为2bytes,最大为5bytes;
可变报文头部(Variable Header): 只有部分MQTT控制报文包含此部分。其内容和长度根据不同的控制报文而不一样;
有效载荷(Payload): 只有部分MQTT控制报文包含此部分。其长度为实际用户数据的长度。
2.1 MQTT固定报文头部(Fixed Header)
MQTT固定报文头部(Fixed Header)的结构如图2所示:
图2.2:MQTT固定报文头部的结构
如图2所示,MQTT固定报文头部由两部分组成:Byte1和Byte2 ···
2.1.1 Byte1
Byte1的高4bits(4bit~7bit)为MQTT控制报文的类型,其值为无符号整数值;低4bits(3bit~0bit)为MQTT控制报文类型相应的标志位(Flags)。
MQTT协议控制报文最多可以有16种报文类型,目前(v3.1.1)使用了14种;控制报文的类型及其相应的标志位如图3所示:
图2.3:MQTT控制报文的类型
从图2.3我们可以了解到MQTT当前版本(v3.1.1)定义的所有控制报文类型;其中每个控制报文类型都有其相对应的控制报文标志位(3bit~0bit),当前版本仅有控制报文类型发布消息(PUBLISH)
使用到了控制报文标志位。发布消息(PUBLISH)
的标志位定义如下:
DUP
—— 发布消息的重复分发标识。0标识没有重复分发;1标识重复分发QoS
—— 发布消息的服务质量QoS,占2bits。其可能值为0,1,2。MQTT的QoS参考此文章:MQTT QoS说明RETAIN
—— 发布消息时设置的消息持久化标识。0表示消息不持久化;1表示消息需要持久化。例如客户端向服务器发布消息时设置了RETAIN位为1,则服务器会将此条消息保存在服务器端,当有新客户端订阅此主题,服务器会将此消息推送给新客户端。
2.1.2 Byte2
Byte2为剩余长度字段(Remaining Length),它表示的是MQTT控制报文后面所剩余的字节数,所剩余的字节数不包含剩余长度字段本身。剩余长度字段(Remaining Length)所占字节最小为1byte,最大为4bytes。剩余长度字段(Remaining Length)采用的是变长编码方案,即每个字节的最高位为延续位(continuation bit
),用来表示否有更多的字节,所以每个字节能编码的最大值为127。以下举例说明MQTT的剩余长度字段的变长编码方案:
图2.4:剩余长度字段(Remaining Length)范围
从图2.4可以得知,MQTT允许的剩余长度范围为:0byte ~ 268435455bytes(256MB)。
算法A: 将非负整数编码成剩余长度字节,算法如下:1
2
3
4
5
6
7
8
9do
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是模运算,DIV是整数除法运算,OR是按位或操作(对应C语言中分别为%
,/
,|
)
算法B: 将剩余长度字节解码成非负整数,算法如下:1
2
3
4
5
6
7
8
9multiplier = 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的值就是剩余长度的值。
下面给出算法A和算法B在Java中的具体实现:
算法A、算法B: Java实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44public class RemainingLengthUtils {
/**
* 算法A:将非负整数的剩余长度编码为字节
* @param len 待编码的剩余长度(非负整数)
* @return 返回剩余字节长度字段的编码字节数组
*/
public static byte[] encode(long len) {
int count = 0; // 记录当前编码的多少个字节,不能超过MQTT限制的最大4个字节
ByteArrayOutputStream bos = new ByteArrayOutputStream(4);
do {
byte encodedByte = (byte) (len % 128);
len = len / 128;
// 如果len > 0说明还有更多字节需要编码
if (len > 0) {
encodedByte |= 0x80;
}
bos.write(encodedByte);
count++;
} while ((len > 0) && (count < 4));
return bos.toByteArray();
}
/**
* 算法B:将剩余长度字节解码成非负整数
* @param bytes 剩余长度字节数组
* @return 返回剩余字节长度(非负整数)
*/
public static long decode(byte[] bytes) {
byte digit;
long value = 0;
int multiplier = 1;
int index = 0; // 剩余长度字段起始位置的下标
do {
digit = bytes[index++];
value += ((digit & 0x7F) * multiplier);
multiplier *= 128;
} while ((digit & 0x80) != 0);
return value;
}
}
2.2 MQTT可变报文头部(Variable Header)
MQTT可变报文头部位于固定报文头部(Fixed Header)和有效载荷(Payload)之间,其格式和内容会根据不同的控制报文类型而不同。注意:并不是所有类型的控制报文都包含可变头部。
很多类型的控制报文的可变头部(Variable Header)都会包含一个2bytes大小的消息ID(Packet Identifier),这些控制报文如下:
PUBLISH
—— 发布消息的QoS > 0
才会包含Packet Identifier,使用当前未使用的Packet IdentifierPUBACK
—— 使用与PUBLISH
相同的Packet Identifier,表示消息确认PUBREC
—— 使用与PUBLISH
相同的Packet Identifier,表示消息确认PUBREL
—— 使用与PUBLISH
相同的Packet Identifier,表示消息确认PUBCOMP
—— 使用与PUBLISH
相同的Packet Identifier,表示消息确认SUBSCRIBE
—— 使用当前未使用的Packet IdentifierSUBACK
—— 使用与SUBSCRIBE
相同的Packet Identifier,表示消息确认UNSUBSCRIBE
—— 使用当前未使用的Packet IdentifierUNSUBACK
—— 使用与SUBSCRIBE
相同的Packet Identifier,表示消息确认
SUBSCRIBE
,UNSUBSCRIBE
和PUBLISH(QoS大于0)
控制报文必须包含一个非零的16位报文标识符(Packet Identifier)。客户端每次发送一个新的这些类型的报文时都必须分配一个当前未使用的Packet Identifier。
MQTT所有类型的控制报文对Packet Identifier字段是否包含,如下表所示:
2.3 MQTT有效载荷(Payload)
MQTT协议结构的最后一部分就是有效载荷(Payload),例如对于PUBLISH
控制报文,Payload里面的内容就是用户的实际应用数据。
下面的表格列出了每种控制报文的Payload包含情况: