MQTT原理及案例

MQTT 协议是当今世界上最受欢迎的物联网协议,没有之一。MQTT 协议为设备提供了稳定、可靠、简单易用的通信基础,截至目前通过 MQTT 协议连接的设备已经过亿,广泛应用于 IoT、M2M 等领域。本篇将从最基础的知识开始,向您讲解 MQTT 协议的原理与应用。

目前 MQTT 主流版本有 MQTT3.1.1 和 MQTT5。MQTT5 完全兼容 MQTT3.1.1,是在 MQTT3.1.1 的基础上进行完善补充。目前 MQTT3.1.1 的使用人数还是更多,所以本文用 MQTT3.1.1 来讲解。

1. MQTT是什么

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)协议是基于 TCP/IP 协议栈构建的异步通信消息协议,是一种轻量级的、客户端服务端架构的、发布/订阅模式的消息传输协议。

MQTT协议最初版本是在1999年建立的,发明人是 Andy Stanford-Clark 和 Arlen Nipper。MQTT 是他们为了利用卫星通讯监控输油管道所开发的协议,由此可见,MQTT 就是专为低带宽、高延迟或不可靠的网络而设计的。

MQTT 协议特点:

  • 简单易用,方便集成
  • 安全可靠,支持TLS/SSL加密和认证机制
  • 轻量级,占用带宽小,支持多种消息传输模式
  • 灵活性,可知设备连接状态,可控数据传输质量

2. MQTT原理

在 MQTT 协议通讯中,最重要的两个角色是服务端和客户端。客户端向一「主题」「发布」消息,服务端处理并推送给「订阅」了该「主题」的其他客户端。

这么说是不是一头雾水?我打个比方,将整个 MQTT 比作我们熟悉的视频软件,一一对应关系如下。

假如你是张三,一名普通的抖音用户,你关注了良许的抖音账号。在这里,张三跟良许不会直接产生关系,而是会通过抖音服务器。抖音服务器就是「服务端」,所有抖音用户就是「客户端」,你关注良许的这个动作,就叫作「订阅」。

 

 

良许如果「发布」了一条视频,那么张三、李四、王五、老六,等等所有关注了良许的粉丝都会收到这个视频推送。这是因为抖音里没有主题的概念,只要良许有发视频,粉丝都会收到推送。

假如抖音也有主题的概念,发布的视频都带有主题的属性。那么,良许发布了编程、副业、职场、吃喝拉撒相关主题的视频,而张三只订阅了吃喝拉撒这个「主题」,那么只有当良许发布了吃喝拉撒这个主题的视频,张三才会收到这个视频。而如果发布了编程、副业相关的视频,张三不会收到任何通知。

 

 

这就是 MQTT 的基础概念。

2.1 服务端

MQTT 服务端通常是一台服务器,它充当着 MQTT 信息传输的中心节点。其主要功能是接收来自 MQTT 客户端的信息并将其传递给其他 MQTT 客户端。此外,MQTT 服务端还负责管理客户端,确保客户端之间的通信畅通无阻,并确保 MQTT 消息被正确接收和准确投递。

 

 

服务端一般就是云平台,OneNET、阿里云、腾讯云等;也可以用 EMQ 或 Mosquitto 自己搭建服务端。

2.2 客户端

MQTT 客户端可以向服务端通过「发布」发送信息,也可以从服务端「订阅」来收取信息。

客户端一般就是我们的单片机,STM32、C51、树莓派等。

2.3 主题

在 MQTT 通讯中,客户端订阅的是一个个「主题」。MQTT 服务端在管理信息通讯时,使用「主题」来控制。

2.4 发布与订阅的特点

  • 相互独立:客户端相互独立,彼此没有直接联系,不用知道对方的任何状态、情况。
  • 空间分离:客户端只要连接同一个 MQTT 通讯网络,无论是互联网或者局域网都可以通讯。
  • 时间异步:客户端的发布与订阅无需同步。若有客户端断连,服务端保存信息,待客户端上线后推送。

3. MQTT报文

MQTT 协议通过交换预定义的 MQTT 控制报文来通信。MQTT 控制报文简称 MQTT 报文,接下来我将详细介绍 MQTT 报文。

3.1 报文结构

一个 MQTT 报文由固定报头、可变报头、有效载荷三部分组成:

  • 固定报头(Fixed header),所有 MQTT 报文有,表示报文类型及报文的分组类标识。
  • 可变报头(Variable header),部分 MQTT 报文有,报文类型决定了可变头是否存在及其具体内容。
  • 有效载荷/消息体(Payload),部分 MQTT 报文有,存放报文的具体内容。

示意图如下:

 

 

整体的报文结构介绍完,下面介绍每个的细节。如果看不懂,没关系,后面会有案例,更加清晰明了。

3.2 固定报头(Fixed header)

 

 

3.2.1 消息类型(message type)

位于 byte 1 的第 7~4 位,表示 MQTT 报文类型,有下面这么多类型:

3.2.2 标志位(DUP、QoS Level、RET)

