MQTT控制报文

做MQTT通讯之前,最好能找到成功案例,然后再看看MQTT通信协议《MQTT-3.1.1-CN》,一定会事倍功半。不了解其协议,在阅读程序时,会有点困难。下面就是MQTT控制报文的相关知识点,供以后查阅。

1MQTT二进制数据

MQTT16位整数使用大端序(big-endian),即高位字节在低位字节前面。

2UTF-8编码字符串的结构:

每个字符串都有一个“两字节的长度字段”作为前缀,它给出这个字符串UTF-8编码的字节数。格式如下:

字符串长度的最高有效字节(MSB) + 字符串长度的最低有效字节(LSB) + UTF-8编码的字符数据

如果“字符串长度”大于0,则后面的“UTF-8编码的字符数据”就是有效的。

3MQTT控制报文的结构:

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编码后为0x00127编码后为0x7F128编码后为0x800x0116383编码后为0xFF0x7F,等。

16383举例:

16383128求余数和商:16383%128=12716383/128=127

由于商大于0,因此余数的最高位置1,就编码为0x7F|0x80=0xFF

将商127128求余数和商:127%128=127127/128=0

由于商等于0,因此余数的最高位置0,就编码为0x7F|0x00=0x7F

所以,16383被编码后,就是0xFF0x7F了。

变长编码方案的算法:

