第三十三章 实战小项目之 MQTT 物联网 (学习笔记)
实战小项目之 MQTT 物联网
物联网曾被认为是继计算机、互联网之后,信息技术行业的第三次浪潮。随着基础通讯设施的不断完善,尤其是 5G 的出现,进一步降低了万物互联的门槛和成本。物联网本身也是 AI 和区块链应用很好的落地场景之一,各大云服务商也在纷纷上架物联网平台和服务。
物联网通讯是物联网的一个核心内容,目前物联网的通讯协议并没有一个统一的标准,比较常见的有 MQTT、CoAP、DDS、XMPP 等,在这其中,MQTT(消息队列遥测传输协议)应该是应用最广泛的标准之一。目前,MQTT 已逐渐成为 IoT 领域最热门的协议,也是国内外各大物联网平台最主流的传输协议,阿里云 IoT 物联网平台很多设备都是通过 MQTT 接入。
本章我们将从最基础的知识开始,向您讲解 MQTT 协议的应用,通过本章的学习,我们将一起在开发板上实现一个简单地物联网小项目,实现远程控制开发板上的外设,譬如 LED、蜂鸣器;亦或者远程获取开发板运行的状态信息。
本章将会讨论如下主题内容。
⚫ MQTT 是什么?
⚫ MQTT 协议介绍
⚫ MQTT 客户端库移植到开发板
⚫ 如何开发 MQTT 客户端应用程序
33.1MQTT 简介
《MQTT 协议规范中文版》一书中对 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)进行了描述:
MQTT 是一种基于客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT)。----MQTT 协议中文版
以上这段话很好的描述了 MQTT 的全部含义,它是一种轻巧、开放、简单、规范的网络通信协议。与 HTTP 协议一样,MQTT 协议也是应用层协议,工作在 TCP/IP 四层模型中的最上层(应用层),构建于 TCP/IP 协议上。MQTT 最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
如今,MQTT 成为了最受欢迎的物联网协议,已广泛应用于车联网、智能家居、即时聊天应用和工业互联网等领域。目前通过 MQTT 协议连接的设备已经过亿,这些都得益于 MQTT 协议为设备提供了稳定、可靠、易用的通信基础。
MQTT 的主要特性
MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,它具有以下主要的几项特性:
①、 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
②、 基于 TCP/IP 提供网络连接。
主流的 MQTT 是基于 TCP 连接进行数据推送的,但是同样也有基于 UDP 的版本,叫做 MQTT-SN。这两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。
③、 支持 QoS 服务质量等级。
根据消息的重要性不同设置不同的服务质量等级。
④、 小型传输,开销很小,协议交换最小化,以降低网络流量。
这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集",要知道嵌入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了,在手机移动应用方面,
MQTT 是一种不错的 Android 消息推送方案。
⑤、 使用 will 遗嘱机制来通知客户端异常断线。
⑥、 基于主题发布/订阅消息,对负载内容屏蔽的消息传输。
⑦、 支持心跳机制。
以上便是 MQTT 的主要特性了。
MQTT 历史
MQTT 协议最初版本是在 1999 年建立的,该协议的发明人是的 Andy Stanford-Clark 和 Arlen Nipper。
图 33.1.1 MQTT 协议发明人之一 Andy-Stanford-Clark
图 33.1.2 MQTT 协议发明人之一 Arlen Nipper
MQTT 最初是用于石油管道的传感器与卫星之间数据传输。他们当时正在开发一个利用卫星通讯监控输油管道的项目,为了实现这个项目要求,他们需要开发一种用于嵌入式设备的通讯协议,这种通讯协议必须满足以下条件:
⚫ 易于实现,服务器必须要实现成千上万个客户端的接入
⚫ 数据传输的服务质量可控,根据数据的重要性和特性,设置不同等级的服务质量
⚫ 占用带宽小,单次数据量小,但不能出错
⚫ 必须能够适应高延迟、掉线、断网等网络通信不可靠的风险
⚫ 设备连接状态可知,云端与设备端保持长连接通过以上几个条件可知:
⚫ MQTT 服务器可以连接大量的远程传感器和控制设备,与远程客户端保持长连接,具有一定的实时性。
⚫ 云端向设备端发送消息,设备端可以在最短的时间内接收到并作出回应。
⚫ MQTT 更适合需要实时控制的场合,尤其适合执行器。
⚫ 云端与客户端需要保持长连接,要能够获取到设备的连接状态,就需要时不时地发送心跳包,这就不会省电,所以,MQTT 并不适合低功耗场合。
从以上几点不难看出,MQTT 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的。虽然历经几十年的更新和变化,以上这些特点仍然是 MQTT 协议的核心特点。但是与最初不同的是,MQTT 协议已经从嵌入式系统应用拓展到开放的物联网(IoT)领域。
MQTT 版本
目前 MQTT 主流版本有两个,分别是 MQTT3.1.1 和 MQTT5。MQTT3.1.1 是在 2014 年 10 月发布的,而 MQTT5 是在 2019 年 3 月发布的。虽然 MQTT3.1.1 与 MQTT5 在时间相差了将近五年,但是 MQTT3.1.1 作为一个经典的版本,目前仍然是主流版本,能够满足大部分实际需求。
MQTT5 是在 MQTT3.1.1 的基础上进行了升级,因此 MQTT5 是完全兼容 MQTT3.1.1 的。而 MQTT5 是在 MQTT3.1.1 的基础上添加了更多的功能、补充完善 MQTT 协议。具体增加了哪些功能,这里不作说明,如果大家有兴趣可以自行百度了解。
图 33.1.3 MQTT 3.1.1 与 MQTT 5
33.2MQTT 协议(上)
在上小节中,我们了解了 MQTT 的背景知识和基本特点,本小节开始我们将一起了解 MQTT 通信基本原理。
MQTT 通信基本原理
MQTT 是一种基于客户端-服务端架构的消息传输协议,所以在 MQTT 协议通信中,有两个最为重要的角色,它们便是服务端和客户端。
服务端
MQTT 服务端通常是一台服务器(broker),它是 MQTT 信息传输的枢纽,负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端;MQTT 服务端还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。
客户端
MQTT 客户端可以向服务端发布信息,也可以从服务端收取信息;我们把客户端发送信息的行为称为 “发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作很像我们在使用微信时“关注”了某个公众号,当公众号的作者发布新的文章时,微信官方会向关注了该公众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看。
MQTT 主题
上面我们讲到了,客户端想要从服务器获取信息,首先需要订阅信息,那客户端如何订阅信息呢?这里我们要引入“主题(Topic)”的概念,“主题”在 MQTT 通信中是一个非常重要的概念,客户端发布信息以及订阅信息都是围绕“主题”来进行的,并且 MQTT 服务端在管理 MQTT 信息时,也是使用“主题”来控制的。
客户端发布消息时需要为消息指定一个“主题”,表示将消息发布到该主题;而对于订阅消息的客户端来说,可通过订阅“主题”来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,
MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)。
为了便于您更好理解服务端是如何通过“主题”来控制客户端之间的信息通讯,我们来看看下图实例:
图 33.2.1 MQTT 通信示例 1
在以上图示中一共有三个 MQTT 客户端,它们分别是开发板、手机和电脑。MQTT 服务端在管理 MQTT 通信时使用了“主题”来对信息进行管理。比如上图所示,假设我们需要利用手机和电脑获取开发板在运行过程中 SoC 芯片的温度,那么首先电脑和手机这两个客户端需要向 MQTT 服务器订阅主题“芯片温度”;接下来,当开发板客户端向服务端的“芯片温度”主题发布信息(假设信息的内容就是当前的温度值)后,服务端就会首先检查都有哪些客户端订阅了“芯片温度”这一主题的信息,而当它发现订阅了该主题的客户端有一个手机和一个电脑,于是服务端就会将刚刚收到的“芯片温度”信息转发给订阅了该主题的手机和电脑客户端。
通过以上的这种实例,手机和电脑便可以获取到开发板运行时 SoC 芯片的温度值。
以上实例中,开发板是“芯片温度”主题的发布者,而手机和电脑则是该主题的订阅者。
值得注意的是,MQTT 客户端在通信时,角色往往不是单一的,一个客户端既可以作为信息发布者也可以同时作为信息订阅者。如下图所示:
图 33.2.2 MQTT 通信示例 2
上图中的所有客户端都是围绕“LED 控制”这一主题进行通信。此时,对于“LED 控制”这一主题来说,手机和电脑客户端成为了 MQTT 信息的发布者而开发板则成为了 MQTT 信息的订阅者(接收者)。
所以由此可知,针对不同的主题,MQTT 客户端可以切换自己的角色,它们可能对主题 A 来说是信息发布者,但是对于主题 B 就成了信息订阅者,所以一个 MQTT 客户端它的角色并不是固定的,所以大家一定要理解“主题”这个概念。
MQTT 发布/订阅特性
从以上实例我们可以看到,MQTT 通信的核心枢纽是 MQTT 服务端,它负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端,还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。
正是因为有了服务端对 MQTT 信息的接收、储存、处理和发送,客户端在发布和订阅信息时,可以相互独立、且在空间上可以分离、时间上可以异步,这就是 MQTT 发布/订阅的特性:客户端相互独立、空间上可分离、时间上可异步,具体介绍如下:
⚫ 客户端相互独立:MQTT 客户端是一个个独立的个体,它们无需了解彼此的存在,依然可以实现信息交流。譬如在上面的实例中,开发板客户端在发布“芯片温度”信息时,开发板客户端本身完全不知道有多少个 MQTT 客户端订阅了“芯片温度”这一主题;而订阅了“芯片温度”主题的手机和电脑客户端也完全不知道彼此的存在,大家只要订阅了“芯片温度”这一主题,MQTT 服务端就会在每次收到新信息时,将信息发送给订阅了“芯片温度”主题的客户端。
⚫ 空间上分离:空间上分离相对容易理解,MQTT 客户端以及 MQTT 服务端它们在通信时是处于同一个通信网络中的,这个网络可以是互联网或者局域网;只要客户端联网,无论他们远在天边还是近在眼前,都可以实现彼此间的通讯交流;其实网络通信本就是如此,所以并不是 MQTT 通信所特有的。
⚫ 时间上可异步:MQTT 客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要,前面我们也介绍了,MQTT 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的,高延迟和不可靠网络必然就会导致时间上的异步;物联网设备在运行过程中发生意外掉线是非常正常的情况,我们使用上面的实例二的场景来作说明,当开发板在运行过程中,可能会由于突然断电(假设开发板是通过电源适配器供电的)导致掉线,这时开发板会断开与 MQTT 服务端的连接。假设此时我们的手机客户端向开发板客户端所订阅的“LED 控制”主题发布了信息,而开发板恰恰不在线,这时,MQTT 服务端可以将“LED 控制”主题的新信息保存,待开发板客户端再次上线后,服务端再将“LED 控制”信息推送给开发板。所以这就必然导致了,手机发送信息与开发板接收信息在时间上是异步的。
总结
本小节向大家介绍了 MQTT 通信的基本原理,在 MQTT 通信中,1 个服务端、多个客户端之间围绕“主题”进行了通信,所以本节重要在于大家需要理解各个客户端的相互关系以及服务端在其中所起的作用,并且理解“主题”这个概念以及 MQTT 发布/订阅模式的特性,后面向大家介绍具体的通信过程时,要迅速的反应过来。
讲到这里请您注意:对于 MQTT 发布/订阅模式的特性,我们总结的几个特点中都有一个“可”字。这个“可”字意味着客户端彼此之间可以独立,空间可以分离,时间可以异步。在我们实际应用中,客户端之间的关系既可以独立也可以相互依存。在空间上,既可以相距甚远,也可以彼此相邻。在时间上,既可以异步也可以同步。这个“可”字所体现的是 MQTT 通讯的灵活性。
连接 MQTT 服务端
MQTT 客户端之间想要实现通信,必须要通过 MQTT 服务端。所以,客户端无论是发布信息还是订阅信息都必须先连接到服务端。下面我们来看一下,客户端连接服务端的详细过程。
MQTT 客户端连接服务端总共包含了两个步骤:
①、 首先客户端需要向服务端发送连接请求,这个连接请求实际上就是向服务端发送一个 CONNECT 报文,也就是发送了一个 CONNECT 数据包。
图 33.2.3 客户端向服务端发送连接请求---CONNECT 报文
②、 MQTT 服务端收到连接请求后,会向客户端发送连接确认。连接确认实际上是向客户端发送一个
CONNACK 报文,也就是 CONNACK 数据包。
图 33.2.4 服务端向客户端发送连接确认---CONNACK 报文
以上就是 MQTT 客户端连接服务端的详细步骤,总结一句话就是:客户端先向服务端发送 CONNECT 报文,服务端收到连接请求后,再向待连接的客户端发送 CONNACK 报文。接下来,我们来看看 CONNECT 报文和 CONNACK 报文数据包中包含了哪些信息。
CONNECT 报文
在上面的描述中我们看到,MQTT 客户端要想连接服务端,首先要向服务端发送 CONNECT 报文。如果此 CONNECT 报文的格式或内容不符合 MQTT 规范,则服务器会拒绝客户端的连接请求。
CONNECT 报文包含的信息如下图所示:
图 33.2.5 CONNECT 报文的内容
所谓报文就是一个数据包,MQTT 报文组成分为三个部分:固定头(Fixed header)、可变头(Variable header)以及有效载荷(Payload,消息体)。这里我们简单地介绍一下:
⚫ 固定头(Fixed header):存在于所有 MQTT 报文中,固定头中有报文类型标识,可用于识别是哪种 MQTT 报文,譬如该报文是 CONNECT 报文还是 CONNACK 报文,亦或是其它类型报文。
⚫ 可变头(Variable header):存在于部分类型的 MQTT 报文中,报文的类型决定了可变头是否存在及其具体的内容。
⚫ 消息体(Payload):存在于部分类型的 MQTT 报文中,payload 就是消息载体的意思。
关于 MQTT 报文格式更加详细的内容,如果大家有兴趣可以自己去了解下,笔者在这里就不再啰嗦了!回到的 CONNECT 报文中,从图 36.2.5 中可知,CONNECT 报文包含了很多的信息,左边的是信息的名称(变量名),右边则是信息的具体内容(变量的值),右边这些具体内容只是笔者给出的一个示例,不是所有的 CONNECT 报文中的 clientId 信息内容都是“client-id”,这里只是举个例子而已!
另外也请注意,上图中有些信息名称旁边标注了“可选”字样,而有些则没有。那些没有标注“可选” 字样的信息是必须包含在 CONNECT 报文中的。而对于标注了“可选”字样的信息,CONNECT 报文既可以包含它们也可以没有它们,具体具体情况而定!
接下来笔者将向大家介绍 CONNECT 报文中这些信息表示什么意思,有什么作用?不过,考虑到我们刚刚接触 MQTT 协议,目前应先从最基础的内容开始学起,所以本小节我们只介绍那些未标注“可选”字样的信息。
clientId--客户端 id
clientId 是 MQTT 客户端的标识,也就是 MQTT 客户端的名字,MQTT 服务端可通过 clientId 来区分不同的客户端,MQTT 服务端用该标识来识别客户端。因此 clientId 必须是独立的,如果两个 MQTT 客户端使用相同 clientId 标识,服务端会把它们当成同一个客户端来处理。通常 clientId 是由一串字符所构成的,譬如,在上面的示例中,clientId 是“client-id”。
keepAlive--心跳时间间隔
对于 MQTT 服务器来说,它要判断一台 MQTT 客户端是否依然与它保持着连接状态,可以检查这台客户端是不是经常发送消息给服务端,如果服务端经常收到客户端的消息,那么没问题,这个客户端肯定在线。
但是有些客户端并不经常发送消息给服务端,对于这种客户端,MQTT 协议使用了类似心跳检测的方法来判断客户端是否在线。前面在介绍 MQTT 时,曾提到过 MQTT 支持心跳机制,心跳机制其实就是用来判断客户端是否与服务端保持着连接的一种方法,也就是说通过心跳机制来检测客户端是否在线。客户端在没有向服务端发送信息时(空闲时),可以定时向服务端发送一个心跳数据包,这个心跳包也被称作心跳请求,心跳请求的作用正是用于告知服务端,当前客户端依然在线,服务端在收到客户端的心跳请求后,会回复一条消息,这条回复消息被称作心跳响应。
关于 MQTT 的心跳机制后面还会向大家进行讲解,这里便不再多说了!CONNECT 报文中的 keepAlive 其实是指定了心跳时间间隔,也就是客户端向服务端发送心跳包的时间间隔。譬如 keepAlive=60,表示告诉服务端,客户端将会每隔 60 秒左右向服务端发送心跳包。
cleanSession--清除会话
所谓“清除会话”这一翻译源自 MQTT 官方文档中文版。这是一个布尔值,cleanSession 标志可用于控制客户端与服务端在连接和断开连接时的行为,我们举个例子来进行说明,QQ、微信这些聊天软件大家都用过,假设当前你的 QQ 账号没有登录或者说当前处于离线状态,与服务器断开了连接;而在离线期间,你的 QQ 好友给你发了几条信息;由于当前你的 QQ 处于离线状态,自然是接收不到好友发送过来的信息,但是,当你的 QQ 恢复连接状态时,立马会接收到好友在离线期间所发给你的信息。
而 cleanSession 就与这个有关系,它是一个布尔值,如果连接服务端时 cleanSession=0,当 MQTT 客户端由离线(与服务端断开连接)再次上线时,离线期间发给客户端的所有 QoS>0 的消息仍然可以接收到;如果连接服务端时 cleanSession=1,当 MQTT 客户端由离线(与服务端断开连接)再次上线时,离线期间发给客户端的所有消息一律接收不到。注意,这里我们提到了 QoS,关于 QoS 的概念后续再向大家介绍,这里暂时先不去理会,先记住有这么个东西。
说白了,想接收离线消息,客户端连接服务端时就必须使用 cleanSession=0;除了这个作用之外,如果 cleanSession=0,则 MQTT 服务端会在客户端断开连接之后“记住”MQTT 客户端在线期间所订阅的所有“主题”;也就是说,服务端会保存、存储客户端所订阅的主题。
如果 cleanSession=1,客户端既无法接收到离线消息、服务端也不会记住该客户端所订阅的主题,服务端不会保存客户端的会话状态,每次连接都是一次新的会话;既然是新的会话,那就不会保存以前的会话状态信息,一切从“新”开始、从而丢弃以前的会话状态信息(包括:离线期间发送给客户端的消息以及客户端订阅的主题等),这就是“清除会话”的含义。
所以这就是 cleanSession 的作用,总的来说:cleanSession 设置为 1,表示此次连接将创建一个新的临时会话,在客户端断开后,这个会话会自动销毁。而 cleanSession 设置为 0,表示创建一个持久性会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。
关于 cleanSession 就给大家介绍这么多,已经解释得很清楚了,相信大家已经明白了。
以上就是 CONNECT 报文的主要内容了,关于 CONNECT 报文中的其它内容,我们会在后续内容中给
大家讲解。下面再看看 MQTT 服务端接收到客户端发来的连接请求后所回复的 CONNACK 报文详细内容。
CONNACK 报文
CONNACK 报文包含的信息如下图所示:
图 33.2.6 CONNACK 报文的内容
CONNACK 报文包括两个信息,一个是 returnCode(连接返回码),另一个是 sessionPresent。以下是这两个信息的说明:
returnCode--连接返回码
当服务端收到了客户端的连接请求后,会向客户端发送 returnCode(连接返回码),用来说明连接情况。如果客户端与服务端成功连接,则返回数字“0”。如果未能成功连接,返回码将会是一个非零的数字,具体这个数字的含义,请见下表:
|
返回码 |
说明 |
|
0 |
连接成功 |
|
1 |
连接 被服务端拒绝,原因是不支持客户端的 MQTT 协议版本 |
|
2 |
连接被服务端拒绝,原因是不支持客户端标识符的编码。可能造成此原因的是客户端标识符编码是 UTF-8,但是服务端不允许使用此编码。 |
|
3 |
连接被服务端拒绝,原因是服务端不可用。即,网络连接已经建立,但 MQTT 服务不可用。 |
|
4 |
连接被服务端拒绝,原因是用户名或密码无效。 |
|
5 |
连接被服务端拒绝,原因是客户端未被授权连接到此服务端。 |
|
6-255 |
保留备用 |
表 33.2.1 返回码说明 sessionPresent
sessionPresent 与 CONNECT 报文中的 cleanSession 有关系,前面说了,客户端连接服务端时,如果 cleanSession=0,服务端会保存与客户端的会话状态,记录客户端订阅的主题,即使客户端断开了与服务端的连接,会话任然保持并保存离线消息,直到客户端重新上线,这些离线消息就会发给客户端。
在 cleanSession=0 的情况下,当客户端连接到服务器之后,可通过 CONNACK 报文中返回的 sessionPresent 来查询服务端是否为客户端保存了会话状态(客户端上一次连接时的会话状态信息),如果服务端已为客户端保存了上一次连接时的会话状态,则 sessionPresent=1,如果没有保存会话状态,则 sessionPresent=0。
如果 cleanSession=1,在这种情况下,客户端是不需要服务端保存会话状态的,那么服务端发送的确认连接 CONNACK 报文中,sessionPresent 肯定是 false(sessionPresent=0),也就是说,服务端没有保存客户端的会话状态信息。
简言之,CONNACK 报文的 sessionPresent 与 CONNECT 报文的 cleanSession 相互配合。其作用是客户端发送连接请求时,服务端告知客户端有没有保存会话状态。这个被服务端保存的会话状态是来自于上一次客户端连接时,譬如离线消息以及上一次连接时客户端所订阅的主题。
Tips:关于 MQTT 协议的参考资料,链接地址如下:
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718027
断开连接
当 MQTT 客户端连接到服务端之后,在后续的通信过程中,如果客户端想要断开与服务端的连接,此时客户端可以主动向服务端发送一个 DISCONNECT 报文来断开与服务端的连接,如下图所示:
图 33.2.7 客户端断开与服务端的连接
发布消息、订阅主题与取消订阅主题
上小节我们学习了 MQTT 客户端连接 MQTT 服务端的详细过程,当客户端连接到服务端之后,便可以发布消息或订阅主题了,本小节我们便来学习客户端如何实现发布消息、订阅主题以及取消订阅主题。
PUBLISH–发布消息
当客户端连接到服务端之后,就可以向服务端发布消息了,每条发布的消息必须指定一个“主题”,表示向某主题发布消息;MQTT 服务端可以通过主题来确定将消息转发给哪些客户端(订阅了该主题的客户端)。例如图 36.2.1 所示,开发板客户端向主题“芯片温度”发布了消息,这个消息就是当前芯片的温度值,而手机和电脑这两个客户端订阅了主题“芯片温度”;所以,当服务端接收到开发板发布的消息之后,就会将这个消息转发给订阅了主题“芯片温度”的手机和电脑这两个客户端,这样,手机和电脑就会收到开发板发布的消息。
MQTT 客户端向服务端发布消息其实就是向服务端发送一个 PUBLISH 报文,服务端收到客户端发送过来的 PUBLISH 报文之后,会向发送发回复一个报文。根据 QoS 的不同,回复的报文类型也是不同的,并且整个发布消息的过程也将会有所区别;譬如对于 QoS=1 时,客户端向服务端发送 PUBLISH 报文,服务端收到 PUBLISH 报文之后会向发送方回复 PUBACK 报文;而对于 QoS=2 的情况,将会更加复杂,这个后续再给大家介绍。
下图是 PUBLISH 报文包含的信息:
图 33.2.8 PUBLISH 报文的内容
同样,左侧是信息的名称,右侧是信息的具体内容。下面我们将一一进行介绍: packetId--报文标识符
报文标识符可用于对 MQTT 报文进行标识(识别不同的报文)。不同的 MQTT 报文所拥有的标识符不同。MQTT 设备可以通过该标识符对 MQTT 报文进行甄别和管理,MQTT 协议内部使用的标识符。请注意:报文标识符的内容与 QoS 级别有密不可分的关系。只有 QoS 级别大于 0 时,报文标识符才是非零数值。如果 QoS 等于 0,报文标识符为 0,为什么是这样的呢?后面向大家介绍 QoS 概念时会做一个简单地说明! topicName--主题名字
这个就是发布消息时对应的主题的名字,这是一个字符串,譬如上图中 topicName=“myTopic”,表示会将消息发布到“myTopic”这个主题。
payload--有效载荷
有效载荷是我们希望通过 MQTT 所发送的实际内容。我们可以使用 MQTT 协议发送字符串文本,图像等格式的内容。这些内容都是通过有效载荷所发送的。
qos--服务质量等级
QoS(Quality of Service)表示 MQTT 消息的服务质量等级。QoS 有三个级别:0、1 和 2,QoS 决定
MQTT 通信有什么样的服务保证。有关 QoS 的详细信息我们会在后续内容中详细讲解。
retain--保留标志在默认情况下,当客户端订阅了某一主题后,并不会马上接收到该主题的信息。因为客户端订阅该主题之后,并没有其它客户端向该主题发布消息;只有在客户端订阅该主题后,服务端接收到该主题的新消息时,服务端才会将最新接收到的该主题消息推送给客户端。
但是在有些情况下,我们需要客户端在订阅了某一主题后马上接收到一条该主题的信息。这时候就需要用到保留标志这一信息。关于保留标志的具体使用方法,我们将在本教程的后续部分进行详细讲解。
dup--重发标志
dup 标志指示此消息是否重复。
当 MQTT 报文的接收方没有及时向报文发送发回复确认收到报文时,发送方会以为对方没有收到信息,会再次重复发送 MQTT 报文(譬如客户端向服务端发送 PUBLISH 报文,服务端收到 PUBLISH 报文之后需要向客户端回复一个 PUBACK 报文,如果客户端没收到 PUBACK 报文,则会认为服务端可能没接收到自己发送的报文,将会再次发送 PUBLISH 报文)。在重复发送 MQTT 报文时,发送方会将此“dup--重发标志”设置为 true。请注意,重发标志只在 QoS 级别大于 0 时使用。有关 QoS 的详细信息,我们将会在后续内容中为您做详细介绍。
SUBSCRIBE--订阅主题
当客户端连接到服务端后,除了可以发布消息,也可以接收消息。我们在之前的课程讲过,所有 MQTT 消息都有主题。客户端要想接收消息,首先要订阅该消息的主题。这样,当有客户端向该主题发布消息后,订阅了该主题的客户端就能接收到消息了。
客户端要想订阅主题,首先要向服务端发送主题订阅请求。客户端是通过向服务端发送 SUBSCRIBE 报文来实现这一请求的。该报文包含有一系列“订阅主题名”。请留意,一个 SUBSCRIBE 报文可以包含有单个或者多个订阅主题名。也就是说,一个 SUBSCRIBE 报文可以用于订阅一个或者多个主题。
在以上 PUBLISH 报文讲解中,我们曾经提到过 QoS(服务质量等级)这一概念。同样的,客户端在订阅主题时也可以明确 QoS。服务端会根据 SUBSCRIBE 中的 QoS 来提供相应的服务保证。
另外每一个 SUBSCRIBE 报文还包含有“报文标识符”。报文标识符可用于对 MQTT 报文进行标识。
不同的 MQTT 报文所拥有的标识符不同。MQTT 设备可以通过该标识符对 MQTT 报文进行甄别和管理。当客户端向服务端发送 SUBSCRIBE 报文,服务端接收到 SUBSCRIBE 报文之后会向客户端回复一个
SUBACK 报文(订阅确认报文),如下图所示:
图 33.2.9 客户端订阅主题
服务端接收到客户端的订阅报文后,会向客户端发送 SUBACK 报文确认订阅。
SUBACK 报文包含有“订阅返回码”和“报文标识符”这两个信息。
returnCode--订阅返回码
客户端向服务端发送订阅请求后,服务端会给客户端返回一个订阅返回码。在之前的讲解中我们说过,客户端可通过一个 SUBSCRIBE 报文发送多个主题的订阅请求。服务端会针对 SUBSCRIBE 报文中的所有订阅主题来逐一回复给客户端一个返回码。
这个返回码的作用是告知客户端是否成功订阅了主题。以下是返回码的详细说明。
|
返回码 |
|
说明 |
|
0x00 |
订阅成功--QoS0 |
|
|
0x01 |
订阅成功--QoS1 |
|
|
0x02 |
订阅成功--QoS2 |
|
|
0x80 |
订阅失败 |
|
表 33.2.2 returnCode 返回码
由上表可知,当 returnCode=0、1 或 2 这三种情况时,都表示订阅成功;具体返回的数字是多少,根据订阅主题时 QoS 的不同,服务端的返回码也会有所不同!
UNSUBSCRIBE--取消订阅主题
客户端订阅了某一主题之后,可以随时取消订阅,MQTT 协议提供了这样的操作。
客户端通过向服务端发送一个 UNSUBSCRIBE 报文来取消订阅主题,当服务端接收到 UNSUBSCRIBE 报文后,会向发送发回复一个 UNSUBACK 报文(取消订阅确认报文),如下图所示:
图 33.2.10 客户端取消订阅主题
UNSUBSCRIBE 报文包含两个重要信息,第一个是取消订阅的主题名称,同一个 UNSUBSCRIBE 报文可以同时包含多个取消订阅的主题名称。另外,UNSUBSCRIBE 报文也包含“报文标识符”,MQTT 设备可以通过该标识符对报文进行管理。
当服务端接收到 UNSUBSCRIBE 报文后,会向客户端发送取消订阅确认报文 – UNSUBACK 报文。
该报文含有客户端所发送的“取消订阅报文标识符”。
客户端接收到 UNSUBACK 报文后就可以确认取消主题订阅已经成功完成了。
主题的进阶知识
通过前面的学习,我们可以知道在 MQTT 通信当中,主题是一个很重要的概念之一。在本小节中,我们来进一步了解 MQTT 主题的概念以及关于主题的进阶知识点。
1、主题的基本形式
主题的基本形式就是一个字符串,譬如:"myTopic"、"currentTemp"、"LEDControl"等,虽然看起来简单,但是有几个点需要大家注意一下:
⚫ 主题是区分大小写的。所以"LEDControl"和"ledControl"是两个不同的主题。
⚫ 主题可以使用空格。譬如"LED Control",虽然主题允许使用空格,但是笔者建议大家尽量不要使用空格。
⚫ 不要使用中文主题。虽然有些 MQTT 服务器支持中文主题,但是绝大部分 MQTT 服务器是不支持中文主题的,所以大家不要使用中文主题,而是使用 ASCII 字符来作为 MQTT 主题。
2、主题分级
MQTT 主题可以是一个简单的字符串,譬如:"myTopic"、"currentTemp"、"LEDControl",事实上,MQTT 协议为了更好的对主题进行管理和分类,支持主题分级,对主题进行分级处理,各个级别之间使用" / "符号进行分隔。如下所示:
"home/sensor/led/brightness"
在以上示例中一共有四级主题,分别是第 1 级 home、第 2 级 sensor、第三级 led、第 4 级 brightness。主题的每一级至少需要一个字符;而只有一个简单字符串的主题,譬如"myTopic"、"currentTemp"、 "LEDControl",这些都是单一级别的主题。我们再来看几个分级主题的示例:
"home/sensor/kitchen/temperature"
"home/sensor/kitchen/brightness"
"home/sensor/bedroom/temperature"
"home/sensor/bedroom/brightness"
需要注意的是,主题名称不要使用" / "开头,譬如:
"/home/sensor/led/brightness" 这样是不行的。 3、主题通配符
当客户端订阅主题时,可以使用通配符同时订阅多个主题。通配符只能在订阅主题时使用,下面我们将
介绍两种通配符:单级通配符和多级通配符。
单级通配符:+ 单级通配符可以匹配任意一个主题级别,注意是一个主题级别,譬如示例如下:
"home/sensor/+/status"
当客户端订阅了上述主题之后,将会收到以下主题的信息内容:
"home/sensor/led/status"
"home/sensor/key/status" "home/sensor/beeper/status"
......
相反,而以下这些主题的信息是无法接收到的:
"dt/sensor/led/status"
"home/kash/key/status" "home/sensor/led/brightness"
......
以上这些注意将无法接收到,原因在于这些主题无法与"home/sensor/+/status"相匹配。这就是单级通配符的概念。
多级通配符:# 多级通配符自然是可以匹配任意数量个主题级别,而不再是单一主题级别,多级通配符使用“#”号来表示,譬如:
"home/sensor/#"
当客户端订阅了上面这个主题之后,便可以收到如下注意的信息:
"home/sensor/led"
"home/sensor/key"
"home/sensor/beeper"
"home/sensor/led/status"
"home/sensor/led/brightness"
"home/sensor/key/status"
"home/sensor/beeper/status"
......
相反,如下主题的信息是无法接收到的:
"home/kash/led"
"dt/sensor/led"
"dt/kash/led"
......
这就是多级通配符的概念。
4、主题应用注意事项
以$开头的主题
以$号开头的主题是 MQTT 服务端系统保留的特殊主题,客户端不可随意订阅或向其发布信息,譬如:
"$SYS/monitor/Clients"
"$SYS/monitor/+"
"$SYS/#"
以上这些主题示例我们不可随便订阅或向其发布信息,类似的主题还有很多,但都是以$符号开头,这里便不再一一列举了。
不要使用“/”作为主题开头
前面就给大家提到过了,我们尽量不要使用“/”作为主题的开头,这样做没有什么意义,而且额外产生一个没有用处的主题级别。
主题中不要使用空格
虽然,MQTT 支持在主题中使用空格,但是我们应该尽量避免使用空格。因为空格在编程当中是一个比较特殊的字符,除了空格之外的其它特殊字符也应该不要使用。
保持主题简洁明了
MQTT 是一种轻量级的通讯协议,它常用于网络带宽受限的环境,因此我们应尽量让主题简洁明了,从而让设备间交互的内容更加简洁,以更好的适应网络带宽受限的环境。
主题中尽量使用 ASCII 字符
虽然有些 MQTT 设备支持 UTF-8 字符作为 MQTT 主题,但是笔者建议您在主题中尽量使用 ASCII 字符。
33.3MQTT 初体验
到目前为止,我们已经学习了 MQTT 通信的基本原理、客户端连接服务端的原理、客户端发布消息、订阅主题等等理论知识。但是光有理论知识是不够的,还需要动手实践来验证我们的理论知识,那本小节我们将一起动手实践来体验 MQTT 通信的魅力!
既然是 MQTT 通信,那就需要有两个重要的角色:MQTT 客户端和 MQTT 服务端。本小节我们将使用电脑作为客户端,而服务端我们将使用本地搭建 MQTT 服务器。
下载、安装 MQTT.fx 客户端软件
想要将电脑作为 MQTT 客户端,我们需要在电脑上安装一个 MQTT 客户端软件,MQTT 客户端软件很多,这里笔者推荐 MQTT.fx 这款软件,这款软件是目前主流的 MQTT 桌面客户端,它支持 Windows、Mac、
Linux 等多种操作系统,它的官网是 http://mqttfx.jensd.de/。
进入到官网下载 MQTT.fx 软件,如下所示:
图 33.3.1 MQTT.fx 下载地址以 1.7.1 版本为例,进入到 http://www.jensd.de/apps/mqttfx/1.7.1/链接地址进行下载,如下:
图 33.3.2 1.7.1 版本下载地址
根据自己的需求下载即可!我们已经下载好 mqttfx 安装包并放到开发板光盘中了,路径为:3、软件
-> mqttfx-1.7.1-windows-x64.exe。笔者使用的是 Windows 64 位系统,所以笔者下载的是最后一个,下载完成之后得到一个 exe 安装包文件:
图 33.3.3 mqttfx-1.7.1-windows.exe 直接双击即可安装,傻瓜式安装,非常简单!安装完成,打开软件之后界面如下所示:
图 33.3.4 MQTT.fx 软件界面 至于怎么使用,稍后再向大家介绍。
MQTT 服务端
MQTT 客户端搞定之后,接下来就是 MQTT 服务端的问题了。我们可以使用现成的 MQTT 服务器,也可以自己搭建一个 MQTT 服务器。
公用 MQTT 服务器
使用现有的 MQTT 服务器,譬如阿里云、百度云、华为云等提供的 MQTT 服务,不过这些大平台貌似都是收费的;对于我们学习测试来说非常不友好,那既然如此我们还有别的选择吗?当然有,我们可以使用公用 MQTT 服务器,这些服务器都是免费供大家学习测试使用的,需要注意的是这些公用 MQTT 服务器仅用于学习测试,不可拿来商用!以下给大家列举了一些公用 MQTT 服务器: test.mosquitto.org(国外)
MQTT 服务器地址:test.mosquitto.org
TCP 端口:1883
TCP/TLS 端口:8883
WebSockets 端口:8080
Websocket/TLS 端口:8081
broker.hivemq.com(国外)
MQTT 服务器地址:broker.hivemq.com
TCP 端口:1883
WebSockets 端口:8000
iot.eclipse.org(国外)
MQTT 服务器地址:broker.hivemq.com
TCP 端口:1883
WebSockets 端口:8000
以上几个公用 MQTT 服务器都是国外的,大家可能因为网络的问题连接不上或者连接很慢,延迟会比较大;以下几个则是国内的公用 MQTT 服务器:然也物联(国内)
官网地址:http://www.ranye-iot.net
MQTT 服务器地址:test.ranye-iot.net
TCP 端口:1883
TCP/TLS 端口:8883
通信猫(国内)
MQTT 服务器地址:mq.tongxinmao.com
TCP 端口:1883
推荐大家使用国内的这些 MQTT 服务器,连接快、延迟低!自己搭建 MQTT 服务器
本章我们使用自己搭建的服务器,仅用于学习测试,不可拿来商用!并且搭建的服务器也只能在局域网内使用,外部网络无法接入。
我们在 ubuntu 下使用 mosquitto 搭建 MQTT 服务器,首先将包含 mosquitto 的库加入软件源,然后更新软件源,再安装 mosquitto 服务端
|
sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa |
//加入库 |
|
sudo apt-get update |
//更新软件源 |
|
sudo apt-get install mosquito |
//安装 mosquito |
图 33.3.5 安装好 mosquitto 服务端
mosquito 安装步骤完成后,进入到 /etc/mosquito/conf.d/ 目录。此目录内,所有以 .conf 为扩展名的文件均会被自动识别为 mosquitto 的配置文件,并在 mosquitto 服务启动过程中被加载和应用,确保配置信息得以生效。
这里我们创建一个自己的配置文件,命名为 myconfig.conf。
sudo vi /etc/mosquitto/conf.d/myconfig.conf
图 33.3.6 创建配置文件 myconfig.conf
#添加监听端口 1883
listener 1883
#关闭匿名访问,客户端必须使用用户名
allow_anonymous false
#指定用于存放用户名和密码的文件
password_file /etc/mosquitto/pwfile.txt
图 33.3.7 配置 myconfig.conf
返回到上层目录,到 /etc/mosquitto 路径下,创建一个 pwfile.txt 文件用于存放用户名和密码。通过 mosquitto_passwd 来设置用户名和密码,笔者用户名就设置为 mqtt1,然后回车,设置密码为 123456。
(密码需要输入两次)
sudo touch pwfile.txt
sudo mosquitto_passwd /etc/mosquitto/pwfile.txt 用户名
图 33.3.8 创建用户名和密码
配置完成后就可以启动 mosquitto,有关的命令如下:
|
sudo service mosquitto start |
|
//启动服务 |
|
sudo service mosquitto status |
|
//查看服务状态 |
|
sudo service mosquitto stop |
|
//停止服务 |
图 33.3.9 启动 mosquitto
从图中可以看到 mosquitto 服务器正在运行,表明配置成功,接下来就可以开始测试。
动手测试现在我们要动手进行 MQTT 通信测试了,首先打开之前安装的 MQTT 客户端软件 MQTT.fx:
图 33.3.10 MQTT.fx 软件
打开之后如上图所示,点击上图中红框标注的齿轮按钮打开配置界面:
图 33.3.11 创建一个新的配置
图 33.3.12 设置 MQTT 用户名和密码
首先点击 1 所示的“+”号创建一个新的配置,然后填写好各个配置参数。这里有一点需要注意一下,
MQTT 服务器地址填写的是 192.168.6.161,这个是笔者 ubuntu 的 ip 地址,能够与 windows、开发板局域网互相通信。读者的 MQTT 服务器地址填写自己对应的 ip 即可。
注意:笔者的网络环境是电脑和开发板都通过网线连接路由器,windows、ubuntu、开发板三者间能够互相 ping 通。若其他网络连接方式请自行测试。
MQTT 用户名和密码要填写之前在 mosquitto 服务端注册的,用户名为 mqtt1,密码为 123456。
图 33.3.13 笔者 ubuntu 的 ip 地址
配置完成之后,点击 4 所示的 Apply 按钮应用配置,最后点击右上角“X”关闭窗口:
图 33.3.14 连接服务器连接成功之后如下所示:
图 33.3.15 连接成功
这样我们的电脑作为 MQTT 客户端就已经成功连接到 MQTT 服务器了,接下来我们便可以发布消息、订阅主题以及取消订阅主题了。
订阅主题
我们先订阅一个主题,如下图所示:
图 33.3.16 订阅主题
首先点击 1 所示的“Subscribe”切换到主题订阅界面,接着填写需要订阅的主题名称,示例中我们使用了分级主题“dt2914/testTopic”,最后点击 3 所示的“Subscribe”按钮向服务器发出订阅请求。订阅成功之后如下图所示:
图 33.3.17 订阅成功
左边会列举出当前客户端所订阅的主题,右边显示消息列表。
发布消息
上例中我们的电脑客户端已经成功订阅了主题“dt2914/testTopic”,接下来我们尝试向该主题发布消息。
如下图所示:
图 33.3.18 发布消息
首先点击 1 所示的“Publish”字样切换到发布消息界面,在 2 所示处填写主题名称,这里笔者需要向 “dt2914/testTopic”主题发布消息;在 3 所示空白处填写需要发布的内容,然后点击 4 所示的“Publish”按钮发布消息。
此时我们的电脑客户端便会接收到自己发布的消息,如下所示:
图 33.3.19 接收到消息这样可能体现不出效果,因为上例中是自己订阅了“dt2914/testTopic”主题,接着又是自己向该主题发布消息,虽然是自发自收,但是这个消息肯定是经过了 MQTT 服务端的。
既然如此,大家可以直接在电脑上运行两个 MQTT.fx 进程,连接服务器时使用不同的 Client ID,这样就是两个不同的 MQTT 客户端了。
大家可以自己去测试,笔者就不再啰嗦了!取消订阅主题取消订阅主题非常简单,如下图所示:
图 33.3.20 取消订阅主题
使用手机作为客户端
前面我们使用电脑作为客户端进行了测试,其实我们的手机也可以作为 MQTT 客户端,同样也需要安装一个客户端软件,这里笔者使用的是 MQTT Client 工具,在手机的应用商城可以找到该软件,下载安装即可!安装完成之后如下所示:
注意:此方式测试需要开发板和电脑连接路由器,手机连接路由器的 wifi,并且三者都在同一个网段下,能够互相ping通。其他网络连接方式请自行测试。
图 33.3.21 手机端 MQTT Clinet 软件 打开该软件然后进行配置、连接 MQTT 服务器,如下所示:
图 33.3.22 手机端连接 MQTT 服务器
一样是要填写好各个参数的配置,Client ID 不能设置跟之前电脑端的 test1 一样,所以这里设置为 phone。
连接成功之后如下所示:
图 33.3.23 手机端连接 MQTT 服务器成功在下边的“Subscribe to a Topic”栏中填写主题信息,然后点击“Subscribe”按钮可订阅主题。“箭头” 标识的按钮是发布消息栏,可向指定主题发布消息。
图 33.3.24 手机端 MQTT Client 界面
现在我们进行一个测试,在前面的示例中,我们的电脑客户端订阅了“dt2914/testTopic”主题,现在我们要使用手机客户端向“dt2914/testTopic”主题发布消息,如下所示:
图 33.3.25 手机端发布消息
点击“Publish”按钮发布消息,手机端就会向“dt2914/testTopic”主题发送“Hello,my name is Dt”消息,接着电脑客户端便会接收到手机发布消息,如下所示:
图 33.3.26 test1 接收手机端消息
以上便是本小节的全部内容了,下小节我们将进一步来学习 MQTT 协议相关的理论知识!
33.4MQTT 协议(下)
在 36.2 小节中给大家介绍了 MQTT 通信的基本原理,本小节我们将进一步来学习 MQTT 协议相关的理论知识,包括 QoS 的概念、保留消息、MQTT 心跳机制、遗嘱的概念、以及用户密码认证等内容,废话不多说,直接开始吧!
QoS 是什么?
前面已经多次提及到 QoS 这个概念,但当时并未向大家解释 QoS 的概念。本小节我们就来解释下 QoS 究竟是什么!
QoS 是什么?
QoS 是 Quality of Service 的缩写,所以中文名便是服务质量。一个物联网通信中有些信息非常重要,我们需要确保这类重要信息可以准确无误的发送和接收,而有些信息则相对不那么重要,这类信息如果在传输中丢失不会影响系统的运行;QoS 便用于告诉客户端或服务器哪些信息是重要信息,需要准确无误的传输、不可丢失;哪些信息不是那么重要,即使在传输过程中丢失也无妨!
MQTT 设计了一套保证消息稳定传输的机制,包括消息应答、存储和重传。在这套机制下,提供了三种不同级别的 QoS(Quality of Service),也就是 MQTT 协议有三种服务质量等级:
⚫ QoS = 0:最多发一次;
⚫ QoS = 1:最少发一次;⚫ QoS = 2:保证收一次。
以上三种不同的服务质量级别意味着不同的 MQTT 传输流程。对于较为重要的 MQTT 消息,我们通常会选择 QoS>0 的服务级别(即 QoS 为 1 或 2)。
另外这里提到的“发”与“收”有两种可能。一种是客户端发布消息时,将消息发送给服务端。一种是客户端订阅了某一主题消息后,服务端将消息发送给客户端。因此发布消息和接收消息的可能是服务端也可能是客户端。为了避免为您造成混淆,我们在本节教程后面的描述中将使用“发送端”来描述发送 MQTT 消息的设备,而使用“接收端”来描述接收 MQTT 消息的设备。
接下来我们仔细看一下这三种服务质量级别的具体含义。
QoS = 0:最多发一次
0 是服务质量 QoS 的最低级别。当 QoS 为 0 级时,MQTT 协议并不保证所有信息都能得以传输。也就是说,QoS=0 的情况下,MQTT 服务端和客户端不会对消息传输是否成功进行确认和检查。消息能否成功传输全看网络环境是否稳定。
也就是说发送一次之后就不管了,最多一次,不管发送是否失败!发送端一旦发送完消息后,就完成任务了,发送端不会检查发出的消息能否被正确接收到。
在网络环境稳定的情况下,信息传输一般是不会出现问题的。这完全依赖于 TCP 重传机制,如果网络不好,TCP 的重传也不是 100%可靠,加上 MQTT 是发送方发出去的消息是依赖代理服务器完成转发的,所以消息最多一次。
QoS = 1:最少发一次
当 QoS 级别为 1 时,发送端在消息发送完成后,会检查接收端是否已经成功接收到了消息,如下图所示:
图 33.4.1 QoS1 消息传输情况
发送端向接收端发送 PUBLISH 报文,当接收端收到 PUBLISH 报文后会向发送端回复一个 PUBACK 报文,如果发送端收到 PUBACK 报文,那么它就知道消息已经被接收端成功接收!
假如过了一段时间后,发送端没有收到 PUBACK 报文,那么发送端会再次发送消息(发送 PUBLISH 报文),然后再次等待接收端的 PUBACK 确认报文。因此,当 QoS=1 时,发送端在一定时间内没有收到接收端的 PUBACK 确认报文,会重复发送同一条消息。
所以 QoS=1 时,每一条消息就至少会传输一次,但也可能会重复传输多次。当发送端重复发送一条消息时,会将 PUBLISH 报文中的 dup 标志设置为 true,如图 36.2.8 中所示。这就是为了告诉接收端,此消息为重复发送的消息,那么我们的 MQTT 客户端在接收到消息之后,可以去判断 dup 标志以确定此消息是否为重复消息,应用程序应该对此作出相应的处理。
注意:Qos=1 时,MQTT 服务器是不会进行去重的,只要发布者或者服务器没有收到 PUBACK 报文,就认为主题消息没有发送成功进入重发;服务器或者订阅者,不会根据 dup 标志的值进行去重(也就是说协议本身不会去重),需要我们的客户端应用程序去进行判断、处理。
QoS = 2:保证收一次
MQTT 服务质量最高级是 2 级,即 QoS=2。当 MQTT 服务质量为 2 级时,MQTT 协议可以确保接收端只接收一次消息(注意是只接收到一次,在 QoS=1 的情况下,接收端接收到消息的次数可能不止一次:>=1)。
为了确保接收端只接收到一次消息,PUBLISH 报文的收发过程相对更加复杂。发送端需要接收端进行两次消息确认,因此,2 级 MQTT 服务质量是最安全的服务级别,也是最慢的服务级别。我们来看看整体的过程:
图 33.4.2 QoS2 消息传输情况从上到下,按照 1、2、3、4 的顺序进行:
①、 首先发送端向接收端发送 PUBLISH 报文;
②、 接收端接收到 PUBLISH 报文后,向发送端回复一个 PUBREC 报文(官方称其为--发布收到);
③、 发送端接收到 PUBREC 报文后,会再次向接收端发送 PUBREL 报文(官方称其为--发布释放);
④、 接收端接收到 PUBREL 报文后,会再次向发送端回复一个 PUBCOMP 报文(官方称其为--发布完成),如果发送端接收到 PUBCOMP 报文表示消息传输成功,它确认接收端已经成功接收到消息,整个过程结束!
以上只是列出了 QoS2 消息传输的基本流程,那 MQTT 协议是如何保证接收端只能接收到一次消息的呢?关于这个问题我们不再向大家进行介绍,因为这些都是由 MQTT 协议来控制完成的,对于应用编程来说,我们并不需要知道其内部实现的机制和原理。当然,如果大家有兴趣,可以自己百度了解!所以您只需要牢记一点,那就是 QoS=2 可以保证接收端只收一次消息。
如何设置 QoS?
了解了 QoS 之后,我们该如何在 MQTT 通信中设置 QoS 等级呢?其实非常简单,发布消息和订阅主题时都可以设置 QoS,前面都已经给大家讲过了;PUBLISH 报文中包含了 qos 参数,用于设置客户端发布消息时使用的 QoS 级别;同理,SUBSCRIBE 报文中也包含了 qos 参数,用于设置客户端订阅主题时使用的
QoS 级别。
换句话说,无论是发布(PUBLISH)还是订阅(SUBSCRIBE),都可以使用数据包中的 qos 参数设置服务质量级别。在前面我们使用的 MQTT.fx 客户端软件中也可以体现出来:
图 33.4.3 MQTT.fx 发布消息时设置 QoS 点击选中 QoS0 时表示发布消息时将 QoS 等级设置为最低级 0。
点击选中 QoS1 时表示发布消息时将 QoS 等级设置为 1。
点击选中 QoS2 时表示发布消息时将 QoS 等级设置为最高级 2。
图 33.4.4 MQTT.fx 订阅主题时设置 QoS 点击选中 QoS0 时表示订阅主题时将 QoS 等级设置为最低级 0。
点击选中 QoS1 时表示订阅主题时将 QoS 等级设置为 1。
点击选中 QoS2 时表示订阅主题时将 QoS 等级设置为最高级 2。
另外,要想实现 QoS>0 的 MQTT 通信,客户端在连接服务端时务必要将 cleanSession 设置为 false。如果这一步没有实现,那么客户端是无法实现 QoS>0 的 MQTT 通信,因为如果 cleanSession 设置为 true,则意味着客户端不会接收到任何离线消息,包括 QoS1 和 QoS2 的情况。所以这一点非常关键,请您务必要留意。
服务质量降级
讲到这里,不知道有没有朋友会感到好奇。假如客户端在发布消息和订阅主题时使用不同级别的 QoS,将会发生什么情况呢。如下图所示,假如客户端 A 发布到主题 1 的消息是采用 QoS=2,然而客户端 B 订阅主题 1 采用 QoS = 1。那么服务端该如何来应对这一情况呢?
图 33.4.5 MQTT 发布/订阅示例 1
在这种情况下,服务端会使用较低级别 QoS 来提供服务。如上图所示,虽然 A 发送到主题 1 的消息采用 QoS 为 2,但是服务端发送主题 1 的消息给 B 时,采用的 QoS 为 1。这是因为 B 在订阅主题 1 时采用的
QoS 为 1。
总之,对于发布和订阅消息的客户端,服务端会主动采用较低级别的 QoS 来实现消息传输。
保留消息
前面我们提到,PUBLISH 报文中有一个 retain 标志,也就是保留标志,是一个布尔值,当 retain 设置为 true 时表示保留消息,如果设置为 false 表示不保留消息。那保留消息到底是什么意思、有什么特殊用途呢?这些答案将在本小节揭晓!保留消息的作用
要说明“保留消息”这一概念,我们先看一个场景。假设我们正在利用 MQTT 协议开发一套智能家居物联网系统,该系统中有一台专门用于检测和发布室温信息的 MQTT 客户端,它每到整点时(也就是每隔一个小时)就会测量当前室温并且向 MQTT 服务端发布室温测量结果。
假设在该智能家具物联网系统中,还有一台信息显示客户端。这台客户端的作用就是把当前的室温显示在屏幕上以便我们实时了解室内温度。换句话说,这台信息显示客户端一启动就会订阅室温主题,这样室温检测客户端一发布消息,显示客户端就能获取到最新的温度消息并显示在屏幕上。
假设某天上午 7:00,我们的室温检测客户端将最新的室温消息发布到了服务端,那么订阅了室温消息的显示客户端也就马上获取到室温消息并且显示在屏幕上。然而在 7:05 分的时候,由于一些原因导致显示客户端发生了重启,显示客户端重启之后、再次启动程序后立刻订阅室温主题。
但这时候问题出现了,室温测量客户端每到整点才发布一次温度信息。上一次发布时间是 7:00,下一次发布时间是 8:00。所以,尽管显示客户端又重新订阅了室温主题,它还要等到 8:00 钟才能收到最新室温消息。在 8:00 前的几十分钟里,显示客户端无法获知当前室温信息,也就无法将室温信息显示在屏幕上供我们查阅。
为了避免以上情况出现,我们可以让室温测量客户端在每次向室温主题发布消息时将 retain 标志设置为 true,以告诉服务端接收到此消息之后需要保留这个消息,这样服务端就会将该消息进行存储、保留,无论显示客户端在任何时间订阅室温主题,订阅之后都会马上收到该主题中的“保留消息”,也就是温度测量客户端发布的最新室温消息。
这就是保留消息的作用,其实非常简单,就是让服务端对客户端发布的消息进行保留,如果有其它客户端订阅了该消息对应的主题时,服务端会立即将保留消息推送给订阅者,而不必等到发送者向主题发布新消息时订阅者才会收到消息。
更新保留消息
但是需要注意的是,每一个主题只能有一个“保留消息”,如果客户端想要更新“保留消息”,就需要向该主题发送一条新的“保留消息”,这样服务端会将新的“保留消息”覆盖旧的“保留消息”。当有客户端订阅该主题时,服务端就会将最新的“保留消息”发送给订阅者。
删除保留消息
如果我们要删除保留消息又该怎么做呢?其实非常简单,只需要向该主题发布一条空的“保留消息”,即可。
使用 MQTT.fx 客户端进行测试
以上给大家详细讲解了 MQTT 协议中“保留消息”的作用,以及如何更新或删除保留消息,那接下来我们将使用 MQTT.fx 客户端进行测试、验证。
首先打开 MQTT.fx 客户端软件,连接好服务器,接下来进行测试,我们先向某个主题发布消息,譬如
“dt2914/testTopic”主题:
图 33.4.6 发布消息(不保留)
上图中右边的“Retain”按钮也就是保留标志,当点击选中时表示使能“保留消息”这一功能,如果未选中,则表示禁用“保留消息”这一功能,也就是消息不保留。我们先不保留消息,填写好需要发送的内容之后,点击“发送”按钮发布消息。
现在我们订阅“dt2914/testTopic”主题:
图 33.4.7 订阅主题
订阅之后我们的客户端并没有接收到前面发布的消息,因为发布在前、订阅在后,必须要等到发布者下一次向该主题发布新消息时才会收到。
现在取消订阅主题“dt2914/testTopic”,然后再向主题“dt2914/testTopic”发布消息,此时我们使能“保留消息”这一功能,如下:
图 33.4.8 发布消息(保留消息)点击 Publish 按钮发布消息,之后我们再重新订阅主题“dt2914/testTopic”,如下:
图 33.4.9 订阅主题(保留消息)
当订阅之后我们的客户端立马就收到了一条消息,并且还标识了这条消息是“保留消息”。这就是“保留消息”的作用。除此之外,大家还可以自己测试更新/删除保留消息,这里就不再演示了。
MQTT 的心跳机制
在医院里,医生利用心跳来判断患者是否还有生命体征。对于 MQTT 服务器来说,它要判断 MQTT 客户端是否依然与它保持着连接,就是检查该客户端是不是经常给它发送数据包,如果经常收到客户端的消息,那么证明该客户端肯定在线。
但是有些客户端并不经常发送消息给服务端,譬如有些客户端每隔一天或没隔一个小时才会向服务端发送消息,除此之外,还有些客户端甚至不会向服务端发送消息,它只订阅主题、负责接收服务端发送给它的消息;那么对于这种客户端,MQTT 协议使用类似心跳检测的方法,来判断客户端是否在线,这就是我们说的心跳机制。
心跳机制的原理就在于:让客户端在没有向服务端发送消息的这个空闲时间里,定时向服务端发送一个心跳包,这个心跳包被称为心跳请求,其实质就是向服务端发送一个 PINGREQ 报文;当服务端收到 PINGREQ 报文后就知道该客户端依然在线,然后向客户端回复一个 PINGRESP 报文,称为心跳响应!如下图所示:
图 33.4.10 心跳请求
由于心跳请求是定时发送的(通过 keepAlive 设置时间间隔,也是告诉服务端,客户端将会多少多少秒向它发送心跳请求,这样服务端就会知道了);一旦服务器未收到客户端的心跳包,那么服务器就会知道,这台客户端可能已经掉线了。
这个心跳机制不仅可以用于服务端判断客户端是否在线,客户端也可使用心跳机制来判断自己与服务端是否保持连接。如果客户端在发送心跳请求(PINGREQ)后,没有收到服务端的心跳响应(PINGRESP),那么客户端就会认为自己与服务端已经断开连接了。
MQTT 的遗嘱机制
“遗嘱”,大家听到这个词是不是感觉跟死亡有关系,事实确实如此,不过在我们的 MQTT 协议中,死亡指的是“客户端掉线”、“与服务端断开了连接”这种意思。
客户端断开与服务端的连接通常是有两种方式的:
⚫ 客户端主动向服务端发送 DISCONNECT 报文,请求断开连接,自然服务端也就知道了客户端要离线了;
⚫ 客户端意外掉线。被动与服务端断开了连接。
客户端意外掉线这种情况对于物联网设备来说是比较常见的,也是必须要考虑在内的情况。前面我们在介绍 MQTT 协议的时候就提到过,MQTT 从诞生之初就是专为低带宽、高延迟或不可靠网络等环境而设计的;所以针对这种意外掉线的情况,MQTT 协议使用了遗嘱机制来服务客户端、管理客户端。
MQTT 协议允许客户端在“活着”的时候就写好遗嘱,这样一旦客户端意外断线,服务端就可以将客户端的遗嘱公之于众。请注意,在这段话中,我将意外断线这几个字特意做了加粗处理,这是因为,客户端的遗嘱只在意外断线时才会发布,如果客户端正常的断开了与服务端的连接(主动断开),这个遗嘱机制是不会启动的,服务端也不会将客户端的遗嘱公布。
那什么是意外断线?其实除了客户端主动向服务端发送 DISCONNECT 报文请求断开连接这种情况之外,其它断线的情况都属于意外断开连接,譬如网络不稳定、客户端设备没电关机了等。
客户端如何设置自己的“遗嘱”信息
客户端连接服务端时发送的 CONNECT 报文中有这样几个参数,如下图红框中所示:
图 33.4.11 CONNECT 报文信息
这几个参数都是以 will 开头的,will 其实就是“遗嘱”的英文单词,所以由此可知,遗嘱需要在客户端连接服务端时就需要设置好,下面分别介绍一下: willTopic -- 遗嘱主题
遗嘱消息和普通 MQTT 消息很相似,也有主题和正文内容。willTopic 的作用正是告知服务端,本客户端的遗嘱主题是什么。只有那些订阅了这一遗嘱主题的客户端才会收到本客户端的遗嘱消息。
以上图为例,此遗嘱主题为“clientWill”,也就是说,只有订阅了主题“clientWill”的客户端,才会收到这台客户端的遗嘱消息。当然,客户端也可以主动向遗嘱主题发布消息,这样通常会有一些妙用,譬如当客户端 A 上线时可以向自己的遗嘱主题发布一条消息,那么那些订阅了该遗嘱主题的客户端可以收到这条消息,这些订阅者也就知道了客户端 A 已经上线了。所以这个可以用来实现一个上线通知的小功能。
willMessage -- 遗嘱消息
遗嘱消息定义了遗嘱的内容。在本示例中,那些订阅了主题“clientWill”的客户端会在客户端意外断线时,收到服务端发布的“client offline”这样的信息。
willRetain -- 遗嘱消息的保留标志
遗嘱消息也可以设置为保留标志,用于告诉服务端是否需要对遗嘱消息进行保留处理。
willQoS -- 遗嘱消息的 QoS
对于遗嘱消息来说,同样可以使用服务质量来控制遗嘱消息的传递和接收。这里的服务质量与普通 MQTT 消息的服务质量是一样的概念。也可以设置为 0、1、2。对于不同的服务质量级别,服务端会使用不同的服务质量来发布遗嘱消息。
MQTT.fx 如何设置客户端的“遗嘱”
MQTT.fx 软件如何为电脑客户端设置遗嘱呢?首先进入到配置页面中,如下所示:
图 33.4.12 MQTT.fx 软件遗嘱设置
MQTT 用户密码认证到目前为止,CONNECT 报文中还有两个参数没给大家介绍,如下所示:
图 33.4.13 CONNECT 报文
也就是上图中红框所示的两个参数:username(用户名)和 password(密码),这里的用户名和密码是客户端连接服务端时进行认证所需要的。
有些 MQTT 服务端需要客户端在连接时提供用户名和密码,只有客户端正确提供了用户名和密码后,才能连接服务端,否则服务端将会拒绝客户端连接,那么客户端也就无法发布和订阅消息了。但有些 MQTT 服务端并不需要客户端提供用户名、密码进行认证。所以这个 username 和 password 是可选的参数,而非必须的,重点在于 MQTT 服务端是否需要用户名、密码认证。
有些 MQTT 服务端开启了用户名、密码认证,这种服务端需要客户端在连接时正确提供用户名、密码认证信息才能连接成功;当然,那些没有开启用户名、密码认证的服务端无需客户端提供用户名和密码认证信息。
前面在测试 MQTT.fx 客户端的时候,就有使用用户名和密码认证,当时是在 MQTT 服务端注册的用户,用户名是 mqtt1、密码是 123456。
图 33.4.14 用户名密码配置
用户名和密码除了用于在连接服务端时进行认证、校验这一功能外,有些 MQTT 服务端也利用此信息来识别客户端属于哪一个用户,从而对客户端进行管理。譬如用户可以拥有私人主题,这些主题只有该用户可以发布和订阅;对于私人主题,服务端就可以利用客户端连接时的用户名和密码来判断该客户端是否有发布订阅该用户私人主题的权限。
33.5移植 MQTT 客户端库
前面的示例中,我们使用 MQTT.fx 客户端软件在自己的电脑上进行了测试(或在手机上使用 MQTT Client 工具进行测试),如果需要在开发板上进行测试,将开发板作为 MQTT 客户端,我们需要自己去编写客户端程序。
首先在编写客户端程序之前,需要移植 MQTT 客户端库到我们的开发板上,基于 MQTT 客户端库来编写一个 MQTT 客户端应用程序。那么接下来笔者将向大家介绍如何移植 MQTT 客户端库。
下载 MQTT 客户端库源码
如何下载 MQTT 客户端库源码包?首先我们进入到 MQTT 的官网地址:https://mqtt.org/
图 33.5.1 MQTT 官网
点击“Software”链接地址,找到“Client libraries”项,如下所示:
图 33.5.2 Client libraries
MQTT 客户端库支持多种不同的编程语言,譬如 C、C++、Go、Java、Lua、Objective-C、Python 等,对于我们来说,我们使用的是 C 语言开发,所以要选择 MQTT C 客户端库,如下所示:
图 33.5.3 MQTT C 客户端库
这里有多种不同的 MQTT C 客户端库,笔者推荐大家使用第一个 Eclipse Paho C,这是一个“MQTT C Client for Posix and Windows”,Paho MQTT C 客户端库是用 ANSI 标准 C 编写的功能齐全的 MQTT 客户端库,可运行在 Linux 系统下,支持 MQTT3.1、MQTT3.1.1、MQTT5.0。点击“Eclipse Paho C”链接地址,如下:
图 33.5.4 Paho MQTT C
在这个页面中会有一些简单地介绍信息,大家可以自己看一看。我们往下看,找到它的下载地址:
图 33.5.5 找到源码链接地址
图 33.5.6 git 仓库地址点击右边的“Release”找到它的发布版本,如下所示:
图 33.5.7 Paho MQTT C 客户端库源码下载
目前最新的版本是 1.3.13,我们不使用最新版本,这里选择 1.3.8 版本,如上图所示,点击“Source code
(tar.gz)”链接地址下载客户端库源码。
下载成功之后会得到如下压缩文件:
图 33.5.8 paho.mqtt.c 客户端库源码
交叉编译 MQTT C 客户端库源码
将 paho.mqtt.c-1.3.8.tar.gz 压缩文件拷贝到 Ubuntu 系统某个目录下,如下所示:
图 33.5.9 将 paho.mqtt.c-1.3.8.tar.gz 拷贝到 Ubuntu
接着将其解压到当前目录,如下所示:
图 33.5.10 解压 paho.mqtt.c-1.3.8.tar.gz
解压成功之后会得到 paho.mqtt.c-1.3.8 文件夹,这就是 paho MQTT C 客户端库源码工程,进入到该目
录下,可以看到工程顶级目录下有一个 CMakeLists.txt 文件,所以可知这是一个由 cmake 构建的工程。
首先我们要新建一个交叉编译配置文件 arm-linux-setup.cmake,进入到 cmake 目录下,新建 arm-linuxsetup.cmake 文件,并输入以下内容:
|
################################## # 配置 ARM 交叉编译 ################################# set(CMAKE_SYSTEM_NAME Linux) #设置目标系统名字 set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构 # 指定编译器的 sysroot 路径 set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots) set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器 arm-linux-gcc set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/ arm-poky-linux-gnueabi-gcc)
# 为编译器添加编译选项 set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) ################################# # end ################################## |
这是配置交叉编译,需要根据自己实际情况修改。编写完成之后保存退出。回到工程的顶层目录,新建一个名为 build 的目录,如下所示:
图 33.5.11 创建 build 目录
进入到 build 目录下,执行 cmake 进行构建:
~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL
_PREFIX=~/tools/paho.mqtt.c-1.3.8/install -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake DPAHO_WITH_SSL=TRUE -DPAHO_BUILD_SAMPLES=TRUE ..
图 33.5.12 执行 cmake 构建
~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake 这是笔者在第三十二章的 32.5.4 设置交叉编译下载的 3.16.0 版本的 cmake 工具,您得根据自己的实际路径来指定。
CMAKE_BUILD_TYPE、CMAKE_INSTALL_PREFIX、CMAKE_TOOLCHAIN_FILE 都是 cmake 变量, CMAKE_INSTALL_PREFIX 这个指定了安装路径,笔者将安装路径设置为顶层目录下的 install 目录。除此之外,还定义了两个缓存变量 PAHO_WITH_SSL 和 PAHO_BUILD_SAMPLES,具体是什么意思大家可以自己查看工程顶级目录下的 README.md 文件,在 README.md 文件中对工程的编译进行了简单介绍。
cmake 执行完毕之后,接着执行 make 编译:
图 33.5.13 make 编译
这里需要给大家简单地说明一下,事实上,MQTT 客户端库依赖于 openssl 库,所以通常在移植 MQTT 客户端库的时候,需要先移植 openssl、交叉编译 openssl 得到库文件以及头文件,然后再来编译 MQTT 客户端库;但我们这里没有去移植 openssl,原因是,我们的开发板出厂系统中已经移植好了 openssl 库,并且我们所使用的交叉编译器在编译工程源码的时候会链接 openssl 库(sysroot 路径指定的)。
编译成功之后,执行 make install 进行安装:
图 33.5.14 make install 安装
对安装目录下的文件夹进行简单介绍进入到 install 安装目录下:
图 33.5.15 install 安装目录
在安装目录下有 bin、include、lib 以及 share 这 4 个文件夹,bin 目录下包含了一些简单的测试 demo, lib 目录下包含了我们编译出来的库文件,如下所示:
图 33.5.16 MQTT 客户端库文件一共有 4 种类型的库,这里我们简单地介绍一下:
⚫ libpaho-mqtt3a.so:异步模式 MQTT 客户端库(不支持 SSL)。
⚫ libpaho-mqtt3as.so:异步模式 MQTT 客户端库(支持 SSL)。
⚫ libpaho-mqtt3c.so:同步模式 MQTT 客户端库(不支持 SSL)。
⚫ libpaho-mqtt3cs.so:支持 SSL 的同步模式客户端库(支持 SSL)。
Paho MQTT C 客户端库支持同步操作模式和异步操作模式两种,关于它们之间的区别笔者不做介绍,顶级目录下 docs/MQTTClient/html/async.html 文档(直接双击打开)中对此有相应的解释,有兴趣的可以看一看;docs 目录下提供了很多供用户参考的文档,包括 API 使用说明、示例代码等等,在后续的学习过程中,可以查看这些文档获取帮助。
MQTT 中使用 SSL/TLS 来提供安全性(由 openssl 提供),使用 SSL 来做一些加密验证,使得数据传输更加安全可靠。
以上便给大家简单地介绍了下这 4 种库文件之间的区别,那后续我们将使用 libpaho-mqtt3c.so。
介绍完库文件之后,再来看看头文件,进入到 include 目录下:
图 33.5.17 include 头文件
在我们的 MQTT 客户端应用程序中只需要包含 MQTTAsync.h 或 MQTTClient.h 头文件即可,其它那些头文件会被这两个头文件所包含;MQTTAsync.h 是异步模式客户端库对外的头文件,而 MQTTClient.h 则是同步模式客户端库对外的头文件。因为后续我们将使用同步模式,所以到时在我们的应用程序中需要包含
MQTTClient.h 头文件。
拷贝库文件到开发板
将编译得到的库文件拷贝到开发板 Linux 系统/usr/lib 目录下,注意不要破坏原有的链接关系,建议在操作之前,先将库文件进行打包,如下所示:
图 33.5.18 打包库文件将压缩包文件 libmqtt.tar.gz 拷贝到开发板 Linux 系统/home/root 目录下,然后将其解压到/usr/lib 目录:
tar -xzf libmqtt.tar.gz -C /usr/lib
图 33.5.19 解压
33.6MQTT 客户端库 API 介绍
上小节我们已经移植了 MQTT 客户端库到我们的开发板,接下来我们便可以开始编写 MQTT 客户端应用程序了,在编写应用程序之前,笔者需要向大家简单地介绍一下 MQTT 客户端库提供的 API。
本小节我们所介绍的这些库函数都是定义在 MQTTClient.h 头文件中,也就是同步模式客户端库 API,所以在我们的应用程序中需要包含头文件 MQTTClient.h。
Tips:MQTT 客户端源码顶级目录 docs/MQTTClient/html/index.html 文档向用户介绍了 API 的使用方法,并且提供了相应示例代码供用户参考。
图 33.6.1 index.html 文档
MQTTClient_message 结构体
这里向大家介绍一个结构体 MQTTClient_message,该结构体很重要,MQTT 客户端应用程序发布消息和接收消息都是围绕着这个结构体。MQTTClient_message 数据结构描述了 MQTT 消息的负载和属性等相关信息,譬如消息的负载、负载的长度、qos、消息的保留标志、dup 标志等,但是消息主题不是这个结构体的一部分。该结构体内容如下:
|
示例代码 33.6.1 MQTTClient_message 结构体 typedef struct { int payloadlen; //负载长度 void* payload; //负载 int qos; //消息的 qos 等级 int retained; //消息的保留标志 int dup; //dup 标志(重复标志) int msgid; //消息标识符,也就是前面说的 packetId ...... } MQTTClient_message; |
当客户端发布消息时就需要实例化一个 MQTTClient_message 对象,同理,当客户端接收到消息时,其实也就是接收到了 MQTTClient_message 对象。通常在实例化 MQTTClient_message 对象时会使用
MQTTClient_message_initializer 宏对其进行初始化。
创建一个客户端对象
在连接服务端之前,需要创建一个客户端对象,使用 MQTTClient_create 函数创建:
int MQTTClient_create(MQTTClient *handle, const char *serverURI, const char *clientId, int persistence_type, void *persistence_context
);
handle:MQTT 客户端句柄; serverURL:MQTT 服务器地址; clientId:客户端 ID; persistence_type:客户端使用的持久化类型:
⚫ MQTTCLIENT_PERSISTENCE_NONE:使用内存持久性。如果运行客户端的设备或系统出现故障或关闭,则任何传输中消息的当前状态都会丢失,并且即使在 QoS1 和 QoS2 下也可能无法传递某些消息。
⚫ MQTTCLIENT_PERSISTENCE_DEFAULT:使用默认的(基于文件系统)持久性机制。传输中消息的状态保存在文件系统中,并在意外故障的情况下提供一些防止消息丢失的保护。
⚫ MQTTCLIENT_PERSISTENCE_USER:使用特定于应用程序的持久性实现。使用这种类型的持久性可以控制应用程序的持久性机制。应用程序必须实现 MQTTClient_persistence 接口。
persistence_context:如果使用 MQTTCLIENT_PERSISTENCE_NONE 持久化类型,则该参数应设置为 NULL。如果选择的是 MQTTCLIENT_PERSISTENCE_DEFAULT 持久化类型,则该参数应设置为持久化目录的位置,如果设置为 NULL,则持久化目录就是客户端应用程序的工作目录。
返回值:客户端对象创建成功返回 MQTTCLIENT_SUCCESS,失败将返回一个错误码。
使用示例
|
MQTTClient client; int rc;
/* 创建 mqtt 客户端对象 */ if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_create(&client, "192.168.6.160", "dt_mqtt_2_id", MQTTCLIENT_PERSISTENCE_NONE, NULL))) { printf("Failed to create client, return code %d\n", rc); return EXIT_FAILURE; } |
注意,"192.168.6.161"是笔者 mqtt 服务器的实际 ip 地址。
连接服务端
客户端创建之后,便可以连接服务器了,调用 MQTTClient_connect 函数连接:
int MQTTClient_connect(MQTTClient handle,
MQTTClient_connectOptions *options
);
handle:客户端句柄;
options:一个指针。指向一个 MQTTClient_connectOptions 结构体对象。MQTTClient_connectOptions 结构体中包含了 keepAlive、cleanSession 以及一个指向 MQTTClient_willOptions 结构体对象的指针 will_opts;
MQTTClient_willOptions 结构体包含了客户端遗嘱相关的信息,遗嘱主题、遗嘱内容、遗嘱消息的 QoS 等级、遗嘱消息的保留标志等。
返回值:连接成功返回 MQTTCLIENT_SUCCESS,是否返回错误码:
⚫ 1:连接被拒绝。不可接受的协议版本,不支持客户端的 MQTT 协议版本
⚫ 2:连接被拒绝:标识符被拒绝
⚫ 3:连接被拒绝:服务器不可用
⚫ 4:连接被拒绝:用户名或密码错误
⚫ 5:连接被拒绝:未授权
⚫ 6-255:保留以备将来使用
|
示例代码 36.6.2 MQTTClient_connectOptions 和 MQTTClient_willOptions typedef struct { int keepAliveInterval; //keepAlive int cleansession; //cleanSession MQTTClient_willOptions *will; //遗嘱相关 const char *username; //用户名 const char *password; //密码 int reliable; //控制同步发布消息还是异步发布消息 ...... ...... } MQTTClient_connectOptions;
typedef struct { const char *topicName; //遗嘱主题 const char *message; //遗嘱内容 int retained; //遗嘱消息的保留标志 int qos; //遗嘱消息的 QoS 等级 ...... ...... } MQTTClient_willOptions; |
使用示例:
|
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer; ......
/* 连接服务器 */ will_opts.topicName = "dt_mqtt/willTopic"; //遗嘱主题 |
will_opts.message = "Abnormally dropped"; //遗嘱内容 will_opts.retained = 1; //遗嘱保留消息 will_opts.qos = 0; //遗嘱 QoS 等级
conn_opts.will = &will_opts; conn_opts.keepAliveInterval = 30; //客户端 keepAlive 间隔时间 conn_opts.cleansession = 0; //客户端 cleanSession 标志 conn_opts.username = "mqtt1"; //用户名 conn_opts.password = "123456"; //密码 if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_connect(client, &conn_opts))) { printf("Failed to connect, return code %d\n", rc);
return EXIT_FAILURE;
}
通常在定义 MQTTClient_connectOptions 对象时会使用 MQTTClient_connectOptions_initializer 宏对其进行初始化操作;而在定义 MQTTClient_willOptions 对象时使用 MQTTClient_willOptions_initializer 宏对其初始化。
设置回调函数
调用 MQTTClient_setCallbacks 函数为应用程序设置回调函数,MQTTClient_setCallbacks 可设置多个回调函数,包括:断开连接时的回调函数 cl(当客户端检测到自己掉线时会执行该函数,如果将其设置为 NULL 表示应用程序不处理断线的情况)、接收消息的回调函数 ma(当客户端接收到服务端发送过来的消息时执行该函数,必须设置此函数否则客户端无法接收消息)、发布消息的回调函数 dc(当客户端发布的消息已经确认发送时执行该回调函数,如果你的应用程序采用同步方式发布消息或者您不想检查是否成功发送时,您可以将此设置为 NULL)。
int MQTTClient_setCallbacks(MQTTClient handle, void *context,
MQTTClient_connectionLost *cl,
MQTTClient_messageArrived *ma,
MQTTClient_deliveryComplete *dc
);
handle:客户端句柄;
context:执行回调函数的时候,会将 context 参数传递给回调函数,因为每一个回调函数都设置了一个
参数用来接收 context 参数。
cl:一个 MQTTClient_connectionLost 类型的函数指针,如下:
typedef void MQTTClient_connectionLost(void *context, char *cause);
参数 cause 表示断线的原因,是一个字符串。
ma:一个 MQTTClient_messageArrived 类型的函数指针,如下:
typedef int MQTTClient_messageArrived(void *context, char *topicName, int topicLen, MQTTClient_message *message);
参数 topicName 表示消息的主题名, topicLen 表示主题名的长度;参数 message 指向一个 MQTTClient_message 对象,也就是客户端所接收到的消息。
dc:一个 MQTTClient_deliveryComplete 类型的函数指针,如下:
typedef void MQTTClient_deliveryComplete(void* context, MQTTClient_deliveryToken dt);
参数 dt 表示 MQTT 消息的值,将其称为传递令牌。发布消息时(应用程序通过
MQTTClient_publishMessage 函数发布消息),MQTT 协议会返回给客户端应用程序一个传递令牌;应用程序可以通过将调用 MQTTClient_publishMessage()返回的传递令牌与传递给此回调的令牌进行匹配来检查消息是否已成功发布。前面提到了“同步发布消息”这个概念,既然有同步发布,那必然有异步发布,确实如何!那如何控制是同步发布还是异步发布呢?就是通过 MQTTClient_connectOptions 对象中的 reliable 成员控制的,这是一个布尔值,当 reliable=1 时使用同步方式发布消息,意味着必须完成当前正在发布的消息(收到确认)之后才能发布另一个消息;如果 reliable=0 则使用异步方式发布消息。
当使用 MQTTClient_connectOptions_initializer 宏对 MQTTClient_connectOptions 对象进行初始化时, reliable 标志被初始化为 1,所以默认是使用了同步方式。
返回值:成功返回 MQTTCLIENT_SUCCESS,失败返回 MQTTCLIENT_FAILURE。注意:调用 MQTTClient_setCallbacks 函数设置回调必须在连接服务器之前完成!使用示例:
|
static void delivered(void *context, MQTTClient_deliveryToken dt) { printf("Message with token value %d delivery confirmed\n", dt); } static int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message) { printf("Message arrived\n"); printf("topic: %s\n", topicName); printf("message: <%d>%s\n", message->payloadlen, (char *)message->payload); MQTTClient_freeMessage(&message); //释放内存 MQTTClient_free(topicName); //释放内存 return 1; } static void connlost(void *context, char *cause) { printf("\nConnection lost\n"); printf(" cause: %s\n", cause); } int main(void) { ......
/* 设置回调 */ if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_setCallbacks(client, NULL, connlost, |
msgarrvd, delivered))) {
printf("Failed to set callbacks, return code %d\n", rc); return EXIT_FAILURE;
}
......
}
对于 msgarrvd 函数有两个点需要注意:
⚫ 退出函数之前需要释放消息的内存空间,必须调用 MQTTClient_freeMessage 函数;同时也要释放主题名称占用的内存空间,必须调用 MQTTClient_free。
⚫ 函数的返回值。此函数的返回值必须是 0 或 1,返回 1 表示消息已经成功处理;返回 0 则表示消息处理存在问题,在这种情况下,客户端库将重新调用 MQTTClient_messageArrived()以尝试再次将消息传递给客户端应用程序,所以返回 0 时不要释放消息和主题所占用的内存空间,否则重新投递失败。 发布消息
当客户端成功连接到服务端之后,便可以发布消息或订阅主题了,应用程序通过
MQTTClient_publishMessage 库函数来发布一个消息:
int MQTTClient_publishMessage(MQTTClient handle, const char *topicName, MQTTClient_message *msg,
MQTTClient_deliveryToken *dt
);
handle:客户端句柄; topicName:主题名称。向该主题发布消息。
msg:指向一个 MQTTClient_message 对象的指针。 dt:返回给应用程序的传递令牌。
返回值:成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。使用示例
|
MQTTClient_message pubmsg = MQTTClient_message_initializer; MQTTClient_deliveryToken token;
......
/* 发布消息 */ pubmsg.payload = "online"; //消息内容 pubmsg.payloadlen = 6; //消息的长度 pubmsg.qos = 0; //QoS 等级 pubmsg.retained = 1; //消息的保留标志
if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_publishMessage(client, "dt_mqtt/testTopic", &pubmsg, &token))) { printf("Failed to publish message, return code %d\n", rc); |
return EXIT_FAILURE;
}
订阅主题和取消订阅主题
客户端应用程序调用 MQTTClient_subscribe 函数来订阅主题:
int MQTTClient_subscribe(MQTTClient handle, const char *topic, int qos
);
handle:客户端句柄; topic:主题名称。客户端订阅的主题。
qos:QoS 等级。
返回值:成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。
使用示例
......
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_subscribe(client, "dt_mqtt/testTopic", 0))) { printf("Failed to subscribe, return code %d\n", rc);
return EXIT_FAILURE;
}
......
当客户端想取消之前订阅的主题时,可调用 MQTTClient_unsubscribe 函数,如下所示:
int MQTTClient_unsubscribe(MQTTClient handle, const char *topic
);
handle:客户端句柄; topic:主题名称。取消订阅该主题。
返回值:成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。 断开服务端连接
当客户端需要主动断开与客户端连接时,可调用 MQTTClient_disconnect 函数:
int MQTTClient_disconnect(MQTTClient handle, int timeout
);
handle:客户端句柄;
timeout:超时时间。客户端将断开连接延迟最多 timeout 时间(以毫秒为单位),以便完成正在进行中
的消息传输。
返回值:如果客户端成功从服务器断开连接,则返回 MQTTCLIENT_SUCCESS;如果客户端无法与服务器断开连接,则返回错误代码。
33.7编写客户端程序
注意:本实验是开发板和电脑 ubuntu 在同个局域网内(二者在同个网段)的基础上测试的,必须保证开发板和 ubuntu 能够互相 ping 通。其他网络环境请自行测试。上小节我们给大家介绍一些基本的 MQTT 客户端库函数,除了这些基本 API 之外,MQTT 客户端库还提供了其它很多的 API,本章就不给大家一一介绍了,大家自己去看。那本小节我们将使用上小节中给大家介绍的几个 API 来编写一个自己的 MQTT 客户端应用程序,然后使其在我们的开发板上运行,实现自己的私人物联网项目。
我们这个小项目的功能设计如下:
⚫ 基于 mosquitto 平台搭建的本地 MQTT 服务器实现个人物联网小项目;
⚫ 用户可通过手机或电脑远程控制开发板上的一颗 LED 灯;
⚫ 开发板客户端每隔 30 秒向服务端发送 SoC 当前的温度值,用户通过手机或电脑可查看到该温度值。
示例程序笔者已经给大家写好了,由于功能比较简单,所以代码比较短,只有一个源文件;虽然只有一个源文件,为了学以致用,我们将使用 cmake 来构建这个小项目。mqtt_prj 工程对应的路径为:开发板光盘
->11、Linux C 应用编程例程源码->33_mqtt->mqtt_prj。工程目录结构如下所示:
图 33.7.1 mqtt 小项目工程目录结构
arm-linux-setup.cmake 文件
arm-linux-setup.cmake 源文件用于配置 cmake 交叉编译,其内容如下所示:
##################################
# 配置 ARM 交叉编译
#################################
set(CMAKE_SYSTEM_NAME Linux) #设置目标系统名字
set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构
# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器 arm-linux-gcc
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/
|
arm-poky-linux-gnueabi-gcc)
# 为编译器添加编译选项 set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) ################################# # end ################################## |
这个就不多说了,大家需要根据自己的交叉编译器的实际安装路径进行修改。
CMakeLists.txt 文件
文件内容如下所示:
|
#******************************************************************************* # Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved. # # 顶层 CMakeLists.txt # All rights reserved. This program and the accompanying materials # are made available under the terms of the Eclipse Public License v2.0 # and Eclipse Distribution License v1.0 which accompany this distribution. #*******************************************************************************/ cmake_minimum_required(VERSION 2.8.12) project(MQTTClient C) message(STATUS "CMake version: " ${CMAKE_VERSION}) message(STATUS "CMake system name: " ${CMAKE_SYSTEM_NAME}) message(STATUS "CMake system processor: " ${CMAKE_SYSTEM_PROCESSOR})
# 设置可执行文件输出路径 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
# 定义可执行文件目标 add_executable(mqttClient mqttClient.c)
# 指定 MQTT 客户端库头文件路径、库路径以及链接库 # ***大家需要根据 MQTT 的实际安装路径设置*** target_include_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/include) #MQTT 头文件搜索路径 target_link_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/lib) #MQTT 库文件搜索路径 target_link_libraries(mqttClient PRIVATE paho-mqtt3c) #MQTT 链接库 libpaho-mqtt3c.so |
客户端应用程序源文件 mqttClient.c 接下来我们看看客户端源码 mqttClient.c 的内容,如下所示:
|
示例代码 33.7.1 mqttClient.c 源文件内容 /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2024. All rights reserved. 文件名 : mqttClient.c 作者 : 邓涛 版本 : V1.1 描述 : 开发板上的 MQTT 客户端应用程序示例代码 其他 : 无 论坛 : www.openedv.com 日志 : V1.1 2024/04/12 邓涛创建 ***************************************************************/
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "MQTTClient.h" //包含 MQTT 客户端库头文件
/* ########################宏定义##################### */ /* * 因为本地服务器是搭建在 ubuntu 上,所以此处的服务器地址 * 要填写您 ubuntu 的 ip 地址,笔者 ubuntu 的 ip 地址为 192.168.6.161, * 你们填写自己的 ip 地址即可 */ #define BROKER_ADDRESS "192.168.6.161" //服务器地址
/* * 客户端 id、用户名、密码 * * 客户端 id 可以随意填写,只需要保证跟 MQTT.fx 客户端使用的不同, * 用户名要使用 MQTT 服务端注册的用户,即 mqtt1 * 密码也是之前设置的 123456 */ #define CLIENTID "test3" //客户端 id #define USERNAME "mqtt1" //用户名 #define PASSWORD "123456" //密码
/* |
* 以下 dt_mqtt/ 是笔者的个人主题级别,可自行设置
|
*/ #define WILL_TOPIC "dt_mqtt/will" //遗嘱主题 #define LED_TOPIC "dt_mqtt/led" //LED 主题 #define TEMP_TOPIC "dt_mqtt/temperature" //温度主题 /* ################################################# */
static int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message) { if (!strcmp(topicName, LED_TOPIC)) { //校验消息的主题 if (!strcmp("2", message->payload)) //如果接收到的消息是"2"则设置 LED 为呼吸灯模式 system("echo heartbeat > /sys/class/leds/sys-led/trigger"); if (!strcmp("1", message->payload)) { //如果是"1"则 LED 常量 system("echo none > /sys/class/leds/sys-led/trigger"); system("echo 1 > /sys/class/leds/sys-led/brightness"); } else if (!strcmp("0", message->payload)) {//如果是"0"则 LED 熄灭 system("echo none > /sys/class/leds/sys-led/trigger"); system("echo 0 > /sys/class/leds/sys-led/brightness"); }
// 接收到其它数据 不做处理 }
/* 释放占用的内存空间 */ MQTTClient_freeMessage(&message); MQTTClient_free(topicName);
/* 退出 */ return 1; } static void connlost(void *context, char *cause) { printf("\nConnection lost\n"); printf(" cause: %s\n", cause); } int main(int argc, char *argv[]) { MQTTClient client; MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; |
MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
|
MQTTClient_message pubmsg = MQTTClient_message_initializer; int rc;
/* 创建 mqtt 客户端对象 */ if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_create(&client, BROKER_ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL))) { printf("Failed to create client, return code %d\n", rc); rc = EXIT_FAILURE; goto exit; }
/* 设置回调 */ if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, NULL))) { printf("Failed to set callbacks, return code %d\n", rc); rc = EXIT_FAILURE; goto destroy_exit; }
/* 连接 MQTT 服务器 */ will_opts.topicName = WILL_TOPIC; //遗嘱主题 will_opts.message = "Unexpected disconnection"; //遗嘱消息 will_opts.retained = 1; //保留消息 will_opts.qos = 0; //QoS0
conn_opts.will = &will_opts; conn_opts.keepAliveInterval = 30; //心跳包间隔时间 conn_opts.cleansession = 0; //cleanSession 标志 conn_opts.username = USERNAME; //用户名 conn_opts.password = PASSWORD; //密码 if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_connect(client, &conn_opts))) { printf("Failed to connect, return code %d\n", rc); rc = EXIT_FAILURE; goto destroy_exit; }
printf("MQTT 服务器连接成功!\n");
/* 发布上线消息 */ |
pubmsg.payload = "Online"; //消息的内容
|
pubmsg.payloadlen = 6; //内容的长度 pubmsg.qos = 0; //QoS 等级 pubmsg.retained = 1; //保留消息 if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_publishMessage(client, WILL_TOPIC, &pubmsg, NULL))) { printf("Failed to publish message, return code %d\n", rc); rc = EXIT_FAILURE; goto disconnect_exit; }
/* 订阅主题 dt_mqtt/led */ if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_subscribe(client, LED_TOPIC, 0))) { printf("Failed to subscribe, return code %d\n", rc); rc = EXIT_FAILURE; goto disconnect_exit; }
/* 向服务端发布芯片温度信息 */ for ( ; ; ) {
MQTTClient_message tempmsg = MQTTClient_message_initializer; char temp_str[10] = {0}; int fd;
/* 读取温度值 */ fd = open("/sys/class/thermal/thermal_zone0/temp", O_RDONLY); read(fd, temp_str, sizeof(temp_str));//读取 temp 属性文件即可获取温度 close(fd);
/* 发布温度信息 */ tempmsg.payload = temp_str; //消息的内容 tempmsg.payloadlen = strlen(temp_str); //内容的长度 tempmsg.qos = 0; //QoS 等级 tempmsg.retained = 1; //保留消息 if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_publishMessage(client, TEMP_TOPIC, &tempmsg, NULL))) { printf("Failed to publish message, return code %d\n", rc); rc = EXIT_FAILURE; goto unsubscribe_exit; }
|
|
~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake setup.cmake -DCMAKE_BUILD_TYPE=Release .. |
-DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux- |
sleep(30); //每隔 30 秒 更新一次数据
|
} unsubscribe_exit: if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_unsubscribe(client, LED_TOPIC))) { printf("Failed to unsubscribe, return code %d\n", rc); rc = EXIT_FAILURE; } disconnect_exit: if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_disconnect(client, 10000))) { printf("Failed to disconnect, return code %d\n", rc); rc = EXIT_FAILURE; } destroy_exit: MQTTClient_destroy(&client); exit: return rc; } |
代码笔者就不再进行介绍了,因为该讲的东西前面都已经讲得很清楚了,没有必要再去啰嗦了!代码中的三个主题需要向大家简单地说明一下:
WILL_TOPIC:这是客户端的遗嘱主题。
LED_TOPIC:LED 主题,我们的开发板客户端订阅了该主题,而我们会通过其它客户端,譬如手机或电脑去向这个主题发布信息,那么接收到信息之后根据信息的内容,来对 LED 做出相应的控制,譬如点亮
LED、熄灭 LED。
TEMP_TOPIC:温度主题,我们的开发板客户端会向这个主题发布消息,这个消息的内容就是开发板这个芯片温度值,开发板的温度值怎么获取?对于我们 MX6U 开发板来说,就是读取 /sys/class/thermal/thermal_zone0/temp 属性文件。同样,其它客户端(譬如手机或电脑)会订阅这个温度主题,所以,手机或电脑就会收到开发板的温度信息。程序中是设置每隔 30 秒发一次。
所以,大家一想,其实这东西真的非常简单,人家 MQTT 协议都给你做好了,你就只管用就是了!构建、编译
我们直接进行编译,进入到工程目录下的 build 目录中,执行 cmake 构建:
图 33.7.2 执行 cmake 构建执行 make 编译:
图 33.7.3 make 编译
编译成功之后,在 build/bin 目录下生成了可执行文件 mqttClient。
将可执行文件 mqttClient 拷贝到开发板 Linux 系统/home/root 目录下。
33.8演示
在进行测试之前,确保开发板和 ubuntu 是在同一局域网内,能够互相 ping 通。执行 mqttClient 客户端应用程序,如下所示:
图 33.8.1 开发板上执行 mqttClient 客户端程序
这样,我们的开发板作为 MQTT 客户端就成功连接上了 MQTT 服务器,并且每隔 30 秒向服务器发布温度信息,同样也会接收 LED 主题的信息。
现在我们使用 MQTT.fx 在电脑上进行测试,打开 MQTT.fx 客户端软件,使用我们前面创建的 test1 客户端去连接本地 MQTT 服务器:
图 33.8.2 MQTT.fx 客户端连接 MQTT 服务器
接下来我们订阅温度主题"dt_mqtt/temperature",订阅之后立马就会收到温度信息,并且之后每隔 30 秒会收到开发板客户端发布的温度信息,如下图所示:
图 33.8.3 订阅温度主题、获取开发板温度信息接着我们向 LED 主题“dt_mqtt/led”发布消息去控制开发板上的 LED:
图 33.8.4 向 LED 主题发 0 控制开发板上的 LED 熄灭
图 33.8.5 向 LED 主题发 1 控制开发板上的 LED 亮
图 33.8.6 向 LED 主题发 2 控制开发板上的 LED 为呼吸灯模式
我这里没法给你演示,你自己看效果,有没有成功控制板上的 LED 灯,反正笔者这里是没有问题的。
除了使用电脑之外,我们还可以使用手机控制或查看开发板芯片温度信息,在手机上使用 MQTT Client 软件连接 MQTT 服务器(不要使用开发板和 MQTT.fx 客户端正在使用的 Cliente ID)。连接成功之后订阅温度主题查看开发板温度信息、向 LED 主题发布信息控制开发板上的 LED 灯,如下所示:
注意:此方式测试需要开发板和电脑连接路由器,手机连接路由器的 wifi,并且三者都在同一个网段下,能够互相ping通。其他网络连接方式请自行测试。
图 33.8.7 手机端订阅温度主题
图 33.8.8 手机端控制 LED 灯本章我们的内容就结束了。
https://gitee.com/powes/,作者:前沿风暴,转载请注明原文链接:https://www.cnblogs.com/Kreos/p/19717063
浙公网安备 33010602011771号