C语言代码封装MQTT协议报文,了解MQTT协议通信过程
MQTT协议图解,一文看懂MQTT协议数据包(真实报文数据解析解释)
MQTT与TCP通信协议的对比:MQTT报文打包–>TCP 数据包->IP数据包
详细介绍报文结构以及实例展示:https://blog.csdn.net/mark_md/category_10322631.html
目前物联网的通讯协议并没有一个统一的标准,比较常见的有MQTT、CoAP、DDS、XMPP 等,在这其中,MQTT(消息队列遥测传输协议)应该是应用最广泛的标准之一。目前,MQTT 已逐渐成为IoT 领域最热门的协议,阿里云IoT 物联网平台使用的就是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 服务端,都必须先连接到服务端。
①、首先客户端需要向服务端发送连接请求,这个连接请求实际上就是向服务端发送一个CONNECT报文,也就是发送了一个CONNECT 数据包。
②、MQTT 服务端收到连接请求后,会向客户端发送连接确认。连接确认实际上是向客户端发送一个CONNACK 报文,也就是CONNACK 数据包。
CONNECT 报文包含的信息如下图所示:
所谓报文就是一个数据包,MQTT 报文组成分为三个部分:固定头(Fixed header)、可变头(Variableheader)以及有效载荷(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 报文包含的信息如下图所示:
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 报文来断开与服务端的连接,如下图所示:
当客户端连接到服务端之后,就可以向服务端发布消息了,每条发布的消息必须指定一个“主题”,表示向某主题发布消息;MQTT 服务端可以通过主题来确定将消息转发给哪些客户端(订阅了该主题的客户端)。
MQTT 客户端向服务端发布消息其实就是向服务端发送一个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 报文还包含有“报文标识符”。报文标识符可用于对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报文后,会向发送发回复一个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 客户端软件,推荐MQTT.fx 这款软件,官网是http://mqttfx.jensd.de/。
以1.7.1 版本为例,进入到http://www.jensd.de/apps/mqttfx/1.7.1/链接地址进行下载,如下:
根据自己的需求下载即可!笔者使用的是Windows 64 位系统,所以笔者下载的是最后一个,下载完成之后得到一个exe 安装包文件:
直接双击即可安装,打开软件之后界面如下所示:
自己搭建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 服务器,在测试过程有几个问题需要注意下:
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”,如下:
当订阅之后我们的客户端立马就收到了一条消息,并且还标识了这条消息是“保留消息”。
心跳机制的原理在于:让客户端在没有向服务端发送消息的这个空闲时间里,定时向服务端发送一个心跳包,这个心跳包被称为心跳请求,其实质就是向服务端发送一个PINGREQ 报文;当服务端收到PINGREQ 报文后就知道该客户端依然在线,然后向客户端回复一个PINGRESP 报文,称为心跳响应,如下图所示:
由于心跳请求是定时发送的(通过keepAlive 设置时间间隔,也是告诉服务端,客户端将会多少多少秒向它发送心跳请求,这样服务端就会知道了);一旦服务器未收到客户端的心跳包,那么服务器就会知道,这台客户端可能已经掉线了。
这个心跳机制不仅可以用于服务端判断客户端是否在线,客户端也可使用心跳机制来判断自己与服务端是否保持连接。如果客户端在发送心跳请求(PINGREQ)后,没有收到服务端的心跳响应(PINGRESP),那么客户端就会认为自己与服务端已经断开连接了。
客户端断开与服务端的连接通常是有两种方式的:
⚫ 客户端主动向服务端发送DISCONNECT 报文,请求断开连接,自然服务端也就知道了客户端要离线了;
⚫ 客户端意外掉线。被动与服务端断开了连接。
MQTT 协议允许客户端在“活着”的时候就写好遗嘱,这样一旦客户端意外断线,服务端就可以将客户端的遗嘱公之于众。
客户端如何设置自己的“遗嘱”信息
客户端连接服务端时发送的CONNECT 报文中有这样几个参数,如下图红框中所示:
这几个参数都是以will 开头的,will 其实就是“遗嘱”的英文单词,下面分别介绍一下:
willTopic – 遗嘱主题
遗嘱消息和普通MQTT 消息很相似,也有主题和正文内容。willTopic 的作用正是告知服务端,本客户端的遗嘱主题是什么。只有那些订阅了这一遗嘱主题的客户端才会收到本客户端的遗嘱消息。
willMessage – 遗嘱消息
遗嘱消息定义了遗嘱的内容。在本示例中,那些订阅了主题“clientWill”的客户端会在客户端意外断线时,收到服务端发布的“client offline”这样的信息。
willRetain – 遗嘱消息的保留标志
遗嘱消息也可以设置为保留标志,用于告诉服务端是否需要对遗嘱消息进行保留处理。
willQoS – 遗嘱消息的QoS
对于遗嘱消息来说,同样可以使用服务质量来控制遗嘱消息的传递和接收。这里的服务质量与普通
MQTT 消息的服务质量是一样的概念。也可以设置为0、1、2。对于不同的服务质量级别,服务端会使用不同的服务质量来发布遗嘱消息。
MQTT.fx 如何设置客户端的“遗嘱”
MQTT.fx 软件如何为电脑客户端设置遗嘱呢?首先进入到配置页面中,如下所示:
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.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”链接地址,如下:
在这个页面中会有一些简单地介绍信息,大家可以自己看一看。我们往下看,找到它的下载地址:
目前最新的版本是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
本小节我们所介绍的这些库函数都是定义在MQTTClient.h 头文件中,也就是同步模式客户端库API,所以在我们的应用程序中需要包含头文件MQTTClient.h。
Tips:MQTT 客户端源码顶级目录docs/MQTTClient/html/index.html 文档向用户介绍了API 的使用方法,并且提供了相应示例代码供用户参考。
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
);
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
##################################
这个就不多说了,大家需要根据自己的交叉编译器的实际安装路径进行修改。
文件内容如下所示:
#*******************************************************************************
# 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 的内容,如下所示:
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : mqttClient.c
作者 : 邓涛
版本 : V1.0
描述 : 开发板上的MQTT客户端应用程序示例代码
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/7/20 邓涛创建
***************************************************************/
#include
#include
#include
#include
#include
#include
#include
#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 灯,如下所示: