Linux MQTT 物联网通信

2023-11-05

目录

MQTT 报文

C语言代码封装MQTT协议报文,了解MQTT协议通信过程
MQTT协议图解,一文看懂MQTT协议数据包(真实报文数据解析解释)
MQTT与TCP通信协议的对比:MQTT报文打包–>TCP 数据包->IP数据包
详细介绍报文结构以及实例展示:https://blog.csdn.net/mark_md/category_10322631.html

MQTT 简介

目前物联网的通讯协议并没有一个统一的标准,比较常见的有MQTT、CoAP、DDS、XMPP 等,在这其中,MQTT(消息队列遥测传输协议)应该是应用最广泛的标准之一。目前,MQTT 已逐渐成为IoT 领域最热门的协议,阿里云IoT 物联网平台使用的就是MQTT协议。

MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,主要的几项特性:

  • ①、使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
  • ②、基于TCP/IP 提供网络连接。主流的MQTT 是基于TCP 连接进行数据推送的,但是同样也有基于UDP 的版本,叫做MQTT-SN。
  • ③、支持QoS 服务质量等级。根据消息的重要性不同设置不同的服务质量等级。
  • ④、小型传输,开销很小,协议交换最小化,以降低网络流量。
    嵌入式设备的运算能力和带宽都相对薄弱,在手机移动应用方面,MQTT 是一种不错的Android 消息推送方案。
  • ⑤、使用will遗嘱机制来通知客户端异常断线。
  • ⑥、基于主题发布/订阅消息,对负载内容屏蔽的消息传输。
  • ⑦、支持心跳机制。

MQTT 协议(上)

MQTT 通信基本原理

MQTT 是一种基于客户端-服务端架构的消息传输协议

服务端
MQTT 服务端通常是一台服务器(broker),它是MQTT 信息传输的枢纽,负责将MQTT 客户端发送来的信息传递给MQTT 客户端;MQTT 服务端还负责管理MQTT 客户端,以确保客户端之间的通讯顺畅,保证MQTT 信息得以正确接收和准确投递。

客户端
MQTT 客户端可以向服务端发布信息,也可以从服务端收取信息;我们把客户端发送信息的行为称为“发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作很像我们在使用微信时“关注”了某个公众号,当公众号的作者发布新的文章时,微信官方会向关注了该公众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看。

MQTT 主题

上面我们讲到了,客户端想要从服务器获取信息,首先需要订阅信息,那客户端如何订阅信息呢?这里我们要引入“主题(Topic)”的概念,“主题”在MQTT 通信中是一个非常重要的概念,客户端发布信息以及订阅信息都是围绕“主题”来进行的,并且MQTT 服务端在管理MQTT 信息时,也是使用“主题”来控制的。

客户端发布消息时需要为消息指定一个“主题”,表示将消息发布到该主题;而对于订阅消息的客户端来说,可通过订阅“主题”来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)。

为了便于您更好理解服务端是如何通过“主题”来控制客户端之间的信息通讯,我们来看看下图实例:
在这里插入图片描述
在以上图示中一共有三个MQTT 客户端,它们分别是开发板、手机和电脑。MQTT 服务端在管理MQTT通信时使用了“主题”来对信息进行管理。比如上图所示,假设我们需要利用手机和电脑获取开发板在运行过程中SoC 芯片的温度,那么首先电脑和手机这两个客户端需要向MQTT 服务器订阅主题“芯片温度”;接下来,当开发板客户端向服务端的“芯片温度”主题发布信息(假设信息的内容就是当前的温度值)后,服务端就会首先检查都有哪些客户端订阅了“芯片温度”这一主题的信息,而当它发现订阅了该主题的客户端有一个手机和一个电脑,于是服务端就会将刚刚收到的“芯片温度”信息转发给订阅了该主题的手机和电脑客户端。

通过以上的这种实例,手机和电脑便可以获取到开发板运行时SoC 芯片的温度值。开发板是“芯片温度”主题的发布者,而手机和电脑则是该主题的订阅者。

值得注意的是,MQTT 客户端在通信时,角色往往不是单一的,一个客户端既可以作为信息发布者也可以同时作为信息订阅者。如下图所示:

在这里插入图片描述
上图中的所有客户端都是围绕“LED 控制”这一主题进行通信。此时,对于“LED 控制”这一主题来说,手机和电脑客户端成为了MQTT 信息的发布者而开发板则成为了MQTT 信息的订阅者(接收者)。

所以由此可知,针对不同的主题,MQTT 客户端可以切换自己的角色,它们可能对主题A 来说是信息发布者,但是对于主题B 就成了信息订阅者。

MQTT 发布/订阅特性

MQTT 发布/订阅的特性:客户端相互独立、空间上可分离、时间上可异步,具体介绍如下:

⚫ 客户端相互独立:MQTT 客户端是一个个独立的个体,它们无需了解彼此的存在,依然可以实现信息交流。譬如在上面的实例中,开发板客户端在发布“芯片温度”信息时,开发板客户端本身完全不知道有多少个MQTT 客户端订阅了“芯片温度”这一主题;而订阅了“芯片温度”主题的手机和电脑客户端也完全不知道彼此的存在,大家只要订阅了“芯片温度”这一主题,MQTT 服务端就会在每次收到新信息时,将信息发送给订阅了“芯片温度”主题的客户端。
⚫ 空间上分离:空间上分离相对容易理解,MQTT 客户端以及MQTT 服务端它们在通信时是处于同一个通信网络中的,这个网络可以是互联网或者局域网;只要客户端联网,无论他们远在天边还是近在眼前,都可以实现彼此间的通讯交流;其实网络通信本就是如此,所以并不是MQTT 通信所特有的。
⚫ 时间上可异步:MQTT 客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要,前面我们也介绍了,MQTT 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的,高延迟和不可靠网络必然就会导致时间上的异步;物联网设备在运行过程中发生意外掉线是非常正常的情况,我们使用上面的实例二的场景来作说明,当开发板在运行过程中,可能会由于突然断电(假设开发板是通过电源适配器供电的)导致掉线,这时开发板会断开与MQTT 服务端的连接。假设此时我们的手机客户端向开发板客户端所订阅的“LED 控制”主题发布了信息,而开发板恰恰不在线,这时,MQTT 服务端可以将“LED 控制”主题的新信息保存,待开发板客户端再次上线后,服务端再将“LED 控制”信息推送给开发板。所以这就必然导致了,手机发送信息与开发板接收信息在时间上是异步的。

连接MQTT 服务端

