背景

之前工作中参与有关协议调试的时候,发现对于协议帧的解析是比较重要的。

参考:《MQTT协议 -- 消息报文格式》《基于STM32实现MQTT》《MQTT协议从服务端到客户端详解》

英文资料:《MQTT Control Packets》

MQTT协议数据包结构

此图是 PUBLISH 报文的组成

在MQTT协议中,一个MQTT数据包由:固定头(Fixed header)、可变头(Variable header)、消息体(payload)三部分构成。

  • (1)固定头(Fixed header)。存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识。
  • (2)可变头(Variable header)。存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容。
  • (3)消息体(Payload)。存在于部分MQTT数据包中,表示客户端收到的真正内容。 与可变头一样,在有些协议类型中有消息内容,有些协议类型中没有消息内容。

MQTT固定头

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

固定头包含两部分内容,首字节(Byte1)和剩余消息报文长度(从Byte2开始,最多占用4个字节)。

Bit 7 6 5 4 3 2 1 0
byte 1 MQTT报文类型 报文类型标志位
byte 2.. 剩余长度

MQTT Control Packet type 报文类型

Byte1的 Bit[7-4]: MQTT Control Packet type,报文类型。总共可以表示16种报文类型,其中0000和1111是保留字段。

报文类型 Bit[7-4]值 数据方向 描述
保留 0000 禁用 保留
CONNECT 0001 Client ---> Server 客户端连接到服务器
CONNACK 0010 Server ---> Client 连接确认
PUBLISH 0011 Client <--> Server 发布消息
PUBACK 0100 Client <--> Server 发不确认
PUBREC 0101 Client <--> Server 消息已接收(QoS2第一阶段)
PUBREL 0110 Client <--> Server 消息释放(QoS2第二阶段)
PUBCOMP 0111 Client <--> Server 发布结束(QoS2第三阶段)
SUBSCRIBE 1000 Client ---> Server 客户端订阅请求
SUBACK 1001 Server ---> Client 服务端订阅确认
UNSUBACRIBE 1010 Client ---> Server 客户端取消订阅
UNSUBACK 1011 Server ---> Client 服务端取消订阅确认
PINGREQ 1100 Client ---> Server 客户端发送心跳
PINGRESP 1101 Server ---> Client 服务端回复心跳
DISCONNECT 1110 Client ---> Server 客户端断开连接请求
保留 1111 禁用 保留

Flags specific to each MQTT Control Packet type 报文类型标志位

Byte1的 Bit[3-0]: Flags specific to each MQTT Control Packet type,字节位用作某些报文类型的标志位。

实际上只有少数报文类型有控制位,如下表。

报文类型 固定头标记 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 报文中有关标志位的意义与用法,容易对学习造成困扰。

剩余长度

剩余长度的计算从理解上是一大难点。注意理解好下面2句加粗的句子。

Remaining Length意思是剩余长度,即可变头(Variable header) + 消息体(payload)的长度。

剩余长度从Byte 2开始,最长可达4字节。即:剩余长度范围是Byte2到Byte5。

计算: 剩余长度 所占用的字节数

MQTT协议规定,byte2(最高到byte5)的bit7(最高位)若为1,则表示还有后续字节存在。

记 N 为 消息报文中的 第n个byte, (2 < N < 5), (Byte 5 的 bit7肯定是0)

如果byte N 的 bit7 是1,那么Byte M (M = N + 1, M < 5 ) 作为剩余长度的一部分,可用于继续计算字节长度;

如果byte N 的 bit7 是0,那么Byte M (M = N + 1, M < 5 ) 就不能看作是剩余长度的一部分计算字节长度。

所以单个字节最大值:01111111,即:0x7F,10进制为127。

MQTT协议最多允许4个字节表示剩余长度。那么最大长度为:0xFF,0xFF,0xFF,0x7F。

计算:剩余长度 所代表长度(以Byte为单位)

消息长度可以简单理解为128进制的数据,4位长度最大可以表示128128128*128Byte=256MB。

注意:长度的计算有些特别,即低位在前,高位在后。

以下是消息长度的长度范围:

占用字节 长度范围的最小值 长度范围的最大值
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)

剩余长度 的有关计算

为了方便读者理解,我们举例并计算一下。

若现收到一段MQTT数据报文: 0x20 0x02 0xAA 0xBB ,一共4个字节

根据 MQTT 数据结构可知,0x20 代表了 CONNACK 报文

第二个字节开始,与 剩余长度有关Remaining Length

显然 , Byte2 (0x02)中的byte2[7] 为 0,代表后面的0xAA 0xBB与剩余长度无关。

再有,Byte2 (0x02) Byte2[6:0] 值 = 2,代表后续的报文长度还有2个字节,它们是0xAA 0xBB

(我们先不关心与固定头无关的部分0xAA 0xBB代表了什么意思,实际上是我乱举例的。)

至此,固定头计算完毕。

这个例子比较简单,我们再来看一段稍微复杂一点的报文。

若现收到一段以 0x30 0x9B 0x01 ... 开头的 MQTT数据报文:

根据 MQTT 数据结构可知,0x30 代表了 PUBLISH 报文

第二个字节开始,与 剩余长度有关Remaining Length

显然,Byte 2 (0x9B) 中的bit7 为1,代表后面的0x01与剩余长度有关。

再有,Byte 3 (0x01) 中的bit7 为0,代表剩余长度`有关的报文在此字节为止。

知道了剩余长度有关的报文字节是 0x9B0x01 ,那么就是计算具体的剩余长度。

注意:要低位在前,高位在后。

Byte 2 中的 0x9b 中能够计算长度的只有 byte2[6:0] 即 (0x9b)&~(0x80) = 0x1B

那么: len = (0x01)* 128 + 0x1B = 155 ,即:后面的报文还有155个字节。

我们也可以通过这个例子知道报文的长度实际上是128进制的存储方式。

至此,固定头计算完毕。

以此类推,我们很容易知道,如果一段报文以 0x20 0xFF 0xFF 0xFF 0x7E开头,那么剩下的还有 266338303 个 报文字节

\[len = (7E_{16})*128^3 + (7F_{16})*128^2 + (7F_{16})*128^1 + (7F_{16})*128^0 = 266338303
\]

我们甚至可以写出一段"报文字节剩余长度计算"的c语言代码。

/*
# Copyright from Web, All Rights Reserved
#
# File Name: endecode_for_rl.c
# Created : Mon, Feb 3, 2020 7:47:02 PM
*/ #include <stdio.h>
typedef unsigned int uint32;
typedef unsigned short uint16;
typedef unsigned char uint8; /*
* buf 存放剩余长度 段的 容器
* length 设置的长度
* 返回值: buf 占用的 字节数
* */
int MQTTPacketSetPacketLenth(uint8 *buf, unsigned long length)
{
// ref : https://blog.csdn.net/weixin_42381351/article/details/89397776
unsigned long rc = 0;
unsigned char d;
do {
d = length % 128;
length /= 128;
/* if there are more digits to encode, set the top bit of this digit */
if (length > 0) {
d |= 0x80;
}
buf[rc++] = d;
} while (length > 0);
return rc;
} /*
* buf 作为 剩余长度 帧 的首地址
* */
unsigned long MQTTPacketGetPacketLenth(uint8 *buf)
{
// 改编自中文版文档中的伪代码
char encodedByte;
unsigned int multiplier = 1;
unsigned long rc = 0;
int i = 0; do {
encodedByte = buf[i++];
rc += (encodedByte & 0x7f) * multiplier;
if (multiplier > 128*128*128)
break; //throw Error(Malformed Remaining Length)
else
multiplier *= 128;
}while ((encodedByte & 0x80) != 0);
return rc;
} int main(void)
{
int i;
unsigned long rl;
int length_step; uint8 packet[256] = {0x80, 0x80, 0x80, 0x01}; // 除了剩余长度以外,没有其他部分
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld\n", rl); length_step = MQTTPacketSetPacketLenth(packet, 16383);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 16383\n", rl); length_step = MQTTPacketSetPacketLenth(packet, 2097151);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 2097151\n", rl); length_step = MQTTPacketSetPacketLenth(packet, 268435455);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 268435455\n", rl); length_step = MQTTPacketSetPacketLenth(packet, 321);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 321\n", rl);
return 0;
}

MQTT 可变头

Variable Header的意思是可变化的消息头部。MQTT数据包中包含一个可变头,它驻位于可变头(Variable header)与消息体(payload)之间。

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

学习固定头的时候,我们可以一个个字节位进行分析计算,但学习可变头我个人认为应该根据具体的报文类型进行完整的分析。

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

可变头的内容因数据包类型而不同,较常的应用是做为包的标识:

Bi 7 6 5 4 3 2 1 0
byte 1 包标签符(MSB)
byte 2… 包标签符(LSB)

使用大端序(big-endian,高位字节在低位字节前面)。这意味着一个16位的字在网络上表示为最高有效字节(MSB),后面跟着最低有效字节(LSB)。

后面的字段也用到了这种编码,这里需要特意强调一下:

有关字符串,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

头2个字节(byte1、byte2)组成为一个完整的无符号的16位数字,代表从byte3开始后面字符串字节长度。

后面的n个字节才是字符串真正的内容。

前后共2+n个字节。

可变头的 报文标识符

Packet Identifier 也可以叫做 Message Identifier,以后在文章中出现的 报文标识符,都以 Packet Identifier 指代。

报文标识符用来区分报文,特别是在重发的报文中用来标识是否是同一个报文,并在需要应答的场景中用于确定是对哪个发送报文的应答。可变报头的报文标识符(Packet Identifier)字段存在于在多个类型的报文里(占用2个字节)。这些报文是:

PUBLISH(QoS > 0时)PUBACKPUBRECPUBRELPUBCOMPSUBSCRIBE, SUBACKUNSUBSCRIBEUNSUBACK

其实是这样的。因为 在 MQTT 协议 中 ,有些报文在发出以后 需要有收到对应响应报文;为了避免不被混淆,所以才用 Packet Identifier 来 "绑定" 处理这些消息。如果没有 Packet Identifier 那么在通信中,连续多条一样的报文就变得无法处理。发送者不知道现在第几条消息被有效处理了,不知道第几条消息被拒绝了。

Bit 7 - 0
byte 1 报文标识符 MSB
byte 2 报文标识符 LSB

Package ID默认是从1(0x01)开始并自增,最大为255(0xff)。

SUBSCRIBEUNSUBSCRIBEPUBLISH(QoS大于0)控制报文必须包含一个非零的16位报文标识符(Packet Identifier)。

  • 客户端每次发送一个新的这些类型的报文时都必须分配一个当前未使用的报文标识符
  • 如果一个客户端要重发这个特殊的控制报文,在随后重发那个报文时,它必须使用相同的标识符

当客户端处理完这个报文对应的确认(ACK, CMP)后,这个报文标识符就释放可重用。

例如:QoS 1的PUBLISH对应的是PUBACK,QoS 2的PUBLISH对应的是PUBCOMP,与SUBSCRIBE或UNSUBSCRIBE对应的分别是SUBACKUNSUBACK

发送一个QoS 0的PUBLISH报文时,相同的条件也适用于服务端。

QoS等于0的PUBLISH报文不能包含报文标识符。

PUBACK, PUBREC, PUBREL报文必须包含与最初发送的PUBLISH报文相同的报文标识符。类似地,SUBACKUNSUBACK必须包含在对应的SUBSCRIBE和UNSUBSCRIBE报文中使用的报文标识符。

需要报文标识符的控制报文在 下表 - 包含报文标识符的控制报文 Control Packets that contain a Packet Identifier`中列出。

控制报文 报文标识符字段
PUBLISH YES(QoS > 0)
PUBACK YES
PUBREC YES
PUBREL YES
PUBCOMP YES
SUBSCRIBE YES
SUBACK YES
UNSUBSCRIBE YES
UNSUBACK YES

客户端和服务端彼此独立地分配报文标识符。因此,客户端服务端组合使用相同的报文标识符可以实现并发的消息交换。

换句话说, 假设客户端发送标识符为0x1234的PUBLISH报文,它有可能会在收到那个报文的PUBACK之前,先收到服务端发送的另一个不同的但是报文标识符也为0x1234的PUBLISH报文。

    Client                     Server

   PUBLISH Packet Identifier=0x1234--->

   <--PUBLISH Packet Identifier=0x1234

   PUBACK Packet Identifier=0x1234--->

   <--PUBACK Packet Identifier=0x1234

上边消息客户端给服务端发送一条消息,使用的Packet ID是0x1234,同时服务端给客户端发送了一条消息,也使用了Packet ID 0x1234。

然后客户端回复服务端,发送PUBACK,最后是客户端收到服务端的回复PUBACK。

Payload消息体

并非所有的报文类型需要包含Payload。

下表 - 包含有效载荷的控制报文 Control Packets that contain a Payload 列出了需要有效载荷的控制报文。

