手撕MQTT协议

0 写在前面

  • 在此文章中我将分析MQTT协议的每一个报文,并会从0开始使用网络调试助手封装MQTT数据包连接阿里云物联网平台
  • 网络调试工具(NetAssist)简介:该工具可以模拟一个TCP客户端或者TCP服务器用于网络调试,由于MQTT底层是基于TCP协议,那么我们完全可以使用此工具自己封装MQTT的数据包,之后再与阿里云物联网平台建立联系

1 上云五要素的由来

1.1 简述

  • 在之前我们知道要连接阿里云需要通过将阿里云的三元组转换为5要素才可以连接上阿里云物联网平台,那么具体转换规则是什么呢?
  • 详细规则请参考官方文档,此处我将从简阐述

1.2 创建设备

  • 为方便后续演示,我现在阿里云物联网平台下面创建一个设备,设备名字为MQTT_Test,创建后如下图所示,其处于未激活状态。
  • 进入MQTT_Test设备详细页面,查看其三元组信息如下:
{
  "ProductKey": "a1qWpd1XfAx",
  "DeviceName": "MQTT_Test",
  "DeviceSecret": "507fadba33d5522d5cb26da252e5d786"
}

1.3 域名

  • 知道三元组后我们首先来生成阿里云物联网服务器域名
  • 官方给的格式如下:
# ${YourProductKey}:产品密钥,在此处为a1qWpd1XfAx
# ${YourRegionId}:阿里云服务器所在地区,此处我的服务器在上海故为cn-shanghai
${YourProductKey}.iot-as-mqtt.${YourRegionId}.aliyuncs.com
  • 最后进行替换即为:a1qWpd1XfAx.iot-as-mqtt.cn-shanghai.aliyuncs.com

1.4 端口

  • 固定为1883

1.5 ClientID

  • 官方格式如下:
# ${ClientID}:实际设备Client ID信息,一般为设备的MAC地址。此处我写为test
# ${Mode}:即安全模式,此处我是用TCP直连,故设置为3
#           securemode=3    TCP直连模式,无需设置SSL/TLS信息
#           securemode=2    TLS直连模式,需要设置SSL/TLS信息
# ${SignMethod}:加密算法类型,有hmacmd5和hmacsha1,此处我选择后者
${ClientID}|securemode=${Mode},signmethod=${SignMethod}|

注意:

  • 不要遗漏参数之间及最后的竖线(|)
  • 设置参数时,需确保参数值中或参数值的前后均没有空格
  • 最后进行替换即为:test|securemode=3,signmethod=hmacsha1|

1.6 用户名

  • 官方格式如下:
# ${DeviceName}:设备名称,此处为MQTT_Test
# ${ProductKey}:产品密钥,此处为a1qWpd1XfAx
${DeviceName}&${ProductKey}
  • 最后进行替换即为:MQTT_Test&a1qWpd1XfAx

1.7 密码

  • 密码是通过hmacsha1算法将明文加密后所生成的密文
  • 明文官方格式如下:
# ${clientId}:设备ID,此处为test
# ${deviceName}:设备名称,此处为MQTT_Test
# ${productKey}:产品密钥,此处为a1qWpd1XfAx
# ${timestamp}:时间戳,可以不进行设置,此处我不设置
clientId${clientId}deviceName${deviceName}productKey${productKey}timestamp${timestamp}
  • 替换后的明文为:clientIdtestdeviceNameMQTT_TestproductKeya1qWpd1XfAx
  • 现在需使用hmacsha1算法将明文加密为密文,其中的加密密钥对应三元组中的DeviceSecret,此处为507fadba33d5522d5cb26da252e5d786
  • 现在我们打开加密网站,选择hmacsha1加密并填入明文与加密密钥,如下图所示:
  • 最后我们的密码是:e6df0319042b8d9eac9535521bddab802df9c89c

1.8 验证

  • 使用mqttfx客户端软件,输入上述生成的5要素,可发现能够连接上阿里云物联网平台

2 MQTT报文

  • MQTT一共只有14种报文,即客户端与服务器之间交互的数据格式只有14种

2.1 CONNECT(连接)报文

2.1.1 报文概览

  • CONNECT报文由固定报头字段、剩余长度字段、可变报头字段、有效载荷构成
  • 当客户端与服务器建立TCP连接之后接下来就是要建立MQTT连接,此时客户端需要发送给服务端一个CONNECT报文
  • 此报文示例格式如下:

2.1.2 固定报头字段

  • 固定报头格式为:
  • 第一个字节高4位表示MQTT报文类型,其中CONNECT报文类型为1,故高4位为0001
  • 第一个字节低4位表示报文标志位,其中CONNECT报文为0,故低4位为0000