位于 byte 1 的第 3~0 位,表示 MQTT 报文的分组类标识。在不使用标识位的消息类型中,标识位被做为保留位。如果收到无效的标志时,接收端必须关闭网络连接。

  • DUP:发布消息的副本

  • QoS:发布消息的服务质量

  • RETAIN:发布保留标识

3.2.3 剩余长度(Remaining Length)

位于 byte 2 的第 3~0 位,表示当前剩余字节数,包括可变报头和负载的数据。剩余长度不包括用于编码剩余长度字段本身的字节数。

3.3 可变报头(Variable header)

某些 MQTT 报文有可变头。它在固定头和有效载荷之间。可变头的内容根据报文类型的不同而不同。可变头的报文标识符字段存在于在多个类型的报文里。

可变报头在后续的报文案例中会详细介绍。

3.4 有效载荷(Payload)

有效载荷就是应用消息,但并不是所有的报文都有有效载荷,只有部分 MQTT 报文有有效载荷,具体如下:

4. QoS,服务质量

QoS(Quality of Service,服务质量)。在数据通信的过程中,有的消息很重要,不可以丢失;有的消息不重要,丢了也没关系。所以在 MQTT 中可以配置 QoS,给不同重要的消息不同的服务质量。

MQTT 协议有三种服务质量级别:

  • QoS = 0:最多发一次
  • QoS = 1:最少发一次
  • QoS = 2:保证收一次

对于不同重要级的消息选择不同的 QoS,较为重要消息的使用 QoS = 1 和 QoS = 2。

4.1 QoS = 0:最多发一次

这种服务质量消息最多只发送一次。接收者不会发送响应,发送者也不会重试。消息可能送达一次也可能根本没送达。

想象你是一个快递员,而你要将包裹(消息)送到不同的收件人(订阅者)。QoS 级别就像你和收件人之间的交付服务等级,它决定了你在送货过程中提供的保证。QoS 0(最多发一次)相当于你将包裹送给收件人后,没有任何确认回执。你只是简单地把包裹放在门口,然后离开。在这种情况下,你无法确定包裹是否成功被收件人接收,也无法知道是否有其他人偷了这个包裹。

 

 

4.2 QoS = 1:最少发一次

服务质量确保消息至少送达一次。QoS 1 的 PUBLISH 报文的可变报头中包含一个报文标识符,需要 PUBACK 报文确认。

QoS 1(最少发一次)相当于你在送货后要求收件人给你一个回执确认。你将包裹送给收件人,然后等待他给你一个回执,告诉你已经收到包裹。如果你没有收到回执,你会重新尝试送货,直到收到回执为止。这样,你可以确保包裹被收件人接收,但可能会增加一些延迟和工作量。

 

 

4.3 QoS = 2:保证收一次

这是最高等级的服务质量,消息丢失和重复都是不可接受的。使用这个服务质量等级会有额外的开销。QoS 2 的消息可变报头中有报文标识符。QoS 2 的 PUBLISH 报文的接收者使用一个两步确认过程来确认收到。

QoS 2(保证收一次)相对于你要确认对方可以收货再发货。你在送货前给收件人发消息问他在不在家,收件人告诉你他在家,你把将包裹送给收件人,然后等待他给你一个回执,告诉你已经收到包裹。如果他没回消息,不在家,就继续发消息直到收件人回消息,告诉你他在家,再送包裹。

 

 

5. MQTT心跳机制

MQTT心跳机制可以比喻为人体的心脏跳动,两者都是为了维持正常的运行状态和连接的稳定性。

当MQTT客户端定期发送心跳包时,它就像是我们的心脏,定时地向服务器发送信号,表明自己的存在和健康状况。如果服务器在一定时间内没有接收到心跳包,就会认为客户端出现异常或离线,类似于身体出现问题时,医生会检查心跳情况来判断身体的健康状况。

客户端定时向服务端发送心跳请求(PINGREQ),告诉服务端,我还和你连接着哦。服务端收到心跳请求后,会回复一条心跳响应(PINGRESP),告诉客户端,我知道你还连着我啦。

 

 

通过心跳机制,MQTT 可以实时监测客户端的连接状态,及时发现和处理异常情况,确保通信的可靠性和稳定性。就像我们依赖心脏维持身体的正常运转一样,MQTT的心跳机制也是保障通信链路顺畅运行的重要机制之一。

6. MQTT遗嘱

遗嘱,和前面的心跳一样,有心跳请求证明客户端还连着服务端,客服端还活着。那么遗嘱就很生动形象了,客户端先把自己的遗嘱给服务端,万一客户端嘎了,服务端就可以执行遗嘱了。

MQTT遗嘱是一种机制,允许客户端在「活着」的时候设置并发送遗嘱消息,以便在客户端意外断线时由服务端公布。

意外断线指的是当客户端在没有发送 DISCONNECT 报文的情况下失去了心跳信号,这通常发生在网络故障或电池耗尽等情况下。此时,服务端会察觉到客户端的异常断开,并将客户端的遗嘱消息发布出来。然而,如果客户端正常断开连接并发送了 DISCONNECT 报文,遗嘱则不会启动,服务端也不会发布客户端的遗嘱消息。

通过合理设置和使用 MQTT 遗嘱机制,可以增强客户端在服务端管理中的作用,并提供实时的设备状态信息。

