简要地说,
MQTT消息 = 固定头部(最小2字节)+ 可变头部 + Playload/消息体/负荷
其中,每个MQTT消息都包含有一个固定的头部,有些消息含有可变头部和消息体。
一、固定头部
固定头部,使用两个字节,共16位:
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
byte 1 | Message Type | DUP flag | QoS level | RETAIN | ||||
byte 2 | Remaining Length |
消息类型(4-7),使用4位二进制表示,可代表16种消息类型:
Mnemonic | Enumeration | Description |
---|---|---|
Reserved | 0 | Reserved |
CONNECT | 1 | Client request to connect to Server |
CONNACK | 2 | Connect Acknowledgment |
PUBLISH | 3 | Publish message |
PUBACK | 4 | Publish Acknowledgment |
PUBREC | 5 | Publish Received (assured delivery part 1) |
PUBREL | 6 | Publish Release (assured delivery part 2) |
PUBCOMP | 7 | Publish Complete (assured delivery part 3) |
SUBSCRIBE | 8 | Client Subscribe request |
SUBACK | 9 | Subscribe Acknowledgment |
UNSUBSCRIBE | 10 | Client Unsubscribe request |
UNSUBACK | 11 | Unsubscribe Acknowledgment |
PINGREQ | 12 | PING Request |
PINGRESP | 13 | PING Response |
DISCONNECT | 14 | Client is Disconnecting |
Reserved | 15 | Reserved |
除去0和15位置属于保留待用,共14种消息事件类型。
保证消息可靠传输,默认为0,只占用1位,表示第一次发送。不能用于检测消息重复发送等。只适用于客户端或服务器端尝试重发PUBLISH, PUBREL, SUBSCRIBE 或 UNSUBSCRIBE消息,注意需要满足以下条件:
当QoS > 0
消息需要回复确认
此时,在可变头部需要包含消息ID。当值为1时,表示当前消息先前已经被传送过。
QoS(Quality of Service,服务质量)
使用两个二进制表示PUBLISH类型消息:
QoS value | bit 2 | bit 1 | Description | ||
---|---|---|---|---|---|
0 | 0 | 0 | 至多一次 | 发完即丢弃 | <=1 |
1 | 0 | 1 | 至少一次 | 需要确认回复 | >=1 |
2 | 1 | 0 | 只有一次 | 需要确认回复 | =1 |
3 | 1 | 1 | 待用,保留位置 |
仅针对PUBLISH消息。不同值,不同含义:
1:表示发送的消息需要一直持久保存(不受服务器重启影响),不但要发送给当前的订阅者,并且以后新来的订阅了此Topic name的订阅者会马上得到推送。
备注:新来乍到的订阅者,只会取出最新的一个RETAIN flag = 1的消息推送。
0:仅仅为当前订阅者推送此消息。
假如服务器收到一个空消息体(zero-length payload)、RETAIN = 1、已存在Topic name的PUBLISH消息,服务器可以删除掉对应的已被持久化的PUBLISH消息。
解析:
C版:
#include
#define BOOL int
int main () {
char publishFixHeader = 50 ; //0 0 1 1 0 0 1 0
doGetBit(publishFixHeader) ;
return 0;
}
void doGetBit(char ch)
{
BOOL retain = (ch & 1 )>0 ? 1:0 ;
int qosLevel = (ch & 0x6) >> 1;
BOOL dupFlag = ((ch & 0x8) >> 3)>0 ? 1:0;
int messageType = (ch >> 4) ;
printf("retain = %d ,qosLevel = %d ,dupFlag= %d , messageType = %d " ,retain,qosLevel,dupFlag,messageType);
}
public class Test{
public static void main(String[] args) {
byte publishFixHeader = 50;//(不超过byte范围)0 0 1 1 0 0 1 0
doGetBit(publishFixHeader);
int ori = 224;//(超过byte范围)1110000,DISCONNECT ,Message Type (14)
byte flag = (byte) ori; //有符号byte
doGetBit(flag);
doGetBit_v2(ori);
}
public static void doGetBit(byte flags) {
boolean retain = (flags & 1) > 0;
int qosLevel = (flags & 0x06) >> 1;
boolean dupFlag = (flags & 8) > 0;
int messageType = (flags >> 4) & 0x0f;//对于byte,只取低四位
System.out.format(
"Message type:%d, DUP flag:%s, QoS level:%d, RETAIN:%s\n",
messageType, dupFlag, qosLevel, retain);
}
public static void doGetBit_v2(int flags) {
boolean retain = (flags & 1) > 0;
int qosLevel = (flags & 0x06) >> 1;
boolean dupFlag = (flags & 8) > 0;
int messageType = flags >> 4;
System.out.format(
"Message type:%d, DUP flag:%s, QoS level:%d, RETAIN:%s\n",
messageType, dupFlag, qosLevel, retain);
}
}
在当前消息中剩余的byte(字节)数,包含可变头部和负荷(称之为内容/body,更为合适)。单个字节最大值:01111111,16进制:0x7F,10进制为127。单个字节为什么不能是11111111(0xFF)呢?因为MQTT协议规定,第八位(最高位)若为1,则表示还有后续字节存在。同时MQTT协议最多允许4个字节表示剩余长度。那么最大长度为:0xFF,0xFF,0xFF,0x7F,二进制表示为:11111111,11111111,11111111,01111111,十进制:268435455 byte=261120KB=256MB=0.25GB 四个字节之间值的范围:
Digits | From | To |
---|---|---|
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) |
注意:剩余长度信息的最后一个字节的最高位必须是0,否则表示还有后续字节存在。
解析:
Java版:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class Test{
public static void main(String[] args) throws IOException {
// 模拟客户端写入最大值
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(arrayOutputStream);
dataOutputStream.write(0xff);
dataOutputStream.write(0xff);
dataOutputStream.write(0xff);
dataOutputStream.write(0x7f);
InputStream arrayInputStream = new ByteArrayInputStream(arrayOutputStream.toByteArray());
// 模拟服务器/客户端解析
System. out.println( "result is " + bytes2Length(arrayInputStream)); //result is 268435455
}
/**
* 转化字节为 int类型长度
* @param in
* @return
* @throws IOException
*/
private static int bytes2Length(InputStream in) throws IOException {
int multiplier = 1;
int length = 0;
int digit = 0;
do {
digit = in.read(); //一个字节的有符号或者无符号,转换转换为四个字节有符号 int类型
length += (digit & 0x7f) * multiplier;
multiplier *= 128;
} while ((digit & 0x80) != 0);
return length;
}
}
c版:
#include
long Convert2Lenth( char *p)
{
long lenth = 0 ;
long multiplier = 1 ;
do{
lenth += ((*p)&0x7f) * multiplier;
multiplier *= 128 ;
}
while(((*p++)&0x80)!= 0 ) ;
return lenth;
}
int main () {
//模拟得到4个字节数据
char data[] = {0xff , 0xff , 0xff , 0x7f };
//转换为剩余长度
long Lenth = Convert2Lenth(data) ;
printf("Lenth = %ld" , Lenth); // Lenth = 268435455
return 0;
}
假设在32位系统下,那么如何将int型长度解析为不确定的字节值呢?
C:(随便取一个0 ~268435455 之间的整数 )
#include
void Lenth2Byte(int lenth)
{
int digit;
do{
digit = lenth%128 ;
lenth = lenth/128 ;
if(lenth > 0)
digit = digit|0x80 ;
printf("digit = 0x%x \n" , digit) ;
}
while(lenth>0);
}
int main () {
//模拟得到一个int类型的长度
int Lenth = 65386655 ;
//转换成4字节输出
Lenth2Byte(Lenth);
return 0;
}
输出为:
二、可变头部
固定头部仅定义了消息类型和一些标志位,一些消息的元数据,需要放入可变头部中。可变头部内容字节长度 + Playload/负荷字节长度 = 剩余长度,这个是需要牢记的。可变头部,包含了协议名称,版本号,连接标志,用户授权,心跳时间等内容,这部分和后面要讲到的CONNECT消息类型,有重复,暂时略过。
三、Playload/消息体/负荷
1、消息体主要是为配合固定/可变头部命令(比如CONNECT可变头部User name标记若为1则需要在消息体中附加用户名称字符串)而存在。CONNECT/SUBSCRIBE/SUBACK/PUBLISH等消息有消息体。PUBLISH的消息体以二进制形式对待。
2、请记住,MQTT协议只允许在PUBLISH类型消息体中使用自定义特性,在固定/可变头部想加入自定义私有特性,就免了吧。这也是为了协议免于流于形式,变得很分裂也为了兼顾现有客户端等。比如支持压缩等,那就可以在Playload中定义数据支持,在应用中进行读取处理。
这部分会在后面详细论述。
四、 消息标识符/消息ID1、固定头中的QoS level标志值为1或2时才会在:PUBLISH,PUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE,SUBACK,UNSUBSCRIBE,UNSUBACK等消息的可变头中出现。
2、一个16位无符号位的short类型值(值不能为 0,0做保留作为无效的消息ID),仅仅要求在一个特定方向(服务器发往客户端为一个方向,客户端发送到服务器端为另一个方向)的通信消息中必须唯一。比如客户端发往服务器,有可能存在服务器发往客户端会同时存在重复,但不碍事。
3、可变头部中,需要两个字节的顺序是MSB(Most Significant Bit) LSB(Last/Least Significant Bit),翻译成中文就是,最高有效位,最低有效位。最高有效位在最低有效位左边/上面,表示这是一个大端字节/网络字节序,符合人的阅读习惯,高位在最左边。
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Message Identifier MSB | ||||||||
Message Identifier LSB |
但凡如此表示的,都可以视为一个16位无符号short类型整数,两个字节表示。在JAVA中处理比较简单:
DataInputStream.readUnsignedShort
或者
in.read() * 0xFF + in.read();
最大长度可为: 65535
------------------------------------------------------------------------------------------------------------------------------------
有关字符串,MQTT采用的是修改版的UTF-8编码,一般形式为如下,需要牢记:
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
byte 1 | String Length MSB | |||||||
byte 2 | String Length LSB | |||||||
bytes 3 ... | Encoded Character Data |
比如Java,使用writeUTF()方法写入一串文字“OTWP”,头两个字节为一个完整的无符号数字,代表字符串字节长度,后面四个字节才是字符串真正的长度,共六个字节:
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
byte 1 | Message Length MSB (0x00) | |||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
byte 2 | Message Length LSB (0x04) | |||||||
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | |
byte 3 | 'O' (0x4F) | |||||||
0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | |
byte 4 | 'T' (0x54) | |||||||
0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | |
byte 5 | 'W' (0x57) | |||||||
0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | |
byte 6 | 'P' (0x50) | |||||||
0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
这点,在程序中,可不用单独处理默认,直接使用readUTF()方法,可自动省去了处理字符串长度的麻烦。当然,可以手动读取字符串:
// 模拟写入
dataOutputStream.writeUTF( "abcd");// 2 + 4 = 6 byte
......
// 模拟读取
int decodedLength = dataInputStream.readUnsignedShort();//2 byte
byte[] decodedString = new byte[decodedLength]; // 4 bytes
dataInputStream.read(decodedString);
String target = new String(decodedString, "UTF-8");
等同于:
String target = dataInputStream.readUTF();//直接读取字符串dataInputStream.readUTF