做MQTT通讯之前,最好能找到成功案例,然后再看看MQTT通信协议《MQTT-3.1.1-CN》,一定会事倍功半。不了解其协议,在阅读程序时,会有点困难。下面就是MQTT控制报文的相关知识点,供以后查阅。
1、MQTT二进制数据
MQTT的16位整数使用大端序(big-endian),即高位字节在低位字节前面。
2、UTF-8编码字符串的结构:
每个字符串都有一个“两字节的长度字段”作为前缀,它给出这个字符串UTF-8编码的字节数。格式如下:
字符串长度的最高有效字节(MSB) + 字符串长度的最低有效字节(LSB) + UTF-8编码的字符数据
如果“字符串长度”大于0,则后面的“UTF-8编码的字符数据”就是有效的。
3、MQTT控制报文的结构:
MQTT控制报文由“固定报头”,“可变报头”和“有效载荷”三部分组成。
1)、固定报头
所有的“控制报文”都包含一个“Fixed header 固定报头”,见下表:
从表中可以看出,“固定报头”第1个字节的“高4位”表示“控制报文的类型”,“低4位”表示“MQTT控制报文类型的标志”,“固定报头”从第2个字节开始是“剩余长度”的值,它所占的字节数不是固定的。
“固定报头”第1个字节的高4位,它表示“控制报文的类型”,见下面的介绍:
Reserved(值为0);保留
CONNECT(值为1),意思是客户端请求连接服务端;
CONNACK(值为2),服务端告诉客户端,连接报文被确认了;
PUBLISH(值为3),服务端和客户端均发布消息;
PUBACK(值为4),服务端和客户端均可在“QoS 1”服务等级里,消息发布收到确认;
PUBREC(值为5),服务端和客户端均可发布收到(保证交付第一步);
PUBREL(值为6),服务端和客户端均可发布释放(保证交付第二步);
PUBCOMP(值为7),服务端和客户端均可在“QoS 2”服务等级里,消息发布完成(保证交互第三步);
SUBSCRIBE(值为8),客户端向服务端发送订阅请求;
SUBACK(值为9),服务端告诉客户端,订阅请求报文确认;
UNSUBSCRIBE(值为10),客户端告诉服务端,取消前面的“订阅请求”;
UNSUBACK(值为11),服务端告诉客户端,“取消订阅报文”被确认;
PINGREQ(值为12),客户端向服务端发送“心跳请求”;
PINGRESP(值为13),服务端告诉客户端,发送“心跳响应”;
DISCONNECT(值为14),客户端告诉服务端,客户端断开连接;
Reserved(值为15);保留;
“固定报头”第1个字节的低4位,它表示“MQTT控制报文类型的标志”。
从表中可以看出,只有在“PUBLISH控制报文”中,才会使用可配置的“固定报头的标志位”,其它的控制报文都是采用一个固定的值,具体,见上面的表格介绍。
“MQTT控制报文类型的标志”如下:
DUP(bit3),QoS(bit2),QoS(bit1),RETAIN(bit0)
DUP1 =控制报文的重复分发标志
QoS2 = PUBLISH报文的服务质量等级
RETAIN3 = PUBLISH报文的保留标志
剩余长度
固定报头从第2个字节开始就是“剩余长度”。
剩余长度(Remaining Length)表示“当前报文剩余部分”的字节数,包括“可变报头”和“负载的数据”。剩余长度不使用“编码剩余长度字段”的字节数。
剩余长度字段使用一个“变长度编码”方案,对小于128的值它使用单字节编码。
更大的值按下面的方式处理:低7位有效位用于编码数据,最高有效位用于指示是否有更多的字节。也就是说,这个剩余长度的字节数不是固定不变的。注意:最多为4个字节,最少为1个字节。
因此每个字节可以编码128个数值和一个延续位(continuation bit)。“剩余长度字段”最大4个字节。
从上表可以看出:0编码后为0x00,127编码后为0x7F,128编码后为0x80和0x01,16383编码后为0xFF和0x7F,等。
拿16383举例:
将16383对128求余数和商:16383%128=127,16383/128=127,
由于商大于0,因此余数的最高位置1,就编码为0x7F|0x80=0xFF
将商127对128求余数和商:127%128=127,127/128=0,
由于商等于0,因此余数的最高位置0,就编码为0x7F|0x00=0x7F
所以,16383被编码后,就是0xFF和0x7F了。
变长编码方案的算法:
/*
对“消息长度length”进行编码,然后保存到buf[]中,返回值为length编码后保存到buf[]中的字节数;
*/
int MQTTPacket_encode(unsigned char* buf, int length)
{
int rc = 0;
char d;
FUNC_ENTRY;
do
{
d = length % 128;//对128求余数,保存到d
length /= 128;//对128求商,并将商值保存到length中
if (length > 0) d |= 0x80;
//对128求商,若商值大于0,表示后面还有数据等待编码,将余数的最高位置1;
buf[rc] = d;//将余数编码值保存到buf[]
rc++;
}while (length > 0);
FUNC_EXIT_RC(rc);
return rc;
}
变长解码方案的算法:
static unsigned char* bufptr;
//函数功能:将bufptr[]中的前count拷贝到c[]中
int bufchar(unsigned char* c, int count)
{
int i;
for (i = 0; i < count; ++i)
*c = *bufptr++;
return count;
}
/*
解码后的"长度"保存到(*value)中,返回值为解码前长度所占的字节数
*/
int MQTTPacket_decode(int (*getcharfn)(unsigned char*, int), int* value)
{
unsigned char c;
int multiplier = 1;
int len = 0;
int rc;
*value = 0;//初始化value所指向的存储区
do
{
rc = -1;//先假定数据是错的
++len;//执行后,表示处理第1个字节
if (len > 4)
{
rc = -1; /* bad data */
goto exit;
}
rc = (*getcharfn)(&c, 1);// getcharfn 是bufchar()函数的指针
//将bufptr[]中的前1个字节拷贝到c中,返回值为1
//c为编码后的长度数据,需要解码
if (rc != 1)
goto exit;
*value += (c & 127) * multiplier;
//解码:计算长度
multiplier *= 128;
} while ( (c & 128) != 0 );
exit:
FUNC_EXIT_RC(len);
return len;
}
//解码后的"长度"保存到(*value)中,返回值为解码前长度所占的字节数
//将buf所指向的长度数据进行解码,然后"消息长度"保存到value[]中,返回值为value的长度
int MQTTPacket_decodeBuf(unsigned char* buf, int* value)
{
int ret;
bufptr = buf;//记录"剩余长度"的地址
ret=MQTTPacket_decode(bufchar, value);
//解码后的"长度"保存到(*value)中,返回值为解码前长度所占的字节数
return ret;
}
2)、可变报头
“可变报头(Variable header)”在“固定报头”和“有效载荷”之间。只有“部分控制报文”需要包含“可变报头”“可变报头”的前两个字节是一个非零的“16位报文标识符(Packet Identifier)”。客户端和服务端可彼此独立地分配报文标识符。
a、计算可变包头的位置
在接收到一个报文时,我们是不知道剩余长度是多少个字节,我可以通过最大剩余长度268435455,编码后的值为(0xFF,0xFF,0xFF,0x7F),了解“剩余长度”的解码方法。
设置剩余长度的初值为x=0;
第1个字节为0xFF,x=(0xFF & 0x7F)*1=127
第2个字节为0xFF,x=x+(0xFF & 0x7F)*128=16383
第3个字节为0xFF,x=x+(0xFF & 0x7F)*128*128=2097151
第4个字节为0x7F,x=x+(0x7F & 0x7F)*128*128*128=268435455
因此,“固定报头的字节数量”就是(1+x),这样“可变包头的起始位置”就是(2+x)。
根据上述计算,可以得出下面的程序:
/*
读"SOCKET端口sn"的数据,长度为len个字节,保存到buffer[]
返回值小于0表示“socket状态无效;
返回值等于0表示“套接字号码无效”
返回值大于0表示接收到的数据长度
*/
int w5x00_read(uint8_t sn, unsigned char* buffer, int len)
{
uint8_t ret;
uint16_t size = 0;
int32_t ret_lenth;
ret=getSn_SR(sn);//获取SOCKET端口sn的状态寄存器
size=getSn_RX_RSR(sn);
/*读SOCKET端口sn的Sn_RX_RSR寄存器,获取该端口的接收缓冲区的数据长度*/
if( (ret == SOCK_ESTABLISHED) && (size>0) )
{
ret_lenth=recv(sn, buffer, len);
//读"SOCKET端口sn"的数据,长度为len个字节,保存到buffer[]
//如果ret_lenth<0,则表示“socket状态无效”
return ret_lenth;
}
return SOCK_ERROR;//返回值等于0表示“错误”
}
//函数功能:读剩余长度字段,解码后将“剩余长度”保存到(* value)
//返回值为“剩余长度字段”的字节数
static int decodePacket(uint8_t sn, int* value)
{
unsigned char i;
int multiplier = 1;
int len = 0;
int rc = -1;
*value = 0;//假定解码后的“剩余长度”值为0
do
{
++len;//计算“剩余长度”的字节数
if (len > 4)//剩余长度字段超过4个字节,表示错误
{
rc = -1; /* bad data */
goto exit;
}
i=0;rc=-1;//为读数据做准备
rc = w5x00_read(sn, &i, 1);
//读"SOCKET端口sn"的数据,长度为1个字节,保存到i
//返回值小于0表示“socket状态无效;
//返回值等于0表示“套接字号码无效”
//返回值大于0表示接收到的数据长度
if (rc != 1) goto exit;//如果没有读到数据,则表示错误
*value += (i & 127) * multiplier;
multiplier *= 128;
} while ((i & 128) != 0);//当i的最高位为0时,则结束while循环
exit:
return len;
}
b、可变报头的报文标识符
PUBLISH(QoS>0时), PUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE, SUBACK,UNSUBSCIBE,UNSUBACK的报文里,均含有“可变报头的报文标识符(Packet Identifier)字段”。见下表:
报文标识符最小值为1,最大值为65535。从上表可知,可变报头只有两个字节。
3)、有效载荷
部分“控制报文”包含“Payload有效载荷”。
由上表可知,只有部分“控制报文”包含“Payload有效载荷”。
对于“PUBLISH控制报文”来说,有效载荷就是“消息”。
下面通过“SUBACK的报文,”来了解有效载荷。
//解码buf[]中的SUBACK的报文,“效载荷的值”保存在grantedQoSs[count],执行成功,则返回1
//grantedQoSs[count]中的值和Publish报文中的QoS相等
// packetid返回的是报文标识符
//maxcount表示有效载荷的最大字节数,suback的有效载荷字节数为1
//*count表示有效载荷的索引,由于suback的有效载荷字节数为1,因此(*count)=0
//grantedQoSs[count]是要返回的“效载荷的值”
//buf[]为接收缓冲区,buflen为buf[]的最大字节总数
int MQTTDeserialize_suback(unsigned short* packetid, int maxcount, int* count, int grantedQoSs[], unsigned char* buf, int buflen)
{
MQTTHeader header = {0};
unsigned char* curdata = buf;
unsigned char* enddata = NULL;
int rc = 0;
int mylen;
header.byte = readChar(&curdata);
//相当于header.byte = *curdata;curdata++;读buf[0]
if (header.bits.type != SUBACK) goto exit;//若报文类型错误,则退出
rc = MQTTPacket_decodeBuf(curdata, &mylen);
//解码后的"剩余长度"保存到mylen中,rc为解码前长度所占的字节数
curdata =curdata + rc;//指向“可变报头的报文标识符”
enddata = curdata + mylen;//enddata指向有效载荷的结束位置
if (enddata - curdata < 2) goto exit;
*packetid = readInt(&curdata);
//读“可变报头的报文标识符”
//curdata=curdata+2,此时指向“有效载荷”的起始位置
*count = 0;
while (curdata < enddata)//查询有效载荷的数据
{
if (*count > maxcount)
{
rc = -1;
printf("SUBACK error!!!\r\n");
goto exit;
}
grantedQoSs[(*count)] = readChar(&curdata);
(*count)++;
}
rc = 1;
exit:
FUNC_EXIT_RC(rc);
return rc;
}
4、“QoS 0,QoS 2,QoS 2”的理解
//服务质量:
//QOS0=0表示消息仅发送一次,无需对方确认是否收到,适用于非关键性消息(如非实时数据更新);
//QOS1=1表示消息至少被发送一次,且至少要收到一次对方发送的“与原发的报文标识符相同的PUBACK报文”;
//QOS2=2表示消息至少被发送一次,且至少要收到一次对方发送的“与原发的报文标识符相同的PUBACK报文”;
enum QoS {
QOS0,
QOS1,
QOS2
};
“QoS 0”意思消息发布后,不需要对方确认是否收到。
对于QoS 0的分发协议,发送者必须发送“QoS等于0和DUP等于0”的PUBLISH报文。
“QoS1”意思消息发布后,需要对方确认收到。
对于QoS 1的分发协议,“发送方”每次发送的“PUBLISH报文”必须包含一个未使用的“报文标识符”,假定为0xFFFF,且“QoS等于1和DUP等于0”。“发送方”必须将这个PUBLISH报文看作是未确认的 ,直到从“接收方”那里收到对应的“PUBACK报文”。“接收方”发送的“PUBACK报文标识符”也必须是0xFFFF,要与“收到的PUBLISH报文”相同。“接收方”发送“PUBACK报文”之后,就可以再次将含有“相同报文标识符”的“PUBLISH报文”当作一个新的消息,并忽略它的DUP标志的值。
“QoS2”意思消息发布后,“发布方”需要“接收方”确认收到,然后“接收方”告诉“发布方”可以释放这个“报文标识符”,最后“发布方”告诉“接收方”发布完成。
对于QoS 2的分发协议,“发送方”先发送发送的“PUBLISH报文”,它必须包含一个“未使用的报文标识符” ,假定为0xFFFF,且报文的QoS等于2且DUP等于0。“发送方”必须将这个“PUBLISH报文”看作是未确认的 ,直到从“接收方”那里收到一个与之对应的“PUBREC报文”为止;当“发送方”收到“PUBREC报文”后,就会发送一个“PUBREL报文”,它们都包含与“原发的PUBLISH报文”相同的“报文标识符0xFFFF”。此时这个“PUBREL报文”也会被“发送方”看作是 未确认的 ,直到从“接收方”那里收到对应的“PUBCOMP报文”为止。“发送方”收到“PUBCOMP报文”之后,就不再重发这个“PUBLISH报文”。
当“接收方”收到“PUBLISH报文”后,会发送的“PUBREC报文”,其“报文标识符”也要是0xFFFF。当“接收方”接收到“PUBREL报文”后,还需要发送“PUBCOMP报文”,其“报文标识符”也要是0xFFFF。在发送完“PUBCOMP报文”之后,“接收方”就可以接收包含“相同报文标识符”的“后续PUBLISH报文”了。
以上大部分来自文档介绍,有部分内容是我个人的理解。若有理解错误的地方,请指正。