7. 报文案例

前面的 MQTT 报文是不是有很多同学听的云里雾里的,现在我们就上案例,直观明了,包教包会!

报文讲解超详细,文章会比较长哦,大家一定要耐住性子。

我们需要创建产品和设备,先给大家介绍一下产品和设备的区别:

  • 产品:一组具有相同功能定义的设备集合,产品下的资源包括设备、设备数据、设备权限、数据触发服务以及基于设备数据的应用等多种资源,用户可以创建多个产品。
  • 设备:归属于某一个产品下,是真实设备在平台的映射,用于和真实设备通过连接报文建立连接关系,平台资源分配的最小单位,设备之间的通过设备名称来区分。

通俗易懂的来说,产品就好比是苹果手机,设备就是苹果11、苹果14、苹果15、苹果15 pro、苹果15 pro max 等等。

7.1 OneNET配置

在详细介绍报文案例前需要配置一下 OneNET。动动小手跟我操作起来。

点开 OneNET 官方网址:中移坤灵 - 中国移动物联网开放平台 (10086.cn)

有账号的登录,没账号的注册一下。

 

 

登录好后点击「开发者中心」。

 

 

接下来我们先创建一个产品,之后再创建具体的设备。

关于产品与设备的区别,可以翻看上面的介绍。

 

 

 

 

 

 

 

 

 

 

 

 

接下来就开始创建产品下的具体设备。

 

 

 

 

 

 

 

 

保存好「MQTT 三元组」:

  • 设备 ID:test1

  • 产品 ID:L14FCC38pq

  • 设备密钥:b0ZHZ1BnNTdBSUV2c0dhUmJGMDBRYVJXS090VEVnMHU=

7.3 CONNECT报文

接下来我会以 MQTT 报文的 CONNECT 报文为例,详细的解释 MQTT 报文的组成与意义。

PS:我们都知道,在计算机内部计算、通信只有 0 和 1 ,但是二进制对于程序员表达来说并不方便,常用的是十六进制,因为一个两位的十六进制数刚好可以表达八位二进制数也就是一比特的值,所以我们的例子会以十六进制来表示二进制报文。

如果有不会转进制和转码的朋友可以百度一下在线进制转换和在线编码转换,也可以像我一样使用串口助手(后续会教)。

在此附上一张 ASCII 码表,里面包含了常用的字符、十六进制、二进制转换。

 

 

CONNECT 报文的作用是用于客户端请求连接到服务器。一条 CONNECT 报文就是以固定报头、可变报头、有效载荷三部分组成。示意如下:

然后把固定报头、可变报头、有效载荷三部分的十六进制数拼起来就是 CONNET 报文了。

CONNECT 报文 = 固定报头 + 可变报头 + 有效载荷

​ = 6C 69 61 6E 67 78 75 + 73 74 75 64 79 + 73 74 6D 33 32

​ = 6C 69 61 6E 67 78 75 73 74 75 64 79 73 74 6D 33 32

是不是有点像火车?一列火车拉了三个车厢,每个车厢拉的是固定报头、可变报头、有效载荷的十六进制数。

7.3.1 固定报头

固定报头由2个字节组成,结构如下图:

 

 

接着以 CONNECT 报文为例,结构如下图,固定报头中消息类型是1,标志位是 0000,所以第一字节是 00010000,转成十六进制是 10,剩余长度未知,所以 CONNECT 报文固定报头的十六进制是:10 XX。

 

 

7.3.2 可变报头

可变报头由协议名、协议级别、连接标志、保持连接四个部分组成。基本是10个字节。

以 CONNECT 报文为例:

协议名,由6个字节组成,表示协议名 MQTT 的 UTF-8 编码的字符串,结构如下图。

byte 1 和 byte 2 表示协议名后面部分的数据长度,这个长度是固定的 4 位。所以 byte 1 保存 00 ,byte 4 保存 04 。

而 byte 3~6 则固定为 M Q T T 这四个字符,每个字节保存成一个字符,对应就是 4D 51 54 54 。

所以协议名转成十六进制是 00 04 4D 51 54 54。

 

 

协议级别,只由1个字节组成,对于协议 MQTT3.1.1 协议级别的值是 4,结构如下图,转成十六进制是 04。

 

 

连接标志,也只由1个字节组成,包含一些用于指定 MQTT 连接行为的参数,还指出有效载荷中的字段是否存在。结构如下图,× 是表示不固定,可能是0,也可能是1,但是保留位(Reserved)必须是0。

从 0 到 7 位分别是保留位(Reserved)、清理会话(Clean Session)、遗嘱标志(Will Flag)、遗嘱 QoS(Will QoS)、遗嘱保留(Will Retain)、密码标志(Password Flag)、用户名标志(User Name Flag)。对应位填 1 则表示有,填 0 则无。

细心的同学就发现遗嘱 QoS(Will QoS)占了两位,这是为什么呢,因为服务质量等级有三级,我们二进制表达需要两位,如果是 00,则 QoS = 0;如果是 01,则 QoS = 1;如果是 10,则 QoS = 2。

这里我们假设连接标志有用户名标志(User Name Flag)、密码标志(Password Flag)和清理会话(Clean Session),那么连接标志的二进制表示是 11000010,转成十六进制是 C2。

 

 