MQTT 客户端之间想要实现通信,必须要通过MQTT 服务端,都必须先连接到服务端。

MQTT 客户端连接服务端的两个步骤

①、首先客户端需要向服务端发送连接请求,这个连接请求实际上就是向服务端发送一个CONNECT报文,也就是发送了一个CONNECT 数据包。
在这里插入图片描述
②、MQTT 服务端收到连接请求后,会向客户端发送连接确认。连接确认实际上是向客户端发送一个CONNACK 报文,也就是CONNACK 数据包。

在这里插入图片描述

CONNECT 请求报文

CONNECT 报文包含的信息如下图所示:

在这里插入图片描述
所谓报文就是一个数据包,MQTT 报文组成分为三个部分:固定头(Fixed header)、可变头(Variableheader)以及有效载荷(Payload,消息体)。这里我们简单地介绍一下:

  • 固定头(Fixed header):存在于所有MQTT 报文中,固定头中有报文类型标识,可用于识别是哪种MQTT 报文,譬如该报文是CONNECT 报文还是CONNACK 报文,亦或是其它类型报文。
  • 可变头(Variable header):存在于部分类型的MQTT 报文中,报文的类型决定了可变头是否存在及其具体的内容。
  • 消息体(Payload):存在于部分类型的MQTT 报文中,payload 就是消息载体的意思。

CONNECT 报文包含了很多的信息,左边的是信息的名称(变量名),右边则是信息的具体内容(变量的值),右边这些具体内容只是一个示例。

有些信息名称旁边标注了“可选”字样,而有些则没有。
没有标注“可选”字样的信息是必须包含在CONNECT 报文中的。

先介绍一下未标注“可选”字样的信息。

clientId–客户端id
clientId 是MQTT 客户端的标识,也就是MQTT 客户端的名字,MQTT 服务端可通过clientId 来区分不同的客户端,MQTT 服务端用该标识来识别客户端。因此clientId 必须是独立的,如果两个MQTT 客户端使用相同clientId 标识,服务端会把它们当成同一个客户端来处理。通常clientId 是由一串字符所构成的,譬如,在上面的示例中,clientId 是“client-id”。

keepAlive–心跳时间间隔
对于MQTT 服务器来说,它要判断一台MQTT 客户端是否依然与它保持着连接状态,可以检查这台客户端是不是经常发送消息给服务端,如果服务端经常收到客户端的消息,那么没问题,这个客户端肯定在线。

但是有些客户端并不经常发送消息给服务端,对于这种客户端,MQTT 协议使用了类似心跳检测的方法来判断客户端是否在线。前面在介绍MQTT 时,曾提到过MQTT 支持心跳机制,心跳机制其实就是用来判断客户端是否与服务端保持着连接的一种方法,也就是说通过心跳机制来检测客户端是否在线。客户端在没有向服务端发送信息时(空闲时),可以定时向服务端发送一个心跳数据包,这个心跳包也被称作心跳请求,心跳请求的作用正是用于告知服务端,当前客户端依然在线,服务端在收到客户端的心跳请求后,会回复一条消息,这条回复消息被称作心跳响应。

譬如keepAlive=60,表示告诉服务端,客户端将会每隔60 秒左右向服务端发送心跳包。

cleanSession–清除会话

这是一个布尔值,cleanSession 标志可用于控制客户端与服务端在连接和断开连接时的行为。在离线期间你的QQ 好友给你发了几条信息;由于当前你的QQ 处于离线状态,自然是接收不到好友发送过来的信息,但是,当你的QQ 恢复连接状态时,立马会接收到好友在离线期间所发给你的信息。

而cleanSession 就与这个有关系,它是一个布尔值,如果连接服务端时cleanSession=0,当MQTT 客户端由离线(与服务端断开连接)再次上线时,离线期间发给客户端的所有QoS>0 的消息仍然可以接收到;如果连接服务端时cleanSession=1,当MQTT 客户端由离线(与服务端断开连接)再次上线时,离线期间发给客户端的所有消息一律接收不到。

下面再看看MQTT 服务端接收到客户端发来的连接请求后所回复的CONNACK 报文详细内容。

CONNACK 回复报文

CONNACK 报文包含的信息如下图所示:

在这里插入图片描述

returnCode–连接返回码

当服务端收到了客户端的连接请求后,会向客户端发送returnCode(连接返回码),用来说明连接情况。见下表:

返回码 说明
0 连接成功
1 连接被服务端拒绝,原因是不支持客户端的MQTT 协议版本
2 连接被服务端拒绝,原因是不支持客户端标识符的编码。可能造成此原因的是客户端标识符编码是UTF-8,但是服务端不允许使用此编码。
3 连接被服务端拒绝,原因是服务端不可用。即,网络连接已经建立,但MQTT服务不可用。
4 连接被服务端拒绝,原因是用户名或密码无效。
5 连接被服务端拒绝,原因是客户端未被授权连接到此服务端。
6~255 保留备用

sessionPresent

sessionPresent 与CONNECT 报文中的cleanSession 有关系。

在cleanSession=0 的情况下,当客户端连接到服务器之后,可通过CONNACK 报文中返回的sessionPresent 来查询服务端是否为客户端保存了会话状态(客户端上一次连接时的会话状态信息),如果服务端已为客户端保存了上一次连接时的会话状态,则sessionPresent=1,如果没有保存会话状态,则sessionPresent=0。

如果cleanSession=1,在这种情况下,客户端是不需要服务端保存会话状态的,那么服务端发送的确认连接CONNACK 报文中,sessionPresent 肯定是false(sessionPresent=0)。

Tips:关于MQTT 协议的参考资料,链接地址如下:
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718027

断开连接

当MQTT 客户端连接到服务端之后,客户端可以主动向服务端发送一个DISCONNECT 报文来断开与服务端的连接,如下图所示:

在这里插入图片描述

发布消息、订阅主题与取消订阅主题

PUBLISH–发布消息报文

当客户端连接到服务端之后,就可以向服务端发布消息了,每条发布的消息必须指定一个“主题”,表示向某主题发布消息;MQTT 服务端可以通过主题来确定将消息转发给哪些客户端(订阅了该主题的客户端)。

MQTT 客户端向服务端发布消息其实就是向服务端发送一个PUBLISH 报文,服务端收到客户端发送过来的PUBLISH 报文之后,会向发送发回复一个报文。

下图是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–订阅主题报文

客户端要想订阅主题,首先要向服务端发送主题订阅请求。客户端是通过向服务端发送SUBSCRIBE 报文来实现这一请求的。该报文包含有一系列“订阅主题名”。一个SUBSCRIBE 报文可以包含有单个或者多个订阅主题名。

另外每一个SUBSCRIBE 报文还包含有“报文标识符”。报文标识符可用于对MQTT 报文进行标识。不同的MQTT 报文所拥有的标识符不同。MQTT 设备可以通过该标识符对MQTT 报文进行甄别和管理。

当客户端向服务端发送SUBSCRIBE 报文,服务端接收到SUBSCRIBE 报文之后会向客户端回复一个SUBACK 报文(订阅确认报文),如下图所示:
在这里插入图片描述

服务端接收到客户端的订阅报文后,会向客户端发送SUBACK 报文确认订阅。

SUBACK 报文包含有“订阅返回码”和“报文标识符”这两个信息。

returnCode–订阅返回码

客户端向服务端发送订阅请求后,服务端会给客户端返回一个订阅返回码。

客户端可通过一个SUBSCRIBE 报文发送多个主题的订阅请求。服务端会针对SUBSCRIBE 报文中的所有订阅主题来逐一回复给客户端一个返回码。

这个返回码的作用是告知客户端是否成功订阅了主题。以下是返回码的详细说明。

项目 Value
0x00 订阅成功–QoS0
0x01 订阅成功–QoS1
0x02 订阅成功–QoS2
0x80 订阅失败

由上表可知,当returnCode=0、1 或2 这三种情况时,都表示订阅成功;具体返回的数字是多少,根据订阅主题时QoS 的不同,服务端的返回码也会有所不同。

UNSUBSCRIBE–取消订阅主题报文

客户端可通过向服务端发送一个UNSUBSCRIBE 报文来取消订阅主题,当服务端接收到UNSUBSCRIBE报文后,会向发送发回复一个UNSUBACK 报文(取消订阅确认报文),如下图所示:
在这里插入图片描述

UNSUBSCRIBE 报文包含两个重要信息,第一个是取消订阅的主题名称,同一个UNSUBSCRIBE 报文可以同时包含多个取消订阅的主题名称。另外,UNSUBSCRIBE 报文也包含“报文标识符”,MQTT 设备可以通过该标识符对报文进行管理。

当服务端接收到UNSUBSCRIBE 报文后,会向客户端发送取消订阅确认报文–UNSUBACK 报文。该报文含有客户端所发送的“取消订阅报文标识符”。

客户端接收到UNSUBACK 报文后就可以确认取消主题订阅已经成功完成了。

主题的格式

1、主题的基本形式
主题的基本形式就是一个字符串,譬如:“myTopic”、“currentTemp”、“LEDControl"等,虽然看起来简单,但是有几个点需要大家注意一下:
⚫ 主题是区分大小写的。所以"LEDControl"和"ledControl"是两个不同的主题。
⚫ 主题可以使用空格。譬如"LED Control”,虽然主题允许使用空格,但是笔者建议大家尽量不要使用空格。
⚫ 不要使用中文主题。虽然有些MQTT 服务器支持中文主题,但是绝大部分MQTT 服务器是不支持中文主题的,所以大家不要使用中文主题,而是使用ASCII 字符来作为MQTT 主题。
⚫ 以 $ 号开头的主题是MQTT 服务端系统保留的特殊主题,客户端不可随意订阅或向其发布信息,譬如:

"$SYS/monitor/Clients"
"$SYS/monitor/+"
"$SYS/#"

⚫ 尽量不要使用“/”作为主题的开头,这样做没有什么意义,而且额外产生一个没有用处的主题级别。

主题中尽量使用ASCII 字符
虽然有些MQTT 设备支持UTF-8 字符作为MQTT 主题,但是笔者建议您在主题中尽量使用ASCII 字符。

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"
......

这就是多级通配符的概念。

MQTT 初体验

初次体验,我们将使用电脑作为客户端,而服务端我们将使用公用MQTT 服务器。

安装MQTT.fx 客户端软件

想要将电脑作为MQTT 客户端,我们需要在电脑上安装一个MQTT 客户端软件,推荐MQTT.fx 这款软件,官网是http://mqttfx.jensd.de/。

在这里插入图片描述
以1.7.1 版本为例,进入到http://www.jensd.de/apps/mqttfx/1.7.1/链接地址进行下载,如下:
在这里插入图片描述
根据自己的需求下载即可!笔者使用的是Windows 64 位系统,所以笔者下载的是最后一个,下载完成之后得到一个exe 安装包文件:
在这里插入图片描述
直接双击即可安装,打开软件之后界面如下所示:

在这里插入图片描述

MQTT 服务端

自己搭建MQTT 服务器

本章我们不使用自己搭建的服务器,因为如果你没有公网IP 的主机,搭建的服务器也只能在局域网内使用,外部网络无法接入。

作为学习,笔者还是给大家简单地提一下关于自己如何去搭建MQTT 的服务器。MQTT 服务器非常多,如apache 的ActiveMQ、emtqqd、HiveMQ、Emitter、Mosquitto、Moquette 等等,既有开源的服务器也有商业服务器;作为学习,我们推荐Mosquitto,Mosquitto 是一个高质量轻量级的开源MQTT Broker,支持MQTTv3.1 和MQTTv3.1.1 协议,也是目前主流的开源MQTT Broker,它的官方地址是https://mosquitto.org/。

在这里插入图片描述

公用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 客户端软件MQTT.fx:
在这里插入图片描述
打开之后如上图所示,点击上图中红框标注的齿轮按钮打开配置界面:

在这里插入图片描述
首先点击1 所示的“+”号创建一个新的配置,然后填写好各个配置参数,笔者使用的然也物联提供的公用MQTT 服务器,服务器地址test.ranye-iot.net,对应的端口号为1883;clientId、keepAlive、cleanSession(勾选Clean Session 表示cleanSession=1)。

配置完成之后,点击3 所示的Apply 按钮应用配置,最后点击右上角“X”关闭窗口:

在这里插入图片描述
连接成功之后如下所示:

在这里插入图片描述

订阅主题
我们先订阅一个主题,如下图所示:
在这里插入图片描述
首先点击1 所示的“Subscribe”切换到主题订阅界面,接着填写需要订阅的主题名称,示例中我们使用了分级主题“dt2914/testTopic”,最后点击3 所示的“Subscribe”按钮向服务器发出订阅请求。订阅成功之后如下图所示:
在这里插入图片描述
左边会列举出当前客户端所订阅的主题,右边显示消息列表。

发布消息
上例中我们的电脑客户端已经成功订阅了主题“dt2914/testTopic”,接下来我们尝试向该主题发布消息。如下图所示:

在这里插入图片描述
首先点击1 所示的“Publish”字样切换到发布消息界面,在2 所示处填写主题名称,这里笔者需要向“dt2914/testTopic”主题发布消息;在3 所示空白处填写需要发布的内容,然后点击4 所示的“Publish”按钮发布消息。

此时我们的电脑客户端便会接收到自己发布的消息,如下所示:

在这里插入图片描述

取消订阅主题

取消订阅主题非常简单,如下图所示:

在这里插入图片描述

使用手机作为客户端

手机也可以作为MQTT 客户端,同样也需要安装一个客户端软件MQTTool 工具。

在这里插入图片描述

打开该软件然后进行配置、连接MQTT 服务器,如下所示:

在这里插入图片描述
连接成功之后如下所示:

在这里插入图片描述
然后点击下边的“Subscribe”按钮可订阅主题、点击“Publish”按钮可发布消息。

现在我们进行一个测试,在前面的示例中,我们的电脑客户端订阅了“dt2914/testTopic”主题,现在我们要使用手机客户端向“dt2914/testTopic”主题发布消息,如下所示:

在这里插入图片描述
点击“Publish”按钮发布消息,接着电脑客户端便会接收到手机发布消息,如下所示:

在这里插入图片描述

然也物联提供的公用MQTT 服务器,在测试过程有几个问题需要注意下:

在这里插入图片描述

MQTT 协议(下)

QoS 等级

QoS 是Quality of Service 的缩写,所以中文名便是服务质量。MQTT 协议有三种服务质量等级:

⚫ QoS = 0:最多发一次;
⚫ QoS = 1:最少发一次;
⚫ QoS = 2:保证收一次。

QoS = 0:最多发一次

0 是服务质量QoS 的最低级别。当QoS 为0 级时,MQTT 协议并不保证所有信息都能得以传输。

也就是说发送一次之后就不管了,最多一次,不管发送是否失败!发送端一旦发送完消息后,就完成任务了,发送端不会检查发出的消息能否被正确接收到。

QoS = 1:最少发一次

当QoS 级别为1 时,发送端在消息发送完成后,会检查接收端是否已经成功接收到了消息,如下图所示:

在这里插入图片描述

发送端向接收端发送PUBLISH 报文,当接收端收到PUBLISH 报文后会向发送端回复一个PUBACK 报文,如果发送端收到PUBACK 报文,那么它就知道消息已经被接收端成功接收!

假如过了一段时间后,发送端没有收到PUBACK 报文,那么发送端会再次发送消息(发送PUBLISH报文),然后再次等待接收端的PUBACK 确认报文。

当发送端重复发送一条消息时,会将PUBLISH 报文中的dup 标志设置为true,如图33.2.8 中所示。这就是为了告诉接收端,此消息为重复发送的消息,那么我们的MQTT 客户端在接收到消息之后,可以去判断dup 标志以确定此消息是否为重复消息,应用程序应该对此作出相应的处理。

注意:Qos=1 时,MQTT 服务器是不会进行去重的。

QoS = 2:保证收一次

MQTT 服务质量最高级是2 级,即QoS=2。当MQTT 服务质量为2 级时,MQTT 协议可以确保接收端只接收一次消息(注意是只接收到一次,在QoS=1 的情况下,接收端接收到消息的次数可能不止一次:>=1)。

为了确保接收端只接收到一次消息,PUBLISH 报文的收发过程相对更加复杂。发送端需要接收端进行两次消息确认,因此,2 级MQTT 服务质量是最安全的服务级别,也是最慢的服务级别。我们来看看整体的过程:
在这里插入图片描述
从上到下,按照1、2、3、4 的顺序进行:
①、首先发送端向接收端发送PUBLISH 报文;
②、接收端接收到PUBLISH 报文后,向发送端回复一个PUBREC 报文(官方称其为–发布收到);
③、发送端接收到PUBREC 报文后,会再次向接收端发送PUBREL 报文(官方称其为–发布释放);
④、接收端接收到PUBREL 报文后,会再次向发送端回复一个PUBCOMP 报文(官方称其为–发布完成),如果发送端接收到PUBCOMP 报文表示消息传输成功,它确认接收端已经成功接收到消息,整个过程结束。

如何设置QoS?

在这里插入图片描述
点击选中QoS0 时表示发布消息时将QoS 等级设置为最低级0。
点击选中QoS1 时表示发布消息时将QoS 等级设置为1。
点击选中QoS2 时表示发布消息时将QoS 等级设置为最高级2。

在这里插入图片描述
点击选中QoS0 时表示订阅主题时将QoS 等级设置为最低级0。
点击选中QoS1 时表示订阅主题时将QoS 等级设置为1。
点击选中QoS2 时表示订阅主题时将QoS 等级设置为最高级2。

要想实现QoS>0 的MQTT 通信,客户端在连接服务端时务必要将cleanSession 设置为false。如果cleanSession 设置为true,则意味着客户端不会接收到任何离线消息。

服务质量降级

假如客户端在发布消息和订阅主题时使用不同级别的QoS,如何来应对这一情况呢?

在这里插入图片描述

在这种情况下,服务端会使用较低级别QoS 来提供服务。如上图所示,虽然A 发送到主题1 的消息采用QoS 为2,但是服务端发送主题1 的消息给B 时,采用的QoS 为1。这是因为B 在订阅主题1 时采用的QoS 为1。

总之,对于发布和订阅消息的客户端,服务端会主动采用较低级别的QoS 来实现消息传输。

保留消息

前面我们提到,PUBLISH 报文中有一个retain 标志,也就是保留标志,是一个布尔值。

作用就是让服务端对客户端发布的消息进行保留,如果有其它客户端订阅了该消息对应的主题时,服务端会立即将保留消息推送给订阅者,而不必等到发送者向主题发布新消息时订阅者才会收到消息。

更新保留消息

每一个主题只能有一个“保留消息”,如果客户端想要更新“保留消息”,就需要向该主题发送一条新的“保留消息”,这样服务端会将新的“保留消息”覆盖旧的“保留消息”。

删除保留消息

只需向该主题发布一条空的“保留消息”即可。

使用MQTT.fx 客户端进行测试

打开MQTT.fx 客户端软件,连接好服务器,先向某个主题发布消息,譬如“dt2914/testTopic”主题:

在这里插入图片描述
上图中右边的“Retained”按钮也就是保留标志,当点击选中时表示使能“保留消息”这一功能,如果未选中,则表示禁用“保留消息”这一功能,也就是消息不保留。我们先不保留消息,填写好需要发送的内容之后,点击“Publish”按钮发布消息。