控制报文 是否包含Payload
CONNECT 需要
CONNACK 不需要
PUBLISH 可选
PUBACK 不需要
PUBREC 不需要
PUBREL 不需要
PUBCOMP 不需要
SUBSCRIBE 需要
SUBACK 需要
UNSUBSCRIBE 需要
UNSUBACK 不需要
PINGREQ 不需要
PINGRESP 不需要
DISCONNECT 不需要

根据上表我们可以知道,Payload消息体作为MQTT数据包的第三部分,被包含于CONNECTSUBSCRIBESUBACKUNSUBSCRIBEPUBLISH这些类型报文里面:

1)CONNECT,消息体内容主要是:客户端的ClientID、订阅的Topic、Message以及用户名和密码。

2)SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS。

3)SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。

4)UNSUBSCRIBE,消息体内容是要订阅的主题。

5)PUBLISH, 消息体内容是要实际消息的内容(虽然是可选的,可是PUBLISH的报文确实比较常用的)。

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

接下来我们根据不同的报文类型进行分析。

MQTT 协议学习:002- 通信报文的构成的更多相关文章

  1. MQTT 协议学习:004-MQTT建立通信与 CONNECT 、CONNACK 报文

    背景 上一讲 MQTT 协议学习:通信报文的构成介绍了在MQTT通信中,各报文的通信流程:从本讲开始,我们开始介绍实际中使用的报文,以及它们的组成. CONNECT - 连接请求 报文 客户端到服务端 ...

  2. MQTT 协议学习: 总结 与 各种定义的速查表

    背景 经过几天的学习与实操,对于MQTT(主要针对 v3.1.1版本)的学习告一段落,为了方便日后的查阅 本文链接:<MQTT 协议学习: 总结 与 各种定义的速查表> 章节整理 MQTT ...

  3. 深度剖析MQTT协议的整个通信流程

    http://www.elecfans.com/d/587483.html MQTT,目前物联网的最主要的协议,基本所有收费的云平台都是基于MQTT协议,比如机智云,和所有的开放云平台比如中国移动的o ...

  4. MQTT 协议学习:003-MQTT通信流程介绍

    背景 有关博文:通信报文的构成 . 上一讲说到可变头与消息体要结合不同的报文类型才能够进行分析(实际上,官方的文档的介绍顺序就是这样的) 那么,我们就来具体看看有关的报文类型. 在此之前 我们捋一捋完 ...

  5. MQTT 协议学习:001-搭建MQTT通信环境,并抓包测试

    背景 目的:了解MQTT 通信的有关概念与流程:方便推算某些数据与文档描述是否一致. 为了能够在保证学习质量的前提下,降低配置环境的门槛,我们将服务器搭建在windwos中,实行内网间的MQTT协议访 ...

  6. MQTT 协议学习:007-Keep Alive 连接保活 与 对应报文(PINGREQ、PINGRESP)

    背景 keep alive 是 CONNECT 报文中可变头的一部分. 我们提到过 Broker 需要知道 Client 是否非正常地断开了和它的连接,以发送遗愿消息.实际上 Client 也需要能够 ...

  7. MQTT 协议学习: QoS等级 与 会话

    背景 QoS 等级 与 通信的流程有关,直接影响了整个通信.而且篇幅比较长,所以我觉得应该单独拎出来讲一下. 概念 QoS 代表了 服务质量等级. 设置上,由2 位 的二进制控制,且值不允许为 3(0 ...

  8. MQTT 协议学习:000-有关概念入门

    背景 从本章开始,在没有特殊说明的情况下,文章中的MQTT版本均为 3.1.1. MQTT 协议是物联网中常见的协议之一,"轻量级物联网消息推送协议",MQTT同HTTP属于第七层 ...

  9. MQTT 协议学习:008-在STM32上移植MQTT

    前言 通过前面了解MQTT有关概念.分析了有关的报文,我们对于这个协议也有了更深的认识.但纸上谈来终觉浅,绝知此事要躬行. 本文参考:<STM32+W5500+MQTT+Android实现远程数 ...

随机推荐

  1. Rect Native 使用

    参见 Rect Native 中文官网. 依赖环境: Homebrew.npm.Node.js.Watchman(监测Bug和文件变化,触发指定操作).flow(JS静态类型检查仪,以方便找出代码中错 ...

  2. Hash Table(散列表)

    这篇主要是基础的数据结构学习,写的时候才明白了书上说到的一些问题,由于该篇仅仅只是对这种数据结构进行一个理解,所以很基础,关于h(x)函数也只是简单的运用了除法散列,然后为了应对冲突,我用的是链接法. ...

  3. Dart语言学习(十三) Dart Mixins 实现多继承

    Mixins Mixins(混入功能)相当于多继承,也就是说可以继承多个类,使用with关键字来实现Mixins的功能. 那么多个类中有相同的方法时候,会被覆盖吗?覆盖的先后是什么? class A{ ...

  4. Web基础了解版09-Cookie-Session

    Cookie Cookie 是一种服务器发送给浏览器以键值对形式存储小量信息的技术. 当浏览器首次请求服务器时,服务器会将一条信息封装成一个Cookie发送给浏览器,浏览器收到Cookie,会将它保存 ...

  5. 2019年springmvc面试高频题(java)

    前言 2019即将过去,伴随我们即将迎来的又是新的一年,过完春节,马上又要迎来新的金三银四面试季.那么,作为程序猿的你,是否真的有所准备的呢,亦或是安于本职工作,继续做好手头上的事情. 当然,不论选择 ...

  6. 奖学金(0)<P2007_1>

    奖学金 (scholar.pas/c/cpp) [问题描述] 某小学最近得到了一笔赞助,打算拿出其中一部分为学习成绩优秀的前5名学生发奖学金.期末,每个学生都有3门课的成绩:语文.数学.英语.先按总分 ...

  7. 二分查找及几种变体的Python实现

    1. 在不重复的有序数组中,查找等于给定值的元素 循环法 def search(lst, target): n = len(lst) if n == 0: return -1 low = 0 high ...

  8. SQL查询效率注意事项 2011.12.27

    一.查询条件精确,针对有参数传入情况 二.SQL逻辑执行顺序 FROM-->JOIN-->WHERE-->GROUP-->HAVING-->DISTINCT-->O ...

  9. 用Jackson进行Json序列化时的常用注解

    Jackson时spring boot默认使用的json格式化的包,它的几个常用注解: @JsonIgnore 用在属性上面,在序列化和反序列化时都自动忽略掉该属性 @JsonProperty(&qu ...

  10. 二次代价函数、交叉熵(cross-entropy)、对数似然代价函数(log-likelihood cost)(04-1)

    二次代价函数 $C = \frac{1} {2n} \sum_{x_1,...x_n} \|y(x)-a^L(x) \|^2$ 其中,C表示代价函数,x表示样本,y表示实际值,a表示输出值,n表示样本 ...