保持连接,由 2 个字节组成,是一个以秒为单位的时间间隔,表示为一个 16 位的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。

 

 

假设空闲的最大时间间隔是 100 秒,则结构如下图,转成十六进制是 00 64。

 

 

我们刚刚已经以 CONNECT 报文的可变头为例,将协议名、协议级别、连接标志、保持连接都给出了例子,写出了十六进制。我们把这些十六进制数按顺序组合起来,CONNECT 报文的可变头的十六进制是:00 04 4D 51 54 54 04 C2 00 64。

7.3.3 有效载荷

有效载荷就是应用消息,部分 MQTT 报文有有效载荷,有效载荷包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段(字段要求以长度为前缀是因为有效载荷的字段要求以 UTF-8 编码格式,其特点就是以一个两字节的长度作为前缀)。

以 CONNECT 报文为例:有效载荷 = 设备 ID + 产品 ID + token。设备 ID、产品 ID、token 是在 MQTT 协议中用于服务端与客户端对接的参数,缺一不可。

设备 ID、产品 ID 是不是很熟悉?就是我们的「MQTT 三元组」,把我们刚刚保存的参数拿出来。

  • 设备 ID:test1
  • 产品 ID:L14FCC38pq
  • 设备密钥:b0ZHZ1BnNTdBSUV2c0dhUmJGMDBRYVJXS090VEVnMHU=

可是,token 是什么我们不知道,这里先挖一个坑,token 在后面会介绍,它是由设备密码和一系列参数经过加密生成的。

那么,通过前面的学习,我们知道 MQTT 报文需要将字符串转成二进制(十六进制表示),有效载荷中的字段需要以长度为前缀,那么转换过程如下,我们一步步来:

首先打开串口助手,没有的同学可以从本文开头的链接拿。

以产品 ID:L14FCC38pq 为例。

 

 

 

 

长度前缀由两位十六进制组成,数据长度 10 转换成十六进制是 0A,所以长度前缀就是 00 0A。

PS:我们算长度前缀要用十进制转十六进制计算器,网上很多进制转换器都可以用,甚至手机的计算器就有进制换算功能,当然,如果你数学很强,口算也是可以的。但是长度前缀不能用串口助手转换,如果长度前缀也用串口助手的话就会出错,如下图,数字 10 会被认为是字符的 1 和 0 转换成十六进制。

 

 

设备 ID 也和产品 ID 一样操作,每次点击发送前记得点复位计数哦,不然就重复计数啦,每步结果如下:

设备密钥需要经过加密,加密需要用 OneNET 官方的 token 生成工具。

官网下载地址:OneNET - 中国移动物联网开放平台 (10086.cn)

也可以拿文章开头提供的,也是官网下载的。

 

 

下载好 token 生成工具,打开界面如下,我来告诉大家每个空填啥。

 

 

各个参数介绍如下表:

et 的时间戳可以用这个在线工具转换,网页地址:时间戳(Unix timestamp)转换工具 - 在线工具 (tool.lu)

 

 

根据介绍,填好各个参数的空,我们选择 sha1 的加密方式,大家可以选择自己喜欢的。填好如下操作:

 

 

我们把得到的 token 按照前面利用串口助手的方式转换,过程如下:

接下来是不是很简单啦,有效载荷 = 设备 ID + 产品 ID + token = 00 05 74 65 73 74 31 00 0A 4C 31 34 46 43 43 33 38 70 71 00 80 76 65 72 73 69 6F 6E 3D 32 30 31 38 2D 31 30 2D 33 31 26 72 65 73 3D 70 72 6F 64 75 63 74 73 25 32 46 4C 31 34 46 43 43 33 38 70 71 25 32 46 64 65 76 69 63 65 73 25 32 46 74 65 73 74 31 26 65 74 3D 32 30 31 37 38 38 31 37 37 36 26 6D 65 74 68 6F 64 3D 73 68 61 31 26 73 69 67 6E 3D 38 75 6B 46 61 5A 37 74 4B 25 32 42 25 32 42 70 69 73 52 45 55 59 52 4C 59 63 53 69 52 56 77 25 33 44

注意:这里的有效载荷仅适用 OneNET,每个平台的 MQTT 报文基本一致,但是有效载荷会有一点不同。

7.3.4 组装CONNECT报文

课堂提问时间:)

由上面的讲解我们可知:

  • 一条 CONNECT 报文是以固定报头+可变报头+有效载荷三部分组成。
  • CONNECT 报文固定报头的十六进制是:10 XX。
  • CONNECT 报文的可变头的十六进制是:00 04 4D 51 54 54 04 C2 00 64
  • CONNECT 报文的有效载荷的十六进制是:00 05 74 65 73 74 31 00 0A 4C 31 34 46 43 43 33 38 70 71 00 80 76 65 72 73 69 6F 6E 3D 32 30 31 38 2D 31 30 2D 33 31 26 72 65 73 3D 70 72 6F 64 75 63 74 73 25 32 46 4C 31 34 46 43 43 33 38 70 71 25 32 46 64 65 76 69 63 65 73 25 32 46 74 65 73 74 31 26 65 74 3D 32 30 31 37 38 38 31 37 37 36 26 6D 65 74 68 6F 64 3D 73 68 61 31 26 73 69 67 6E 3D 38 75 6B 46 61 5A 37 74 4B 25 32 42 25 32 42 70 69 73 52 45 55 59 52 4C 59 63 53 69 52 56 77 25 33 44