现在我们订阅“dt2914/testTopic”主题:

在这里插入图片描述
订阅之后我们的客户端并没有接收到前面发布的消息,因为发布在前、订阅在后,必须要等到发布者下一次向该主题发布新消息时才会收到。

现在取消订阅主题“dt2914/testTopic”,然后再向主题“dt2914/testTopic”发布消息,此时我们使能“保留消息”这一功能,如下:

在这里插入图片描述
点击Publish 按钮发布消息,之后我们再重新订阅主题“dt2914/testTopic”,如下:

在这里插入图片描述
当订阅之后我们的客户端立马就收到了一条消息,并且还标识了这条消息是“保留消息”。

MQTT 的心跳机制

心跳机制的原理在于:让客户端在没有向服务端发送消息的这个空闲时间里,定时向服务端发送一个心跳包,这个心跳包被称为心跳请求,其实质就是向服务端发送一个PINGREQ 报文;当服务端收到PINGREQ 报文后就知道该客户端依然在线,然后向客户端回复一个PINGRESP 报文,称为心跳响应,如下图所示:

在这里插入图片描述
由于心跳请求是定时发送的(通过keepAlive 设置时间间隔,也是告诉服务端,客户端将会多少多少秒向它发送心跳请求,这样服务端就会知道了);一旦服务器未收到客户端的心跳包,那么服务器就会知道,这台客户端可能已经掉线了。

这个心跳机制不仅可以用于服务端判断客户端是否在线,客户端也可使用心跳机制来判断自己与服务端是否保持连接。如果客户端在发送心跳请求(PINGREQ)后,没有收到服务端的心跳响应(PINGRESP),那么客户端就会认为自己与服务端已经断开连接了。

MQTT 的遗嘱机制

客户端断开与服务端的连接通常是有两种方式的:

⚫ 客户端主动向服务端发送DISCONNECT 报文,请求断开连接,自然服务端也就知道了客户端要离线了;
⚫ 客户端意外掉线。被动与服务端断开了连接。

MQTT 协议允许客户端在“活着”的时候就写好遗嘱,这样一旦客户端意外断线,服务端就可以将客户端的遗嘱公之于众

客户端如何设置自己的“遗嘱”信息

客户端连接服务端时发送的CONNECT 报文中有这样几个参数,如下图红框中所示:

在这里插入图片描述

这几个参数都是以will 开头的,will 其实就是“遗嘱”的英文单词,下面分别介绍一下:

willTopic – 遗嘱主题
遗嘱消息和普通MQTT 消息很相似,也有主题和正文内容。willTopic 的作用正是告知服务端,本客户端的遗嘱主题是什么。只有那些订阅了这一遗嘱主题的客户端才会收到本客户端的遗嘱消息。

willMessage – 遗嘱消息
遗嘱消息定义了遗嘱的内容。在本示例中,那些订阅了主题“clientWill”的客户端会在客户端意外断线时,收到服务端发布的“client offline”这样的信息。
willRetain – 遗嘱消息的保留标志
遗嘱消息也可以设置为保留标志,用于告诉服务端是否需要对遗嘱消息进行保留处理。
willQoS – 遗嘱消息的QoS
对于遗嘱消息来说,同样可以使用服务质量来控制遗嘱消息的传递和接收。这里的服务质量与普通
MQTT 消息的服务质量是一样的概念。也可以设置为0、1、2。对于不同的服务质量级别,服务端会使用不同的服务质量来发布遗嘱消息。
MQTT.fx 如何设置客户端的“遗嘱”
MQTT.fx 软件如何为电脑客户端设置遗嘱呢?首先进入到配置页面中,如下所示:

在这里插入图片描述

MQTT 用户密码认证

在这里插入图片描述
username(用户名)和password(密码),这里的用户名和密码是客户端连接服务端时进行认证所需要的。

有些MQTT 服务端需要客户端在连接时提供用户名和密码,只有客户端正确提供了用户名和密码后,才能连接服务端,否则服务端将会拒绝客户端连接,那么客户端也就无法发布和订阅消息了。

接下来我们使用MQTT.fx 来测试一下,笔者使用的是然也物联提供的公共版MQTT 服务器,这个服务器提供了一个测试用的用户名和密码,用户名是test-user、密码是ranye-iot,那我们使用这个用户名和密码连接服务端试试:

在这里插入图片描述
填写完用户名、密码之后点击右下角“Apply”按钮应用,然后关闭配置窗口。然后点击Connect 按钮连接服务端:

在这里插入图片描述
点击Connect 按钮连接服务器,就会成功连接上。在手机上使用MQTTool 工具也可以,大家自己去试试。

用户名和密码除了用于在连接服务端时进行认证、校验这一功能外,有些MQTT 服务端也利用此信息来识别客户端属于哪一个用户,从而对客户端进行管理。

申请社区版MQTT 服务

在前面的示例中,我们都是使用的公共版MQTT 服务器进行了测试,只要输入了正确的服务器地址就可以连接,大家都可以对相同的主题发布消息、订阅该主题,导致我们发布的信息谁都能看到。

然也物联平台也提供了免费的社区版MQTT 服务,社区版MQTT 服务是面向个人用户的免费MQTT 服务。与公共版MQTT 服务不同的是,社区版MQTT 服务中,用户个人主题和信息传输受到用户名和密码保护。即,A 用户的个人主题只有A 用户可以发布和订阅,其他用户不能对该主题进行订阅和发布,这样会使得安全性得到提升。

当客户端连接社区版MQTT 服务端时,需要提供正确的用户名和密码,服务端会对此进行验证,如果没有提供正确的用户名、密码信息,则服务器将拒绝为用户提供MQTT 服务,也就是拒绝客户端连接。所以,我们个人用户可使用然也物联的社区版MQTT 服务来搭建自己的私人物联网项目,注意仅限于个人用户使用,不可商用!

那接下来,笔者将向大家介绍如何通过然也物联平台申请社区版MQTT 服务。

首先进入到然也物联的官方网站:http://www.ranye-iot.net/
在这里插入图片描述

点击上边的“注册用户”注册一个用户:

在这里插入图片描述

大家根据指示填写信息,完成用户注册。
注册完成之后登陆然也物联平台,登陆成功之后如下所示:
在这里插入图片描述

左上角会出现一个“平台申请”链接,点击“平台申请”链接即可申请然也物联的社区版MQTT 服务,如下所示:

在这里插入图片描述
在这里插入图片描述
同样,大家根据指示说明填写好信息。在填写信息之前,仔细阅读相关说明以及相应的要求,最后有三个题目需要大家填写正确,只有正确回答所有问题最终才会显示“提交申请”这个按钮。这样做为了防止申请服务的用户是真正需要使用MQTT 服务的用户、而不是随随便便的一个用户。

当我们提交申请之后,页面会出现一个提示信息,这个大家要认真看一下,提示中说到:然也物联官方工作人员会在未来几天之内添加你申请时留下的微信号,以人工的形式进一步完成申请审核。所以大家要留意下自己的微信,未来几天内会不会有然也物联官方工作人员添加你为好友,到时你要同意一下。

当我们的申请通过之后,官方工作人员会通过微信通知我们,告诉我们申请已经成功了!接着工作人员会将相关的使用注意事项、使用方法通过微信发送给您,大家要认真阅读尤其是注意事项;如果违反了它的规定,将会停止您使用社区版服务。

除了注意事项之外,还包括对于我们使用社区版MQTT 服务非常重要的信息,譬如社区版MQTT 服务器的地址(iot.ranye-iot.net)、端口(1883)以及然也物联给我们提供的客户端连接认证信息。

然也物联给每一个申请的用户提供了8 组客户端连接认证信息,也就是8 组用户名、密码、clientId,也就是说允许我们同时使用8 台客户端设备连接服务端;每一台客户端设备连接服务端时使用其中一组用户名、密码、clientId 信息,只有用户名、密码、clientId 匹配、服务端的认证才会通过、才可成功连接到服务端。

社区版MQTT 服务器为用户提供了个人专属主题级别,只有用户自己的客户端设备才可以使用自己的个人专属主题级别,譬如向个人专属主题发布消息、订阅个人专属主题;而其它用户是无法向您的个人专属主题发布消息、也不能订阅您的个人专属主题,因为社区版MQTT 服务中个人专属主题级别受到了用户名、密码保护,同样您也不能向其他用户的个人专属主题发布消息以及订阅其他用户的个人专属主题;这样使得我们的MQTT 物联网通信安全性大大提升!

在后续我们会自己编写一个MQTT 客户端程序,在我们的开发板上运行,将开发板作为MQTT 客户端去连接然也物联社区版MQTT 服务器。

移植MQTT客户端库

前面的示例中,我们使用MQTT.fx 客户端软件在自己的电脑上进行了测试,如果需要在开发板上进行测试,将开发板作为MQTT 客户端,我们需要自己去编写客户端程序。

首先在编写客户端程序之前,需要移植MQTT 客户端库到我们的开发板上。

下载MQTT客户端库源码
如何下载MQTT 客户端库源码包?首先我们进入到MQTT 的官网地址:https://mqtt.org/
在这里插入图片描述

点击“Software”链接地址,找到“Client libraries”项,如下所示:

在这里插入图片描述
MQTT 客户端库支持多种不同的编程语言,譬如C、C++、Go、Java、Lua、Objective-C、Python 等,对于我们来说,我们使用的是C 语言开发,所以要选择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”链接地址,如下:
在这里插入图片描述
在这个页面中会有一些简单地介绍信息,大家可以自己看一看。我们往下看,找到它的下载地址:

在这里插入图片描述
在这里插入图片描述
点击右边的“Release”找到它的发布版本,如下所示:

在这里插入图片描述
目前最新的版本是1.3.9,我们不使用最新版本,建议大家使用1.3.8 版本,如上图所示,点击“Source code (tar.gz)”链接地址下载客户端库源码。
下载成功之后会得到如下压缩文件:
在这里插入图片描述

交叉编译MQTT C 客户端库源码

将paho.mqtt.c-1.3.8.tar.gz 压缩文件拷贝到Ubuntu 系统某个目录下,如下所示:
在这里插入图片描述
接着将其解压到当前目录,如下所示:

在这里插入图片描述
解压成功之后会得到paho.mqtt.c-1.3.8 文件夹,这就是paho MQTT C 客户端库源码工程,进入到该目录下,可以看到工程顶级目录下有一个CMakeLists.txt 文件,所以可知这是一个由cmake 构建的工程。
首先我们要新建一个交叉编译配置文件arm-linux-setup.cmake,进入到cmake 目录下,新建arm-linux-setup.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 的目录,如下所示:
在这里插入图片描述
进入到build 目录下,执行cmake 进行构建:

~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/home/dt/tools/paho.mqtt.c-1.3.8/install -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DPAHO_WITH_SSL=TRUE -DPAHO_BUILD_SAMPLES=TRUE ..

在这里插入图片描述
~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake 这是第三十二章时笔者下载的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 编译:
在这里插入图片描述
这里需要给大家简单地说明一下,事实上,MQTT 客户端库依赖于openssl 库,所以通常在移植MQTT客户端库的时候,需要先移植openssl、交叉编译openssl 得到库文件以及头文件,然后再来编译MQTT 客户端库;但我们这里没有去移植openssl,原因是,我们的开发板出厂系统中已经移植好了openssl 库,并且我们所使用的交叉编译器在编译工程源码的时候会链接openssl 库(sysroot 路径指定的)。

编译成功之后,执行make install 进行安装:
在这里插入图片描述
对安装目录下的文件夹进行简单介绍
进入到安装目录下:

在这里插入图片描述
在安装目录下有bin、include、lib 以及share 这4 个文件夹,bin 目录下包含了一些简单的测试demo,lib 目录下包含了我们编译出来的库文件,如下所示:
在这里插入图片描述
一共有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 目录下

在这里插入图片描述

在我们的MQTT 客户端应用程序中只需要包含MQTTAsync.h 或MQTTClient.h 头文件即可,其它那些头文件会被这两个头文件所包含;

MQTTAsync.h 是异步模式客户端库对外的头文件,而MQTTClient.h 则是同步模式客户端库对外的头文件。因为后续我们将使用同步模式,所以到时在我们的应用程序中需要包含MQTTClient.h 头文件。

拷贝库文件到开发板

将编译得到的库文件拷贝到开发板Linux 系统/usr/lib 目录下,注意不要破坏原有的链接关系,建议在操作之前,先将库文件进行打包,如下所示:

在这里插入图片描述

将压缩包文件libmqtt.tar.gz 拷贝到开发板Linux 系统/home/root 目录下,然后将其解压到/usr/lib 目录:

tar -xzf libmqtt.tar.gz -C /usr/lib

在这里插入图片描述

MQTT 客户端库API 介绍