2.1.3 剩余长度字段

  • 剩余长度等于可变报头长度加上负载长度
  • 剩余长度字段最大为4字节,其中每个字节的第8位(最高位)为标志位,表示长度是否溢出。剩下的7个位来表示数据长度。由此可见,真正用来表示数据长度的只有7位,即可代表数据长度为127字节。换句话说,如果只用一个字节来表示剩余字段长度的话,那么要求可变报头长度加上负载长度最小为0字节,最大为127字节。如果超过127字节的话就需要使用更多的字节来表示剩余长度字段
  • 举例:
    • 假如可变报头长度加上负载长度长度为115个字节,其小于127字节,那么只需要使用1个字节表示即可,那么剩余长度字段表示为0x73(0111 0011),其标志位为0
    • 假如可变报头长度加上负载长度长度为200个字节,其大于127字节,那么需要使用2个字节表示即可,故剩余长度字段表示为0xC801
  • 解码算法:
    • 0x73(0111 0011)= 115 * 128^0 = 115
    • 0xC8 01(1100 1000 0000 0001) = 72 * 128^0 + 1 * 128^1 = 200
    • 0xFF FF 7F(1111 1111 1111 1111 0111 1111) = 127 * 128^0 + 127 * 128^1 + 127 * 128^2 = 2097151
#include 

int main(void) { 
    unsigned int length, value;
    int base = 1;
    int i = 0;
    unsigned char code[4] = {0xff, 0xff, 0x7f};
    do
    {
        length = code[i++];
        value += (length & 127) * base;
        base = base * 128;
        if (value > 128 * 128 * 128)
        {
            printf("code is too long\n");
            break;
        }
    }while((length & 128) != 0);
    printf("length=%d\n", value);
    return 0;
}
  • 编码算法:
    • 循环除128再取余
#include 

int main(void) { 
    unsigned char byte_num[4] = {0};
    unsigned shang, yushu;
    unsigned int length = 72;
    shang = length;
    yushu = length;
    int i = 0;
    do
    {
        yushu = shang % 128;
        shang = shang / 128;
        if (shang > 0)
        {
            byte_num[i++] = yushu | 128;
        }
        else
        {
            byte_num[i] = yushu;
        }
    }while(shang > 0);
    printf("%d\n", i);
    for (int j = 0; j <= i; j++)
    {
        printf("%#x ", byte_num[j]);
    }
}

2.1.4 可变报头字段

2.1.4.1 协议名(Protocol Name)

  • 协议名是表示协议名 MQTT 的UTF-8编码的字符串。其长度为4字节