求 CONNECT 报文。

是不是很简单,CONNECT 报文:10 XX 00 04 4D 51 54 54 04 C2 00 64 00 05 74 65 73 74 31 00 0A 4C 31 34 46 43 43 33 38 70 71 00 80 76 65 72 73 69 6F 6E 3D 32 30 31 38 2D 31 30 2D 33 31 26 72 65 73 3D 70 72 6F 64 75 63 74 73 25 32 46 4C 31 34 46 43 43 33 38 70 71 25 32 46 64 65 76 69 63 65 73 25 32 46 74 65 73 74 31 26 65 74 3D 32 30 31 37 38 38 31 37 37 36 26 6D 65 74 68 6F 64 3D 73 68 61 31 26 73 69 67 6E 3D 38 75 6B 46 61 5A 37 74 4B 25 32 42 25 32 42 70 69 73 52 45 55 59 52 4C 59 63 53 69 52 56 77 25 33 44。

这里就有同学就要问了,那 XX 还是未知啊。是的,XX 是什么还记得吗?

XX 是剩余长度,表示当前剩余字节数,包括可变报头和负载的数据的长度。剩余长度不包括用于编码剩余长度字段本身的字节数。简单来说,XX 后面有多少个字符,XX 就是多少,剩余长度就是多少。

拿出我们的串口助手,记得复位后再发送哦。

 

 

于是乎,剩余长度是159。当剩余长度小于128的时候,我们可以用单字节表示;但如果剩余长度大于等于128,我们就要用双字节甚至更多字节表示。

举几个例子:

剩余长度小于等于128的情况很好理解,直接转换就是了。但是大于 128 的情况是不是就一头雾水了,我给大家解释一下。

一个字节有 8 位二进制,最多是 11111111 应该能 256 才对,为什么单字节最多表示 127 呢?

这是因为剩余长度是使用变长度编码方案,低 7 位(第 0 到 6 位)用于编码数据,最高有效位(第 7 位)用于表示是否有更多字节,最大可以使用 4 个字节。

 

 

所以第 0 到 6 位,最多只能表示到 127。

第 7 位为 0,表示后面没有字节了;第 7 位为 1,表示后面还有字节。字节遵循低位在前,高位在后

举个例子,159 = 128*1 +31。

所以低八位为 31 并且低八位的第 7 位需要置 1,为 159(0x9F),高八位为 1(0x01)。

剩余长度 159 的十六进制就表示为 9F 01。

最后,让我们来补齐拼图的最后一块吧!

完整 CONNECT 报文:10 9F 01 00 04 4D 51 54 54 04 C2 00 64 00 05 74 65 73 74 31 00 0A 4C 31 34 46 43 43 33 38 70 71 00 80 76 65 72 73 69 6F 6E 3D 32 30 31 38 2D 31 30 2D 33 31 26 72 65 73 3D 70 72 6F 64 75 63 74 73 25 32 46 4C 31 34 46 43 43 33 38 70 71 25 32 46 64 65 76 69 63 65 73 25 32 46 74 65 73 74 31 26 65 74 3D 32 30 31 37 38 38 31 37 37 36 26 6D 65 74 68 6F 64 3D 73 68 61 31 26 73 69 67 6E 3D 38 75 6B 46 61 5A 37 74 4B 25 32 42 25 32 42 70 69 73 52 45 55 59 52 4C 59 63 53 69 52 56 77 25 33 44。

就不转成二进制了,不然要被 0 和 1 刷屏了哈哈哈哈哈。

7.3.5 CONNECT报文发送

我们还没有写单片机的程序,所以使用网络调试助手当作客户端,展示一下 CONNECT 报文效果。我们首先要知道 OneNET 服务器地址是  : 1883,地址是从 OneNET 文档中心得到的。

 

 

打开网络调试助手,没有的同学可以从本文开头的链接拿。

 

 

 

 

可以切到 OneNET 看看设备会显示在线,100 秒后会断开,断了也没关系,再次发送 CONNNECT 报文就可以啦。

 

 

如果我们写的 CONNECT 报文不对,收到报文如下并会断开连接。

 

 

那么有同学可能就好奇了,为什么 00 是表示连接成功,04表示连接失败?

因为我们作为客户端发送了一条 CONNECT 报文,OneNET 服务器作为服务端就会返回给我们一条 CONNACK 报文,我们收到的 20 02 00 00 和 20 02 00 04 ,四位字节就是服务端返回的 CONNACK 报文。关于 CONNACK 报文的详细介绍在下一节,这里我先告诉大家如果没连接成功,怎么找报文错误原因。

“00”、“04” 的意义如下图,“00” 表示连接已被服务端接受,就是连接成功;“04” 表示用户名或密码的数据格式无效,可能是有效载荷没写对。其它错误大家可以根据返回的 CONNACK 报文,找报文错误原因。

 

 