本小节我们所介绍的这些库函数都是定义在MQTTClient.h 头文件中,也就是同步模式客户端库API,所以在我们的应用程序中需要包含头文件MQTTClient.h。

Tips:MQTT 客户端源码顶级目录docs/MQTTClient/html/index.html 文档向用户介绍了API 的使用方法,并且提供了相应示例代码供用户参考。
在这里插入图片描述

MQTTClient_message结构体

MQTT 客户端应用程序发布消息和接收消息都是围绕着这个结构体。

MQTTClient_message 数据结构描述了MQTT 消息的负载和属性等相关信息,譬如消息的负载、负载的长度、qos、消息的保留标志、dup 标志等,但是消息主题不是这个结构体的一部分。该结构体内容如下:

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, "tcp://iot.ranye-iot.net:1883",
		"dt_mqtt_2_id",
		MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
	printf("Failed to create client, return code %d\n", rc);
	return EXIT_FAILURE;
}

注意,"tcp://iot.ranye-iot.net:1883"地址中,第一个冒号前面的tcp 表示我们使用的是TCP 连接;后面的1883 表示MQTT 服务器对应的端口号。

连接服务端

客户端创建之后,便可以连接服务器了,调用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:保留以备将来使用

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 = "dt2914/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 = "dt_mqtt_2"; //用户名
conn_opts.password = "dt291444"; //密码
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 可设置多个回调函数,包括:

1、断开连接时的回调函数cl (当客户端检测到自己掉线时会执行该函数,如果将其设置为NULL表示应用程序不处理断线的情况);
2、接收消息的回调函数ma(当客户端接收到服务端发送过来的消息时执行该函数,必须设置此函数否则客户端无法接收消息);
3、发布消息的回调函数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, "dt2914/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, "dt2914/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;如果客户端无法与服务器断开连接,则返回错误代码。

编写客户端程序

上小节介绍一些基本的MQTT 客户端库函数,除了这些基本API 之外,MQTT 客户端库还提供了其它很多的API。

本小节将使用上面介绍的几个API 来编写一个自己的MQTT 客户端应用程序,然后使其在我们的开发板上运行,实现自己的私人物联网项目。

功能设计如下:
⚫ 基于然也物联平台提供的社区版MQTT 服务器实现个人物联网小项目;
⚫ 用户可通过手机或电脑远程控制开发板上的一颗LED 灯;
开发板客户端每隔30 秒向服务端发送SoC 当前的温度值,用户通过手机或电脑可查看到该温度值。

示例程序笔者已经给大家写好了,由于功能比较简单,所以代码比较短,只有一个源文件;虽然只有一个源文件,为了学以致用,我们将使用cmake 来构建这个小项目。工程目录结构如下所示:
在这里插入图片描述