/*

对“消息长度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端口snSn_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、可变报头的报文标识符

PUBLISHQoS>0时), PUBACKPUBRECPUBRELPUBCOMPSUBSCRIBE, SUBACKUNSUBSCIBEUNSUBACK的报文里,均含有“可变报头的报文标识符(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[]为接收缓冲区,buflenbuf[]的最大字节总数

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 0QoS 2QoS 2”的理解

//服务质量:

//QOS0=0表示消息仅发送一次,无需对方确认是否收到,适用于非关键性消息(如非实时数据更新);

//QOS1=1表示消息至少被发送一次,且至少要收到一次对方发送的“与原发的报文标识符相同的PUBACK报文”;

//QOS2=2表示消息至少被发送一次,且至少要收到一次对方发送的“与原发的报文标识符相同的PUBACK报文”;

enum QoS {

QOS0,

QOS1,

QOS2

};

QoS 0意思消息发布后,不需要对方确认是否收到。

对于QoS 0的分发协议,发送者必须发送“QoS等于0DUP等于0”的PUBLISH报文。

QoS1意思消息发布后,需要对方确认收到。

对于QoS 1的分发协议,“发送方”每次发送的“PUBLISH报文”必须包含一个未使用的“报文标识符”,假定为0xFFFF,且“QoS等于1DUP等于0”。“发送方”必须将这个PUBLISH报文看作是未确认的 ,直到从“接收方”那里收到对应的“PUBACK报文”。“接收方”发送的“PUBACK报文标识符”也必须是0xFFFF,要与“收到的PUBLISH报文”相同。“接收方”发送“PUBACK报文”之后,就可以再次将含有“相同报文标识符”的“PUBLISH报文”当作一个新的消息,并忽略它的DUP标志的值。

QoS2意思消息发布后,“发布方”需要“接收方”确认收到,然后“接收方”告诉“发布方”可以释放这个“报文标识符”,最后“发布方”告诉“接收方”发布完成。

对于QoS 2的分发协议,“发送方”先发送发送的“PUBLISH报文”,它必须包含一个“未使用的报文标识符” ,假定为0xFFFF,且报文的QoS等于2DUP等于0。“发送方”必须将这个“PUBLISH报文”看作是未确认的 ,直到从“接收方”那里收到一个与之对应的“PUBREC报文”为止;当“发送方”收到“PUBREC报文”后,就会发送一个“PUBREL报文”,它们都包含与“原发的PUBLISH报文”相同的“报文标识符0xFFFF”。此时这个“PUBREL报文”也会被“发送方”看作是 未确认的 ,直到从“接收方”那里收到对应的“PUBCOMP报文”为止。“发送方”收到“PUBCOMP报文”之后,就不再重发这个“PUBLISH报文”。

当“接收方”收到“PUBLISH报文”后,会发送的“PUBREC报文”,其“报文标识符”也要是0xFFFF。当“接收方”接收到“PUBREL报文”后,还需要发送“PUBCOMP报文”,其“报文标识符”也要是0xFFFF。在发送完“PUBCOMP报文”之后,“接收方”就可以接收包含“相同报文标识符”的“后续PUBLISH报文”了。

以上大部分来自文档介绍,有部分内容是我个人的理解。若有理解错误的地方,请指正。 

<think>好的,我需要帮助用户了解MQTT协议的报文结构或格式。首先,我应该回顾已有的引用内容,看看哪些信息可以直接使用。根据用户提供的引用,特别是引用[1]、[2]、[3]和[4],这些资料详细讨论了MQTT报文格式,特别是CONNECT和PUBLISH报文结构。 首先,MQTT协议的基本报文结构分为固定报头、可变报头和有效载荷三个部分。我需要明确这三个部分的组成和作用。固定报头包含控制报文类型和标志位,以及剩余长度。引用[2]和[4]提到了固定报头的结构,比如控制报文类型占第一个字节的高4位,标志位占低4位,剩余长度使用可变字节编码。 接下来是可变报头,这部分根据不同的报文类型有不同的内容。例如,CONNECT报文中的可变报头包括协议名、协议级别、连接标志和保持连接时间,这些在引用[2]和[3]中有详细说明。而PUBLISH报文的可变报头可能包含主题名和报文标识符,引用[4]给出了具体的例子。 有效载荷部分通常包含实际传输的数据,比如CONNECT报文中的客户端ID、用户名和密码,PUBLISH报文中的消息内容。引用[3]提到载荷可能由多个长度加内容的字段组成,取决于可变头中的标志位。 用户可能需要具体的例子来理解结构,例如CONNECT报文的字节序列分析,如引用[2]中的示例,或者PUBLISH报文结构,如引用[4]中的QoS0实例。此外,代码示例可能有助于用户更好地理解如何实际操作,例如引用[5]中提到的C#代码实现,但用户问题中没有直接请求代码,所以可能不需要深入代码细节,但可以简要提及。 需要注意的是,回答要符合用户的要求,使用中文,数学表达式用$...$,独立公式用$$...$$,但MQTT报文结构更多是字段说明,可能不需要数学公式,但如果有涉及长度计算或位运算的地方,可能需要应用。例如,剩余长度的可变字节编码可能需要解释,每个字节的最高位表示是否继续,后面7位为数据,可以用数学表达式说明,如$$ \text{剩余长度} = \sum_{i=0}^{n} (b_i \& 0x7F) \times 128^i $$。 此外,用户要求生成相关问题,需要根据内容提出相关问题,例如报文类型、连接过程、QoS级别的影响等。 最后,确保引用的正确标注,例如在提到CONNECT报文结构时引用[2]和[3],提到PUBLISH时引用[4]等。回答结构要清晰,分点说明每个部分的结构,并给出示例,帮助用户逐步理解。</think>### MQTT协议报文结构解析 #### 一、基本报文结构 MQTT报文由三部分组成:**固定报头**、**可变报头**和**有效载荷**,具体结构如下: 1. **固定报头** - **控制报文类型**(1字节高4位):定义报文类型(如CONNECT、PUBLISH等),共16种类型。 - **标志位**(1字节低4位):不同报文类型的控制标志,例如PUBLISH报文的QoS级别和保留标志[^2][^4]。 - **剩余长度**(1-4字节):表示可变报头+有效载荷的总字节数,采用可变字节编码,计算公式为: $$ \text{剩余长度} = \sum_{i=0}^{n} (b_i \& 0x7F) \times 128^i $$ 其中$b_i$为每个字节的值[^4]。 2. **可变报头** 根据报文类型不同,内容差异较大: - **CONNECT报文**:包含协议名(如`MQTT`)、协议级别(如`0x05`表示MQTT 5.0)、连接标志(用户名/密码、清理会话等)、保持连接时间[^3]。 - **PUBLISH报文**:包含主题名(UTF-8字符串)和报文标识符(仅QoS≥1时存在)[^4]。 3. **有效载荷** 部分报文(如CONNECT、PUBLISH)包含实际数据: - **CONNECT报文**:客户端ID、用户名、密码(由连接标志决定是否包含)。 - **PUBLISH报文**:消息内容(如`hello mqtt demo`)。 --- #### 二、典型报文示例 1. **CONNECT报文**(连接请求) - 固定报头:`0x10`(类型1,标志位0)。 - 可变报头:协议名`MQTT`(长度+内容)、协议级别`0x04`(MQTT 3.1.1)、连接标志`0xC2`(清理会话=1,密码=1)、保持时间`0x003C`(60秒)。 - 有效载荷:客户端ID`Client01`、用户名`user`、密码`pass`(按长度+内容格式排列)。 2. **PUBLISH报文**(QoS0消息) - 固定报头:`0x30`(类型3,QoS=0,保留=0)。 - 可变报头:主题`aliyun_mqtt_test`(长度+内容)。 - 有效载荷:消息`hello mqtt demo`。 --- #### 三、关键字段解析 1. **连接标志(Connect Flags)** 包含8个二进制位,控制是否清理会话、是否包含用户名/密码等。例如: $$ \text{0xC2} = 1100\,0010 \rightarrow \text{清理会话=1,密码=1} $$ [^3] 2. **QoS级别对PUBLISH的影响** - QoS0:无报文标识符,不保证送达。 - QoS1/2:需报文标识符(2字节),支持重传机制。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值