7.4 CONNACK报文

我们平时点外卖的时候,饭饭送到以后,外卖软件上需要我们点击确认收货。同样的,在 MQTT 中,客户端发送 CONNECT 报文,向服务端提出连接请求,服务端返回 CONNACK 报文,确认连接请求。

 

 

CONNACK 报文只由固定报头和可变报头两部分组成,没有有效载荷。

7.4.1 固定报头

和 CONNECT 报文大同小异,结构如下图,固定报头中消息类型是 2,标志位是 0000,所以第一字节是 00100000,转成十六进制是 20,剩余长度为2(后面只有两字节的可变报头),所以 CONNACK 报文固定报头的十六进制是:20 02。

 

 

7.4.2 可变报头

CONNACK 报文的可变报头比 CONNECT 报文简单很多,只由连接确认标志和连接返回码构成,结构如下图,× 是表示不固定,可能是0,也可能是1:

 

 

连接确认标志的位 7-1 是保留位且必须设置为 0。第 0 (SP)位是当前会话(Session Present)标志,如果没有保存的会话状态或收到清理会话命令则为 0 ,如果收到保存会话则为 1 。

连接返回码表示连接结果,有以下几种情况:

 

 

可变报头的十六进制都是未知,所以是:XX XX。

7.4.3 组装CONNACK报文

CONNACK 报文是不是简单很多啦,我就不提问了,直接公布答案:20 02 XX XX。如上面所述,连接正常的话返回值是 20 02 00 00 。

7.5 SUBSCRIBE报文

SUBSCRIBE 报文是用于客户端请求订阅主题。订阅某主题之后,所有发布的该主题的消息我们都可以收到。

一条 SUBSCRIBE 报文是以固定报头、可变报头、有效载荷三部分组成。

7.5.1 固定报头

固定报头和之前的报文大同小异,由两个字节组成,结构如下。固定报头中订阅消息类型是 8 ,保留位必须是 0010,所以第一字节是 10000010,转成十六进制是 82,剩余长度未知,所以 SUBSCRIBE 报文固定报头的十六进制是:82 XX。

 

 

7.5.2 可变报头

可变报头也由两个字节组成,结构如下,设置了报文标识符,相对于给报文起个名字。下图来自 MQTT 官方文档,它的示例是报文标识符是 10,大家可以设成自己喜欢的,也可以按照示例,我按照示例来讲解,于是转成十六进制是 00 0A。

 

 

7.5.3 有效载荷

有效载荷由主题过滤器和服务质量要求两部分组成,结构如下图。

 

 

主题过滤器可在如下页面找到,我们选择 $sys/L14FCC38pq/{device-name}/thing/property/set 作为例子订阅,服务质量要求为0。

 

 

要把 {device-name} 换成设备名,所以,字符串 $sys/L14FCC38pq/test1/thing/property/set 转成十六进制是:24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74。

别忘了,有效载荷的字段要求以长度为前缀,共 40 个字符,所以是:00 28。

服务质量要求为0,则二进制是 00000000,转成十六进制是:00。

于是,有效荷载 = 长度 + 主题过滤器 + 服务质量要求 = 00 28 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74 00。

7.5.4 组装SUBSCRIBE报文

课堂提问时间:)

由上面的讲解我们可知:

  • 一条 SUBSCRIBE 报文是以固定报头、可变报头、有效载荷三部分组成。
  • SUBSCRIBE 报文固定报头的十六进制是:82 XX。
  • SUBSCRIBE 报文的可变头的十六进制是:00 0A。
  • SUBSCRIBE 报文的有效载荷的十六进制是:00 28 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74 00。

求 SUBSCRIBE 报文。

有了组装 CONNECT 报文的经验,XX,剩余长度是多少大家应该都知道了吧,后面有多少字节就是多少,45 个,转成十六进制就是 2D。

于是乎,完整 SUBSCRIBE 报文:82 2D 00 0A 00 28 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74 00。

7.5.5 SUBSCRIBE报文发送

发送成功就会有 SUBACK 报文返回过来,如下图。

 

 

如果我们发送的 SUBSCRIBE 报文有错误,就会断开连接。

 

 

7.6 SUBACK报文

SUBACK,订阅确认,报文是用于服务端回复客户端表示自己收到了 SUBSCRIBE 报文。和 CONNACK 报文一样,类似于外卖送到了确认收货的动作。

 

 

一条 SUBACK 报文由固定报头、可变报头、有效载荷三部分组成。前面以及介绍了三条报文了,大家应该对固定报头、可变报头、有效载荷都很熟悉了,接下来我就不啰嗦了,我们直接上干货!

7.6.1 固定报头

结构如下图,固定报头中消息类型是 9,标志位是 0000,所以第一字节是 10010000,转成十六进制是 90,剩余长度为3(后面固定有三字节),所以 CONNACK 报文固定报头的十六进制是:90 03。

 

 

7.6.2 可变报头

还记得 SUBSCRIBE 报文的可变报头吗,只有两个字节,定义了报文标识符,就是给报文取了个名字。到 SUBACK 报文服务端就需要返回一模一样的报文标识符,告诉客户端,是这个名字的报文被我确认了。我们刚刚的例子报文标识符是 00 0A,所以这里也是 00 0A。

 

 