mqtt_prj 工程对应的路径为:开发板光盘->11、Linux C 应用编程例程源码->33_mqtt->mqtt_prj。
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/dt/tools/paho.mqtt.c-1.3.8/install/include)#MQTT头文件搜索路径
target_link_directories(mqttClient PRIVATE /home/dt/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 的内容,如下所示:

/***************************************************************
 Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
 文件名 : mqttClient.c
 作者 : 邓涛
 版本 : V1.0
 描述 : 开发板上的MQTT客户端应用程序示例代码
 其他 : 无
 论坛 : www.openedv.com
 日志 : 初版 V1.0 2021/7/20 邓涛创建
 ***************************************************************/

#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客户端库头文件

/* ########################宏定义##################### */
#define BROKER_ADDRESS	"tcp://iot.ranye-iot.net:1883"	//然也物联平台社区版MQTT服务器地址

/* 客户端id、用户名、密码 *
 * 当您成功申请到然也物联平台的社区版MQTT服务后,然也物联工作人员会给你发送8组用于连接社区版MQTT服务器的
 * 客户端连接认证信息:也就是客户端id、用户名和密码
 * 注意一共有8组,您选择其中一组覆盖下面的示例值
 * 后续我们使用MQTT.fx或MQTTool的时候 也需要使用一组连接认证信息去连接社区版MQTT服务器!
 * 由于这是属于个人隐私 笔者不可能将自己的信息写到下面 */
#define CLIENTID		"您的客户端ID"		//客户端id
#define USERNAME		"您的用户名"		//用户名
#define PASSWORD		"您的密码"			//密码

/* 然也物联社区版MQTT服务为每个申请成功的用户提供了个人专属主题级别,在官方发给您的微信信息中提到了
 * 以下 dt_mqtt/ 便是笔者的个人主题级别
 * dt_mqtt其实就是笔者申请社区版MQTT服务时注册的用户名
 * 大家也是一样,所以你们需要替换下面的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;
		}

		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 秒发一次。

构建、编译
我们直接进行编译,进入到工程目录下的build 目录中,执行cmake 构建:

~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DCMAKE_BUILD_TYPE=Release ..

在这里插入图片描述
执行make 编译:
在这里插入图片描述
编译成功之后,在build/bin 目录下生成了可执行文件mqttClient。
将可执行文件mqttClient 拷贝到开发板Linux 系统/home/root 目录下。

演示

在进行测试之前,确保开发板是能够上网的,也就是能够连接外网,注意不是局域网。执行mqttClient 客户端应用程序,如下所示:

在这里插入图片描述

这样,我们的开发板作为MQTT 客户端就成功连接上了MQTT 服务器,并且每隔30 秒向服务器发布温度信息,同样也会接收LED 主题的信息。

现在我们使用MQTT.fx 在电脑上进行测试,打开MQTT.fx 客户端软件,使用然也物联工作人员发给你的8 组客户端连接认证信息中的其中一组(不要使用开发板客户端已经使用的那组)去连接然也物联社区版MQTT 服务器:
在这里插入图片描述
接下来我们订阅温度主题"dt_mqtt/temperature",你需要修改成你的个人专属主题级别,也就是将dt_mqtt换成你的个人专属主题级别。订阅之后立马就会收到温度信息,并且之后每隔30 秒会收到开发板客户端发布的温度信息,如下图所示:
在这里插入图片描述
接着我们向LED 主题“dt_mqtt/led”(同样你也需要修改成你的个人专属主题级别,也就是将dt_mqtt换成你的个人专属主题级别)发布消息去控制开发板上的LED:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我这里没法给你演示,你自己看效果,有没有成功控制板上的LED 灯,反正笔者这里是没有问题的。

除了使用电脑之外,我们还可以使用手机控制或查看开发板芯片温度信息,在手机上使用MQTTool 软件连接然也物联社区版MQTT 服务器(同样也是使用8 组连接认证信息中的其中一组,不要使用开发板和MQTT.fx 正在使用的这组信息),连接成功之后订阅温度主题查看开发板温度信息、向LED 主题发布信息控制开发板上的LED 灯,如下所示:

在这里插入图片描述
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux MQTT 物联网通信 的相关文章

  • FileOutputStream.close() 中的设备 ioctl 不合适

    我有一些代码可以使用以下命令将一些首选项保存到文件中FileOutputStream 这是我已经写了一千遍的标准代码 FileOutputStream out new FileOutputStream file try BufferedOu
  • ALSA:snd_pcm_writei 调用时缓冲区不足

    当运行我最近从灰烬中带回来的旧程序时 我遇到了缓冲区不足的情况 该程序将原始声音文件完全加载到内存中 2100 字节长 525 帧 并准备 ALSA 进行输出 44 1khz 2 通道 有符号 16 位 if err snd pcm set
  • docker 非 root 绑定安装权限,WITH --userns-remap

    all 尝试让绑定安装权限正常工作 我的目标是在容器中绑定安装卷 以便 a 容器不以 root 用户身份运行入口点 二 docker daemon 配置了 userns remap 这样容器 主机上没有 root c 我可以绑定挂载和读 写
  • 在 .gitconfig 中隐藏 GitHub 令牌

    我想将所有点文件存储在 GitHub 上 包括 gitconfig 这需要我将 GitHub 令牌隐藏在 gitconfig 中 为此 我有一个 gitconfig hidden token 文件 这是我打算编辑并放在隐藏令牌的 git 下
  • 在 Linux 上以编程方式设置 DNS 名称服务器

    我希望能够通过我的 C C 程序为 Linux 上的 DNS 名称服务器添加 IP 地址 我在一个带有只读 etc resolv conf 的嵌入式平台上 这意味着我不能简单地将 nameserver xxx xxx xxx xxx 行添加
  • 就分页分段内存而言的程序寿命

    我对 x86 Linux 机器中的分段和分页过程有一个令人困惑的概念 如果有人能澄清从开始到结束所涉及的所有步骤 我们将很高兴 x86 使用分页分段内存技术进行内存管理 任何人都可以解释一下从可执行的 elf 格式文件从硬盘加载到主内存到它
  • linux-x64 二进制文件无法在 linuxmusl-x64 平台上使用错误

    我正在安装Sharp用于使用 package json 的 Nodejs 项目的 docker 映像上的映像压缩包 当我创建容器时 我收到有关 Sharp 包的以下错误 app node modules sharp lib libvips
  • 执行“minikube start”命令时出现问题

    malik malik minikube start minikube v1 12 0 on Ubuntu 18 04 Using the docker driver based on existing profile Starting c
  • 如何在linux中以编程方式获取dir的大小?

    我想通过 C 程序获取 linux 中特定目录的确切大小 我尝试使用 statfs path struct statfs 但它没有给出确切的大小 我也尝试过 stat 但它返回任何目录的大小为 4096 请建议我如何获取 dir 的确切大小
  • 如何阻止ubuntu在使用apt安装或更新软件包时弹出“Daemons using outdatedlibraries”? [关闭]

    Closed 这个问题是与编程或软件开发无关 help closed questions 目前不接受答案 我最近新安装了 Ubuntu 22 04 LTS 我发现每次使用 apt 安装或更新软件包时 它都会询问我有关Which servic
  • 使用非规范地址检索内存数据会导致 SIGSEGV 而不是 SIGBUS

    我无法使用以下汇编代码产生 总线错误 这里我使用的内存地址不是合法的 规范地址 那么 我怎样才能触发该错误呢 我在带有 NASM 2 14 02 的 Ubuntu 20 04 LTS 下运行这段代码 但它会导致负载出现 SIGSEGV 分段
  • 如何获取 (Linux) 机器的 IP 地址?

    这个问题和之前问的几乎一样如何获取本地计算机的IP地址 https stackoverflow com questions 122208 get the ip address of local computer 问题 但是我需要找到一个的I
  • CMake 链接 glfw3 lib 错误

    我正在使用 CLion 并且正在使用 glfw3 库编写一个程序 http www glfw org docs latest http www glfw org docs latest 我安装并正确执行了库中的所有操作 我有 a 和 h 文
  • 内核的panic()函数是否完全冻结所有其他进程?

    我想确认内核的panic 功能和其他类似kernel halt and machine halt 一旦触发 保证机器完全冻结 那么 所有的内核和用户进程都被冻结了吗 是panic 可以被调度程序中断吗 中断处理程序仍然可以执行吗 用例 如果
  • 如何在 Linux 中使用 C 语言使用共享内存

    我的一个项目有点问题 我一直在试图找到一个有据可查的使用共享内存的例子fork 但没有成功 基本上情况是 当用户启动程序时 我需要在共享内存中存储两个值 当前路径这是一个char and a 文件名这也是char 根据命令参数 启动一个新进
  • 为什么opencv videowriter这么慢?

    你好 stackoverflow 社区 我有一个棘手的问题 我需要你的帮助来了解这里发生了什么 我的程序从视频采集卡 Blackmagic 捕获帧 到目前为止 它工作得很好 同时我用 opencv cv imshow 显示捕获的图像 它也工
  • 如何在 *nix 中登录时运行脚本?

    我知道我曾经知道如何做到这一点 但是 如何在 unix 中登录时运行脚本 bash 可以 From 维基百科 Bash http en wikipedia org wiki Bash 28Unix shell 29 当 Bash 启动时 它
  • Apache 访问 Linux 中的 NTFS 链接文件夹

    在 Debian jessie 中使用 Apache2 PHP 当我想在 Apache 的文档文件夹 var www 中创建一个新的小节时 我只需创建一个指向我的 php 文件所在的外部文件夹的链接 然后只需更改该文件夹的所有者和权限文件夹
  • 检查已安装的软件包,如果没有找到则安装

    我需要检查已安装的软件包 如果未安装则安装它们 RHEL CentOS Fedora 示例 rpm qa grep glibc static glibc static 2 12 1 80 el6 3 5 i686 如何在 BASH 中进行检
  • 复制目录内容

    我想将目录 tmp1 的内容复制到另一个目录 tmp2 tmp1 可能包含文件和其他目录 我想使用C C 复制tmp1的内容 包括模式 如果 tmp1 包含目录树 我想递归复制它们 最简单的解决方案是什么 我找到了一个解决方案来打开目录并读

随机推荐