2.1.4.2 协议级别 (Protocol Level )

  • 客户端用8位的无符号值表示协议的修订版本。对于3.1.1版协议,协议级别字段的值是4(0x04)。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x01(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接。

2.1.4.3 连接标志(Connect Flags)

  • 连接标志字段包含一些用于指定MQTT连接行为的参数。它还指出有效载荷中的字段是否存

    在。

  • 每个位分别表示如下意思:
    • bit0(保留位):服务端必须验证bit0是否为0,如果不为0必须断开客户端连接
    • bit1(清理会话):置1的话表示服务器不会保留客户端的历史数据,当客户端重连时候则是以一个新会话开始;置0表示服务器会保留当前客户端的消息,当客户端掉线重连后服务器会向客户端推送历史消息
    • bit2(遗嘱):即当客户端掉线后,服务器会向关联这个客户端的设备发送遗嘱消息。如果遗嘱标志被设置为1,连接标志中的Will QoS和Will Retain字段会被服务端用到,同时有效载荷中必须包含Will Topic和Will Message字段;如果遗嘱标志被设置为0,连接标志中的Will QoS和Will Retain字段必须设置为0,并且有效载荷中不能包含Will Topic和Will Message字段。
    • bit3 & bit4(遗嘱QoS):指定发布遗嘱消息时使用的服务质量等级,等级为0,1,2但不能为3。
    • bit5(遗嘱保留):
  • 如果遗嘱标志被设置为0:遗嘱保留(Will Retain)标志也必须设置为0 ;
  • 如果遗嘱标志被设置为1:如果遗嘱保留被设置为0,服务端必须将遗嘱消息当作非保留消息发布;如果遗嘱保留被设置为1,服务端必须将遗嘱消息当作保留消息发布
  • bit6(用户名标志):如果用户名(User Name)标志被设置为0,有效载荷中不能包含用户名字段;如果用户名(User Name)标志被设置为1,有效载荷中必须包含用户名字段
  • bit7(密码标志):如果密码(Password)标志被设置为0,有效载荷中不能包含密码字段;如果密码(Password)标志被设置为1,有效载荷中必须包含密码字段;如果用户名标志被设置为0,密码标志也必须设置为0

2.1.4.4 保持连接(Keep Alive)

  • 一个以秒为单位的时间间隔,表示为一个16位(2字节)的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其它的控制报文可以发送,客户端必须发送一个PINGREQ报文用来保活(即告诉服务器我还没在线,不要踢掉我)。
  • 不管保持连接的值是多少,客户端任何时候都可以发送PINGREQ报文,并且使用PINGRESP报文判断网络和服务端的活动状态。
  • 如果保持连接的值非零,并且服务端在1.5倍的保持连接时间内没有收到客户端的控制报文,它必须断开客户端的网络连接,认为客户端已经掉线,进而断开与客户端的连接。
  • 保持连接的实际值是由应用指定的,一般是几分钟。允许的最大值是18小时12分15秒

2.1.5 有效载荷(Payload)

  • CONNECT报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头字段中的[连接标志](#2.1.4.3 连接标志(Connect Flags))决定是否包含这些载荷。如果包含的话,载荷必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。并且每个载荷前面都应该跟上这个载荷的长度字段(2字节)

2.1.6 阿里云CONNECT报文封装示例

  • [固定报头字段](#2.1.2 固定报头字段),字节流为:
10               # 固定报头
  • [剩余长度字段](#2.1.3 剩余长度字段),此时未知,使用??代替。字节流为:
10               # 固定报头  
??               # 剩余长度
  • [协议名](#2.1.4.1 协议名(Protocol Name)),此处为长度加上协议名(MQTT),字符串MQTT转成十六进制后为4D 51 54 54,其长度为4字节,字节流为:
10               # 固定报头  
??               # 剩余长度
00 04            # 协议名长度
4D 51 54 54      # 协议名
  • [协议级别](#2.1.4.2 协议级别 (Protocol Level )),此处使用MQTT3.1.1版本为0x04,字节流为:
10               # 固定报头  
??               # 剩余长度
00 04            # 协议名长度
4D 51 54 54      # 协议名
04               # 协议基本,MQTT3.1.1
  • [连接标志](#2.1.4.3 连接标志(Connect Flags)),此处每次与服务器建立连接都希望是一个新会话,因此Clean Sessin(bit1)置1;未使用遗嘱,因此bit2/3/4/5均置0;使用了用户名密码,故bit6/7置1;组合来看,此字节为0xC2(1100 0010)。字节流为:
10               # 固定报头  
??               # 剩余长度
00 04            # 协议名长度
4D 51 54 54      # 协议名
04               # 协议基本,MQTT3.1.1
C2               # 连接标志
  • [保持连接](#2.1.4.4 保持连接(Keep Alive)),此处选择为60秒,即0x3C。字节流为:
10               # 固定报头  
??               # 剩余长度
00 04            # 协议名长度
4D 51 54 54      # 协议名
04               # 协议基本,MQTT3.1.1
C2               # 连接标志
3C               # 保活时间
  • [有效载荷](#2.1.5 有效载荷(Payload)):由于没有设置遗嘱,故此处载荷只有客户端标识、用户名、密码
  • 客户端标识(Client ID):在[1.5](#1.5 ClientID)中已经生成,为test|securemode=3,signmethod=hmacsha1|,将其转换为16进制后如下,其长度为38字节(0x26)
00 26           # 客户端标识长度
74 65 73 74 7C  # 客户端标识
73 65 63 75 72 
65 6D 6F 64 65 
3D 33 2C 73 69 
67 6E 6D 65 74 
68 6F 64 3D 68 
6D 61 63 73 68 
61 31 7C 
  • 用户名:在[1.6](#1.6 用户名)中已经生成,为MQTT_Test&a1qWpd1XfAx,将其转换为16进制后如下,其长度为21字节(0x15)
00 15           # 用户名长度
4D 51 54 54 5F  # 用户名
54 65 73 74 26 
61 31 71 57 70 
64 31 58 66 41 
78 
  • 密码:在[1.7](#1.7 密码)中已经生成,为e6df0319042b8d9eac9535521bddab802df9c89c,将其转换为16进制后如下,其长度为40字节(0x28)
00 28           # 密码长度
65 36 64 66 30  # 密码
33 31 39 30 34 
32 62 38 64 39 
65 61 63 39 35 
33 35 35 32 31 
62 64 64 61 62 
38 30 32 64 66 
39 63 38 39 63 
  • 所有字节流信息求得后可计算剩余长度字段为115字节(除剩余长度字段外的所有字段字节总和),经过[2.1.3](#2.1.3 剩余长度字段)编码算法计算后剩余字段表示为0x73,故最终字节流如下:
10               # 固定报头  
73               # 剩余长度
00 04            # 协议名长度
4D 51 54 54      # 协议名
04               # 协议基本,MQTT3.1.1
C2               # 连接标志
3C               # 保活时间
00 26            # 客户端标识长度
74 65 73 74 7C   # 客户端标识
73 65 63 75 72 
65 6D 6F 64 65 
3D 33 2C 73 69 
67 6E 6D 65 74 
68 6F 64 3D 68 
6D 61 63 73 68 
61 31 7C 
00 15            # 用户名长度
4D 51 54 54 5F   # 用户名
54 65 73 74 26 
61 31 71 57 70 
64 31 58 66 41 
78 
00 28            # 密码长度
65 36 64 66 30   # 密码
33 31 39 30 34 
32 62 38 64 39 
65 61 63 39 35 
33 35 35 32 31 
62 64 64 61 62 
38 30 32 64 66 
39 63 38 39 63 

# 换个格式表示
10 73 00 04 4d 51 54 54 04 c2 00 3c 00 26 74 65 73 74 7c 73 65 63 75 72 65 6d 6f 64 65 3d 33 2c 73 69 67 6e 6d 65 74 68 6f 64 3d 68 6d 61 63 73 68 61 31 7c 00 15 4d 51 54 54 5f 54 65 73 74 26 61 31 71 57 70 64 31 58 66 41 78 00 28 65 36 64 66 30 33 31 39 30 34 32 62 38 64 39 65 61 63 39 35 33 35 35 32 31 62 64 64 61 62 38 30 32 64 66 39 63 38 39 63
  • 我们将最后字节流作为TCP协议负载发送给阿里云物联网服务器,会看到服务器会给一个响应报文。
  • 去云服务器控制台查看,MQTT_Test设备已成功上线,由于我们只是向服务器发送了连接报文,之后就没有发送任何报文和心跳报文,所以在60秒过后服务器会断开与客户端的连接
  • 此刻我们可以通过WireSHark抓包软件来查看报文信息,如图:
  • 第1、2、3条数据是本地客户端与阿里云物联网平台建立TCP连接时候所经历的3次握手阶段
  • 第4条数据则是本地客户端向阿里云服务器发送的连接报文
  • 第5条数据是阿里云服务器给本地客户端的TCP响应信息,表示服务器收到客户端的数据
  • 第6条数据是阿里云服务器给本地客户端的MQTT响应信息,表示服务器收到客户端的连接请求
  • 第7条数据是本地客户端给阿里云服务器发送的响应信息,表示客户端已经收到服务器的响应数据
  • 第8、9、10、11条数据是TCP断开连接时候所经历的4次挥手阶段
  • 至此,一个客户端连接服务器的过程分析完毕

2.2 CONNACK(确认连接请求)报文

2.2.1 报文概览

  • 当服务器收到客户端的的CONNECT(连接请求)报文后。需要给客户端返回一个CONNACK(确认连接请求)报文
  • 如果客户端在某一段时间内没有收到服务端的CONNACK(确认连接请求)报文,客户端应该关闭网络连接

2.2.2 固定报头字段

  • 在[MQTT报文](#2 MQTT报文)中讲到,CONNACK报文的类型值为2,故byte1的高4位为0010,保留位为0000,故字节1为0x20

2.2.3 剩余长度字段

  • 表示可变报头的长度,对于CONNACK报文来说这个值等于2

2.2.4 可变报头字段

  • 可变报头字段表示如下:

2.2.4.1 连接确认标志( Connect Acknowledge Flags)

  • 第1个字节是连接确认标志,bit7~bit1是保留位且必须设置为0。 第0 (SP)位是当前会话(Session Present)标志。

2.2.4.2 当前会话标志(Session Present)

  • 为连接确认标志的第0位
  • 如果服务端收到清理会话(Clean Session)标志为1的连接([在连接标志中设置](#2.1.4.3 连接标志(Connect Flags))),除了将CONNACK报文中的返回码设置为0之外,还必须将CONNACK报文中的当前会话设置(Session Present)标志为0
  • 如果服务端收到一个Clean Session为0的连接,当前会话标志的值取决于服务端是否已经保存了ClientId对应客户端的会话状态。如果服务端已经保存了会话状态,它必须将CONNACK报文中的当前会话标志设置为1。如果服务端没有已保存的会话状态,它必须将CONNACK报文中的当前会话设置为0。还需要将CONNACK报文中的返回码设置为0
  • 一旦完成了会话的初始化设置,已经保存会话状态的客户端将期望服务端维持它存储的会话状态。如果客户端从服务端收到的当前的值与预期的不同,客户端可以选择继续这个会话或者断开连接。客户端可以丢弃客户端和服务端之间的会话状态,方法是:断开连接,将[清理会话标志](#2.1.4.3 连接标志(Connect Flags))设置为1,再次连接,然后再次断开连接。
  • 如果服务端发送了一个包含非零返回码的CONNACK报文,它必须将当前会话标志设置为0

2.2.4.3 连接返回码 (Connect Return code)

  • 是可变报头字段的第2个字节
  • 连接返回码字段使用一个字节的无符号值,如果服务端收到一个合法的CONNECT报文,但出于某些原因无法处理它,服务端应该尝试发送一个包含非零返回码的CONNACK报文。客户端在接收到一个包含非零返回CONNACK报文后,必须关闭网络连接
  • 各种返回码含义如下表:

2.2.5 有效载荷

  • CONNACK报文没有有效载荷

2.2.6 阿里云CONNACK报文解读

  • 当我们发送一个CONNECT(连接请求报文)后,服务器会返回CONNACK报文,如下图:

[图片上传失败...(image-71434a-1611540580613)]

  • 返回CONNACK字节码为:20 02 00 00,解读如下:
20          # 报文类型,2代表CONNACK报文
02          # 可变报头长度
00 00       # 返回码,00表示连接被服务器接收

2.3 DISCONNECT(断开连接报文)

2.3.1 报文概览

  • DISCONNECT报文是客户端发给服务端的最后一个控制报文。表示客户端正常断开连接。

2.3.2 固定报头字段

  • 如图,字节1的高4位表示报文类型,14表示DISCONNECT报文
  • 服务端必须验证所有的保留位都被设置为0,如果它们不为0必须与客户端断开连接

2.3.3 剩余长度字段

  • 剩余长度字段等于可变报头大小段加上负载大小,DISCONNECT没有可变报头与负载,故也没有剩余长度字段

2.3.4 可变报头字段

2.3.5 有效载荷

2.3.6 阿里云DISCONNECT报文封装示例

  • DISCONNECT报文设置如下:
e0      # 报文类型 
00      # 保留位

# 换个格式表示
e0 00
  • 使用网络调试助手连接阿里云,之后再断开阿里云(通过发送E0 00断开连接)
  • 使用WireSHark抓包后如下,当发送断开连接请求之后就断开的TCP连接

2.4 PINGREQ(心跳请求)报文

2.4.1 报文概览

  • 客户端在连接上服务器之后为了告诉服务器自己还在线,需要定期向服务器发送PINGREQ报文
  • 服务端必须发送PINGRESP报文响应客户端的PINGREQ报文

2.4.2 固定报头字段

  • 如图,字节1的高4位表示报文类型,12表示PINGREQ报文

2.4.3 剩余长度字段

  • 剩余长度字段等于可变报头大小段加上负载大小,PINGREQ没有可变报头与负载,故也没有剩余长度字段

2.4.4 可变报头字段

2.4.5 有效载荷

2.4.6 阿里云PINGERQ报文封装示例

  • PINGERQ报文设置如下:
C0       # 报文类型 
00       # 保留位

# 换个格式表示
C0 00
  • 通过网络调试助手向阿里云服务器发送一个PINGREQ报文,服务器回复了一个PINGRESP报文
  • 抓包后如下:

2.5 PINGRESP(心跳响应)报文

2.5.1 报文概览

  • 服务端发送PINGRESP报文响应客户端的PINGREQ报文,表示服务端还活着。

2.5.2 固定报头字段

  • 如图,字节1的高4位表示报文类型,13(0xD0)表示PINGREQ报文

2.5.3 剩余长度字段

  • 剩余长度字段等于可变报头大小段加上负载大小,PINGREAP没有可变报头与负载,故也没有剩余长度字段

2.5.4 可变报头字段

2.5.5 有效载荷

2.5.6 阿里云PINGERQ报文封装示例

  • 参考[2.4.6](#2.4.6 阿里云PINGERQ报文封装示例)

2.6 SUBSCRIBE(订阅)报文

2.6.1 报文概览

  • 客户端向服务端发送SUBSCRIBE报文用于订阅主题
  • SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端

2.6.2 固定报头字段

  • 如图,字节1的高4位表示报文类型,8表示SUBSCRIBE报文
  • SUBSCRIBE控制报文的字节1的低4位是保留位,必须分别设置为0、0、1、0

2.6.3 剩余长度字段

  • 剩余长度字段等于可变报头字段的长度(2字节)加上有效载荷的长度

2.6.4 可变报头字段

2.6.4.1 报文标识符

  • 很多控制报文的可变报头部分包含一个两字节的报文标识符字段。这些报文是PUBLISH(QoS > 0时), PUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE,SUBACK,UNSUBSCIBE,UNSUBACK。
  • SUBSCRIBE,UNSUBSCRIBE和PUBLISH(QoS大于0)控制报文必须包含一个非零的16位报文标识符(Packet Identifier)。客户端每次发送一个新的这些类型的报文时都必须分配一个当前未使用的报文标识符。如果一个客户端要重发这个特殊的控制报文,在随后重发那个报文时,它必须使用相同的标识符。当客户端处理完这个报文对应的确认后,这个报文标识符就释放可重用。
  • QoS 1的PUBLISH对应的是PUBACK, QoS 2的PUBLISH对应的是PUBCOMP
  • 与SUBSCRIBE或UNSUBSCRIBE对应的分别是SUBACK或UNSUBACK
  • 总结:报文标识符的出现是为了区分报文,假如我们连续发了2条订阅报文,一条为报文1,一条为报文2。此刻服务器会返回订阅结果报文,如果此时只返回了报文1的响应报文,那么客户端可以马上知道订阅报文2订阅主题失败。如果没有报文标识符的话,客户端压根就不知道是哪个报文订阅主题失败
  • 下表说明了那些报文需要报文标识符:
  • 报文标识符,此处标识符为10,即报文编号为10

2.6.5 有效载荷

  • 载荷构成如下图:
  • 所订阅主题长度,用2字节表示
  • 主题过滤器:即你想订阅的主题
  • 服务质量等级(QoS):使用最后一个字节的bit1和bit0表示

2.6.6 阿里云SUBSCRIBE报文封装示例

  • [固定报头字段](#2.6.2 固定报头字段)
82         # 报文类型+保留位
  • [剩余长度字段](#2.6.3 剩余长度字段)
??         # 可变报头+负载
  • [可变报头字段](#2.6.4 可变报头字段)
00 08      # 设为8号报文
  • [有效载荷](#2.6.5 有效载荷)
  • 主题长度:31字节
00 1f          # 要订阅的主题名长度(31字节)
  • 主题名:此处选用阿里云服务器中的自定义主题/a1qWpd1XfAx/MQTT_Test/user/get,将其转换为16进制后如下:
2F 61 31 71 57     # 要订阅的主题名
70 64 31 58 66 
41 78 2F 4D 51 
54 54 5F 54 65 
73 74 2F 75 73 
65 72 2F 67 65 
74 
  • 服务质量选择为0
00        # 保留位+QoS(0)
  • 所有字段凑在一起组成了SUBSCRIBE报文
82            # 报文类型+保留位
24            # 可变报头+负载=36字节(0x24)
00 08         # 设为8号报文
00 1f         # 要订阅的主题名长度(31字节)
2F 61 31 71 57# 要订阅的主题名
70 64 31 58 66 
41 78 2F 4D 51 
54 54 5F 54 65 
73 74 2F 75 73 
65 72 2F 67 65 
74 
00            # 保留位+QoS(0)

# 改个形式
82 24 00 08 00 1f 2f 61 31 71 57 70 64 31 58 66 41 78 2f 4d 51 54 54 5f 54 65 73 74 2f 75 73 65 72 2f 67 65 74 00
  • 使用网络调试助手发送上述字节流,可成功接收到SUBACK报文,即订阅成功

2.7 SUBACK(订阅确认)报文

2.7.1 报文概览

  • 服务端发送SUBACK报文给客户端,用于确认它已收到并且正在处理SUBSCRIBE报文。

2.7.2 固定报头字段

  • 订阅确认报文固定报头字段如下图所示,字节1高4位表示报文类型,SUBACK类型为0x90

2.7.3 剩余长度字段

  • 等于可变报头的长度加上有效载荷的长度。

2.7.4 可变报头字段

  • 即SUBSCRIBE报文中的[报文标识符](#2.6.4.1 报文标识符),表示回复的是哪一条订阅报文

2.7.5 有效载荷

  • 即一串返回码,等于订阅成功的报文的QoS。例如,我的SUBSCRIBE报文中需要按照QoS等于1的服务质量去订阅某个报文,如果订阅成功,那么服务器将返回SUBACK,其中的有效载荷字段就是对应SUBSCRIBE报文中规定的QoS等级,即1(0x01)
  • 注意:阿里云服务器SUBACK中的有效载荷字段均为1(0x01),较为特殊!!!
  • 0x00:订阅成功,QoS为0
  • 0x01:订阅成功,QoS为1
  • 0x02:订阅成功,QoS为2
  • 0x80:订阅失败

2.7.6 阿里云SUBACK示例

  • 在[2.6.6](#2.6.6 阿里云SUBSCRIBE报文封装示例)中,当成功发送一个SUBSCRIBE报文去订阅某个主题后,服务器回复了一个字节流,为:
90 03 00 08 01

# 含义如下
90   # 固定报头字段,表示SUBACK报文
03   # 剩余长度字段,等于可变报头加负载,此处为3字节
00 08# 对应2.6.6中的报文标识符(8号报文)
01   # 订阅结果,阿里云物联网服务器均为01

2.8 UNSUBSCRIBE(取消订阅)报文

2.8.1 报文概览

  • 客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题

2.8.2 固定报头字段

  • 字节1的高4位表示报文类型,其中UNSUBSCRIBE为10(0xA)
  • 字节1的低4位必须为0010

2.8.3 剩余长度字段

  • 等于可变报头的长度加上有效载荷的长度

2.8.4 可变报头字段

  • 可变报头包含一个报文标识符,可以理解为报文编号,参见[2.6.4.1](#2.6.4.1 报文标识符)

2.8.5 有效载荷

  • 即想取消订阅的主题名
  • 主题名前面都有一个2字节的长度,代表主题名长度,例子如下:

2.8.6 阿里云UNSUBSCRIBE报文封装示例

  • 在[2.6.6](#2.6.6 阿里云SUBSCRIBE报文封装示例)中我们订阅了/a1qWpd1XfAx/MQTT_Test/user/get主题,在这里我们来取消订阅该主题
  • 最终格式如下:
A2            # 报文类型+保留位
23            # 可变报头+负载=35字节(0x23)
00 08         # 设为8号报文
00 1f         # 要取消订阅的主题名长度(31字节)
2F 61 31 71 57# 要取消订阅的主题名
70 64 31 58 66 
41 78 2F 4D 51 
54 54 5F 54 65 
73 74 2F 75 73 
65 72 2F 67 65 
74 

# 改个形式
A2 23 00 08 00 1f 2f 61 31 71 57 70 64 31 58 66 41 78 2f 4d 51 54 54 5f 54 65 73 74 2f 75 73 65 72 2f 67 65 74 00
  • 通过网络助手调试如下:

2.9 UNSUBACK(取消订阅响应)报文

2.9.1 报文概览

  • 服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文

2.9.2 固定报头字段

  • 字节1高4位表示报文类型,其中UNSUBACK表示为0xB

2.9.3 剩余长度字段

  • 可变报头的长度,对UNSUBACK报文这个值等于2

2.9.4 可变报头字段

  • 可变报头为等待确认的UNSUBSCRIBE报文的报文标识符,即[2.8.4](#2.8.4 可变报头字段)

2.9 5 有效载荷

  • UNSUBACK报文没有有效载荷

2.9.6 阿里云UNSUBACK示例

  • 在[2.8.6](#2.8.6 阿里云UNSUBSCRIBE报文封装示例)示例中,我们在发送UNSUBSCRIBE报文后服务器会有一个回复,详情如下:
B0     # 报文类型,B表示SUBACK报文
02     # 剩余长度字段
00 08  # 报文编号,对应2.8.4中的报文编号,此处为8号报文

2.10 PUBLISH(发布消息)报文

2.10.1 报文概览

  • 使用PUBLISH报文向服务器的某个主题发送消息

2.10.2 固定报头字段

  • 字节1的高4位表示报文类型,PUBLISH报文为0x3
  • 字节低4为分别为:
  • bit0:保留标志。
    • RETAIN=1:表示服务器需要保留这条消息,当有其他设备订阅这个主题时,服务器会将保留的消息推送给订阅这个主题的设备(客户端)。例如,张三丰向太极拳主题发送了白鹤亮翅的招式秘籍,且这条消息RETAIN为1,那么服务器将会保留白鹤亮翅秘籍,几天后,当有其他想学太极的人(张无忌)订阅了张三丰的太极拳主题后,服务器会立刻将张三丰的白鹤亮翅等秘籍推送给张无忌
    • RETAIN=0:服务器不保留消息
  • bit1&bit2:服务质量等级
  • bit3:重发标志,如果DUP标志被设置为0,表示这是客户端或服务端第一次请求发送这个PUBLISH报文。如

    果DUP标志被设置为1,表示这可能是一个早前报文请求的重发。 对于QoS 0的消息,DUP标志必须设置为0 重发,此标志需置1

2.10.3 剩余长度字段

  • 可变报头的长度加上有效载荷的长度

2.10.4 可变报头字段

2.10.4.1 主题名

  • 即向哪个主题发布消息,其前面需要指明该主题长度

2.10.4.2 报文标识符

  • 只有当QoS等级是1或2时,PUBLISH报文中才包含报文标识符字段
  • 例子如下:主题名为 “a/b”,长度等于3,报文标识符为 “10”

2.10.5 有效载荷

  • 即我们要发布的消息,可谓纯文本字符串或者json字符串等
  • 其大小可以用固定报头中的剩余长度字段的值减去可变报头的长度。由于剩余长度的值最大为268 435 455字节,所以有效载荷长度不是无限大

2.10.6 响应

  • 服务器需要按照QoS等级选择是否回复客户端发送的PUBLISH报文,具体情况见下表:

2.10.7 阿里云PUBLISH报文封装

2.10.7.1 Qos=0的报文

  • 此处我们需要向服务器对应设备MQTT_Test下的/a1qWpd1XfAx/MQTT_Test/user/update主题发布QoS=0的消息,即字符串Hello。封装后如下:
30 29 00 22 2f 61 31 71 57 70 64 31 58 66 41 78 2f 4d 51 54 54 5f 54 65 73 74 2f 75 73 65 72 2f 75 70 64 61 74 65 48 65 6c 6c 6f
# 详情如下
30              # 报文类型为PUBLISH,且要求服务器不保留消息,QoS为0,不为重发的消息
29              # 剩余长度为41字节
00 22           # 主题长度为34字节
2f 61 31 71 57  # 主题名/a1qWpd1XfAx/MQTT_Test/user/update
70 64 31 58 66 
41 78 2f 4d 51 
54 54 5f 54 65 
73 74 2f 75 73 
65 72 2f 75 70 
64 61 74 65
48 65 6c 6c 6f  # 发送的消息Hello
  • 使用网络调试工具发送后如下,此处因为发布的消息等级QoS为0,所以服务器不会回复任何信息
  • 在阿里云服务器的日志服务里面我们可以看到消息已经成功发送至云平台

2.10.7.2 QoS=1的报文

  • 此处我们需要向服务器对应设备MQTT_Test下的/a1qWpd1XfAx/MQTT_Test/user/update主题发布QoS=1的消息,即字符串World。封装后如下:
32 2B 00 22 2f 61 31 71 57 70 64 31 58 66 41 78 2f 4d 51 54 54 5f 54 65 73 74 2f 75 73 65 72 2f 75 70 64 61 74 65 00 07 57 6F 72 6C 64 
# 详情如下
32              # 报文类型为PUBLISH,且要求服务器不保留消息,QoS为1,不为重发的消息
2B              # 剩余长度为43字节
00 22           # 主题长度为34字节
2f 61 31 71 57  # 主题名/a1qWpd1XfAx/MQTT_Test/user/update
70 64 31 58 66 
41 78 2f 4d 51 
54 54 5f 54 65 
73 74 2f 75 73 
65 72 2f 75 70 
64 61 74 65
00 07           # 报文标识符0x07
57 6F 72 6C 64  # 发送的消息World
  • 使用网络调试工具发送后如下,此处因为发布的消息等级QoS为1,所以服务器回复了消息
  • 在阿里云服务器的日志服务里面我们可以看到消息已经成功发送至云平台

2.11 PUBACK(发布确认)报文

2.11.1 报文概览

  • PUBACK报文是对QoS为1等级的PUBLISH报文的响应

2.11.2 固定报头字段

  • PUBACK类型为0x4

2.11.3 剩余长度字段

  • 可变报头的长度。对PUBACK报文这个值等于2

2.11.4 可变报头字段

  • 等待确认的PUBLISH报文的报文标识符

2.11.5 有效载荷

  • PUBACK报文没有有效载荷

2.11.5 阿里云PUBACK示例

  • 在[2.10.7.2](#2.10.7.2 QoS=1的报文)中我们发送了一个QoS为1的报文,阿里云也回复了一个PUBACK报文,详情如下:
40 02 00 07 
# 详情如下
40     # 报文类型,此处为PUBACK报文
02     # 剩余长度2字节
00 07  # 可变报头,对应7号报文

2.12 PUBREC、PUBREL、PUBCOMP

  • 由于阿里云物联网平台不支持QoS=2的消息发布,故此处对上述3个响应报文不做过多阐述,感兴趣的同学可参考网上资料。

你可能感兴趣的:(手撕MQTT协议)