7.6.3 有效载荷

有效载荷只有一字节的返回码,我们发送的服务质量等级是多少,返回码就是多少。

 

 

允许的返回码值:

  • 0x00 - 最大 QoS 0

  • 0x01 - 成功 – 最大 QoS 1

  • 0x02 - 成功 – 最大 QoS 2

  • 0x80 - Failure 失败

上一次发送我们设置的服务质量等级是 00,返回了 00。如果我们服务质量等级改成 01,那么 SUBACK 最后一位将返回了 01。

 

 

7.6.4 组装SUBACK报文

so easy,上图就是答案了:90 03 00 0A XX。

7.7 UNSUBSCRIBE报文

UNSUBSCRIBE 报文,客户端向服务端发送,用于取消订阅主题,取消订阅某主题后,就不会收到该主题的新消息了。一条 UNSUBSCRIBE 报文是以固定报头、可变报头、有效载荷三部分组成。

7.7.1 固定报头

结构如下图,固定报头中消息类型是 10,标志位是 0010,所以第一字节是 10100010,转成十六进制是 A2,剩余长度未知,所以 UNSUBSCRIBE报文固定报头的十六进制是:A2 XX。

 

 

7.7.2 可变报头

老朋友了,报文标识符,大家可以随意取名字,这里我们就使用:00 0B。

 

 

7.7.3 有效载荷

有效载荷只有一个部分,那就是你想要取消订阅的主题,以长度为前缀。这里和 SUBSCRIBE 报文不一样的是没有了服务质量要求

 

 

我们取消订阅刚刚订阅的 $sys/L14FCC38pq/test1/thing/property/set 。

转成十六进制,加上长度前缀是:00 28 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74。

7.7.4 组装UNSUBSCRIBE报文

不多逼逼。

固定报头:A2 XX

可变报头:00 0B

有效载荷:00 28 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74

剩余长度 XX:2C

完整 UNSUBSCRIBE 报文:A2 2C 00 0B 00 28 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 73 65 74。

7.7.5 UNSUBSCRIBE报文发送

发送效果如下:

 

 

7.8 UNSUBACK报文

UNSUBACK报文,取消订阅确认,是用于服务端回复客户端表示自己收到了 UNSUBSCRIBE 报文。

 

 

一条 UNSUBACK 报文由固定报头、可变报头两部分组成。

7.8.1 固定报头

结构如下图,固定报头中消息类型是 11,标志位是 0000,所以第一字节是 10110000,转成十六进制是 B0,剩余长度为 2(后面固定有两字节),所以 UNSUBACK 报文固定报头的十六进制是:B0 02。

 

 

7.8.2 可变报头

还记得 UNSUBSCRIBE 报文的可变报头吗,只有两个字节,定义了报文标识符,就是给报文取了个名字。到 UNSUBACK 报文服务端就需要返回一模一样的报文标识符,告诉客户端,是这个名字的报文被我确认了。我们刚刚的例子是 00 0B,所以这里也是 00 0B。

 

 

7.8.4 组装UNSUBACK报文

B0 02 00 0B,看看是不是和我们刚刚效果图上的一样。

 

 

7.9 PUBLISH报文

PUBLISH 报文,发布消息,是双向的,既可以客户端到服务端,也可以服务端到客户端。但是需要注意的是发布消息只能在同一产品 ID下进行,不能进行跨产品消息推送。

7.9.1 固定报头

结构如下图,固定报头中消息类型是 3 ,标志位是未知,剩余长度未知。

标志位填写规则如下:

  • DUP:消息第一次发送为 0 ,如果重发为 1 。若 QoS = 0 则 DUP 必须为 0,毕竟 QoS = 0 没有请求重发。
  • QoS 等级:QoS = 0 则 为 00;如果是 01,则 QoS = 1;如果是 10,则 QoS = 2。
  • RETAIN:服务端发送 PUBLISH 报文给客户端时,如果消息是作为客户端一个新订阅的结果发送,报文的保留标志设为 1。当一个 PUBLISH 报文发送给客户端是因为匹配一个已建立的订阅时,服务端必须将保留标志设为 0,不管它收到的这个消息中保留标志的值是多少。如果客户端发给服务端的 PUBLISH 报文的保留标志位 0,服务端不能存储这个消息也不能移除或替换任何现存的保留消息。是不是听不懂,没关系,我直接告诉大家,这里设成 0 就行了。

 

 

作为例子,我让 QoS = 0,于是 PUBLISH 报文固定报头的十六进制是:30 XX。

7.9.2 可变报头

可变报头由主题名报文标识符两部分组成。

主题名(即主题)用于识别有效载荷应该被发布到哪一个信息通道,必须是 UTF-8 编码的格式,也是需要以长度为前缀。只有当 QoS = 1 或 QoS = 2 时,报文标识符才能出现在 PUBLISH 报文中。因为 QoS0 不需要接收端返回报文,所以也就不用指明是哪条报文,也就不用给报文取名字了。

下图是官方文档的示例,可以参考一下,我们的例子不是这样的。

 

 

这里我们选择下面红框中的主题,直连设备上报属性。

 

 

$sys/L14FCC38pq/test1/thing/property/post 转成十六进制是:24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 70 6F 73 74。共 41 个字符,加上长度前缀 00 29,可变报头为:00 29 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 70 6F 73 74。

7.9.3 有效载荷

我们要发布的内容就是有效载荷,其格式必须是 JSON 格式,也要转为十六进制。

注意:这里不需要以长度为前缀了,因为是 JSON 格式,不是 UTF-8。

JSON格式规则如下:

  • 数组用方括号 [] 表示。
  • 对象用大括号 {} 表示。
  • 名称/值对组合成数组和对象。
  • 名称置于双引号中,值有字符串、数值、布尔值、null、对象和数组。
  • 并列的数据之间用逗号 , 分隔

示例如下:

{
    "id": "123",
    "version": "1.0",
    "params": {
        "Power": {
            "value": "12345",
            "time": 1599534283111
        },
        "temp": {
            "value": 23.6,
            "time": 1599534283111
        }   
    }
}

我们就简单点,给 test1 的物模型“收到数据”发布 6 个 6 ,要使用标识符 receive 。这个 receive 标识符就是我们在创建产品的时候所设置的属性,还记得吗?

 

 

对应的 JSON 格式包如下:

{
    "id":"1386772172",
    "version":"1.0",
    "params":{
        "receive":{
            "value":"666666"
        }
    }
}

转换十六进制时要把换行符号和 tab 去掉,于是,{"id":"1386772172","version":"1.0","params":{"receive":{"value":"666666"}}} 转成十六进制是:7B 22 69 64 22 3A 22 31 33 38 36 37 37 32 31 37 32 22 2C 22 76 65 72 73 69 6F 6E 22 3A 22 31 2E 30 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 72 65 63 65 69 76 65 22 3A 7B 22 76 61 6C 75 65 22 3A 22 36 36 36 36 36 36 22 7D 7D 7D。

7.9.4 组装PUBLISH报文

不多逼逼。

固定报头:30 XX

可变报头:00 29 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 70 6F 73 74

有效载荷:7B 22 69 64 22 3A 22 31 33 38 36 37 37 32 31 37 32 22 2C 22 76 65 72 73 69 6F 6E 22 3A 22 31 2E 30 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 72 65 63 65 69 76 65 22 3A 7B 22 76 61 6C 75 65 22 3A 22 36 36 36 36 36 36 22 7D 7D 7D

剩余长度 XX:76

完整PUBLISH报文:30 76 00 29 24 73 79 73 2F 4C 31 34 46 43 43 33 38 70 71 2F 74 65 73 74 31 2F 74 68 69 6E 67 2F 70 72 6F 70 65 72 74 79 2F 70 6F 73 74 7B 22 69 64 22 3A 22 31 33 38 36 37 37 32 31 37 32 22 2C 22 76 65 72 73 69 6F 6E 22 3A 22 31 2E 30 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 72 65 63 65 69 76 65 22 3A 7B 22 76 61 6C 75 65 22 3A 22 36 36 36 36 36 36 22 7D 7D 7D

7.9.5 PUBLISH报文发送

效果展示,当当当:

 

 

7.10 PINGREQ报文和PINGRESP报文

PINGREQ 报文和 PINGRESP 报文是心跳请求和心跳响应,是 MQTT 心跳机制的两个报文。客户端定时向服务端发送心跳请求(PINGREQ),告诉服务端,我还和你连接着哦。服务端收到心跳请求后,会回复一条心跳响应(PINGRESP),告诉客户端,我知道你还连着我啦。

 

 

通过心跳机制,MQTT 可以实时监测客户端的连接状态,及时发现和处理异常情况,确保通信的可靠性和稳定性。就像我们依赖心脏维持身体的正常运转一样,MQTT的心跳机制也是保障通信链路顺畅运行的重要机制之一。

7.10.1 固定报头

PINGREQ 报文和 PINGRESP 报文都很简单,只有固定报头。

PINGREQ 报文,二进制:1100 0000 0000 0000,十六进制:C0 00。

 

 

PINGRESP 报文,二进制:1100 0000 0000 0000,十六进制:D0 00。

 

 

7.10.2 PINGREQ报文和PINGRESP报文发送

 

 

报文小结

我们用到的 MQTT 报文都总结在下面啦,不想做笔记的同学可以直接copy。表中的 L 是长度前缀,是两位字符噢,剩余长度是一位字符。

总结

通过本文的学习,相信你已经对 MQTT 有了一定深度的了解和认识。希望这些知识对你在物联网领域的学习和实践有所帮助。感谢各位看官,love and peace!

更多关于 MQTT 的细节可以看官方文档。

  • 官方文档 3.1.1 中文翻译下载

链接: 提取码:s3aw

  • 官方文档 5.0 中文翻译下载

链接: 提取码:r367

 

 

出处:源码下载及前置阅读

本文首发 良许嵌入式网 : ,欢迎关注!

本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):

如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。

posted @ 2024-07-31 09:26  沦陷城池  阅读(4)  评论(0编辑  收藏  举报