概述
在之前的文章中:https://blog.csdn.net/m0_50114967/article/details/127016395
已经简单地介绍了MQTT协议,对比于其它网络协议,MQTT协议在物联网的开发中,它的特点使它适用于大多数受限的环境。例如网络代价昂贵,带宽低、不可靠,在嵌入设备中运行,处理器和内存资源有限。
MQTT介绍
下面深入了解一下MQTT协议的特点和优势,下图是一个MQTT的概念图:
如图所示,MQTT基于一个MQTT服务器(MQTT Broker),所有设备或客户端都可以是一个发布设备同时也可以是一个订阅设备,所以,只要你的设备可以连接在同一个MQTT服务器,都可以给其它设备进行发布任务或接收其它设备发布的数据,实现一对多的消息发布,完美地解决设备或应用程序的耦合。
MQTT消息服务质量
MQTT发布的消息有三种服务质量:
QoS0:最多一次,可能导至发布的消息丢失
QoS1:至少一次,可能导至发布的消息多次发布
QoS2:确保只有一次,保证消息到达对方并且只到达一次
QoS等级越高,系统消耗也越高,在应用时可以根据需求选择合适的QoS等级。
MQTT遗嘱标记
服务器与客户端通信时,当遇到异常或客户端心跳超时的情况,MQTT服务器会替客户端发布一个遗嘱消息。当然如果服务器收到来自客户端的断开的消息(如自主选择断开连接),则不会触发遗嘱消息的发送。 在重要的应用或设备上使用该标记,可以在发生网络故障或网络波动,设备在保持连接周期内未能通讯,连接被服务端关闭,设备意外掉电,设备尝试进行不被允许的操作而被服务端关闭连接,例如订阅自身权限以外的主题等异常时通知第三方。
心跳机制
客户端可以设置一个心跳时间间隔,客户端会周期性地给服务发送一个心跳请求(PINGREQ),服务器收到请求后会回复并响应心跳请求(PINGRESP)。如果客户端在发送心跳请求(PINGREQ)后,没有收到服务端的心跳响应(PINGRESP),那么客户端就会认为自己与服务端的连接已经被断开了。
更多MQTT的介绍可以到http://mqtt.p2hp.com/了解
在ESP32上使用MQTT协议
在ESP32上使用MQTT协议,可以用的库比较多,这里选择pubsubclient ,该库可以找到的资料与文档说明都比较详细,唯一的不足是该库只能发布QoS0的消息和订阅Qos0和Qos1的消息。
该库的项目地址:https://github.com/knolleary/pubsubclient
API文档(英文)地址:https://pubsubclient.knolleary.net/api
主要函数和方法
PubSubClient (server, port, [callback], client, [stream])
创建一个完全配置的客户端实例。
参数:
server IPAddress, uint8_t[] 或 const char[] -服务器地址
port int - 要连接的端口
callback function* (可选) -一个指向消息回调函数的指针,消息到达客户端时调用该函数
client -使用的网络客户端,例如WiFiClient
stream Stream (可选) - 将接收到的消息写入的流
boolean connect (clientID, [username, password], [willTopic, willQoS, willRetain, willMessage], [cleanSession])
客户端连接到服务器
参数:
clientID const char[] - 连接到服务器时使用的客户端ID
Credentials - (可选)
username const char[] - 要使用的用户名。如果为NULL,则不使用用户名或密码
password const char[] - 要使用的密码。如果为NULL,表示不使用密码
Will - (可选)
willTopic const char[] - 遗嘱消息要使用的主题
willQoS int: 0,1 or 2 - 遗嘱消息将要使用的服务质量
willRetain boolean - 遗嘱是否应以保留标记公布
willMessage const char[] - 遗嘱消息的有效负载
cleanSession boolean (可选) - 是否连接clean-session
返回值:
false - 连接失败
true - 连接成功
boolean connected ()
检查客户端是否连接到服务器端。
返回
false - 未连接
true - 已连接
boolean loop ()
需要循环调用该函数,以便客户端用来处理传入消息并维护与服务器的连接。
返回值:
false - 客户端不在连接状态
true - 客户端处于连接状态
boolean publish (topic, payload, [length], [retained])
将消息发布到指定的主题。
参数:
topic const char[] - 要发布的主题
payload const char[], byte[] - 要发布的消息
length unsigned int (可选) - 有效载荷的长度。如果有效负载是byte[],则为必选项。
retained boolean (可选) - 是否保留消息
false - 不保留
true - 保留
返回:
false - 发布失败,要么连接丢失,要么消息长度超长
true - 发布成功
boolean subscribe (topic, [qos])
定阅发送到主题的消息
参数:
topic const char[] - 订阅的主题
qos int: 只能选择 0 或 1 (可选) - 订阅消息的服务质量
返回:
false - 订阅失败,要么连接丢失,要么消息超长
true - 订阅成功
ESP32连接MQTT服务器
pubsubclient这个库的头文件为PubSubClient.h,同时,该库需要使用一个网络客户端,这里直接使用WiFi库来创建一个网络客户端实例,所以同时要引入WiFi.h这个头文件。
同时,我们创建几个变量,来保存连接服务器所需要的数据,如服务器地址和连接端口。
#include <WiFi.h>
#include <PubSubClient.h>
const char* mqttServer = "broker.emqx.io"; //MQTT服务器地址,一个公共的免费MQTT服务器
const int mqttPort = 1883; //连接的端口
/***********************************************************************************
* 创建一个网络客户端,用来创建MQTT客户端实例
***********************************************************************************/
WiFiClient espClient;
/***********************************************************************************
* 创建一个完全配置的客户端实例。
* 参数
* mqttServer - 服务器地址
* mqttPort - 要连接的端口
* callback - 一个指向消息回调函数的指针,当消息到达此客户端创建的订阅时调用该函数
* espClient - 使用的网络客户端,例如WiFiClient
***********************************************************************************/
PubSubClient mqtt_client(mqttServer,mqttPort,callback,espClient);
客户端已经设置好,后面我们需要利用connect ()来连接服务器,正常情况下,连接服务器前,我们先确定ESP32是否已连接网络,因为这里我们是用WIFI来连接网络的,所以这里写一个函数来方便调用
/***********************************************************************************
* 函数:连接mqtt服务器
* 返回:
* 返回连接状态
***********************************************************************************/
boolean connect_MQTT(){
if(WiFi.status() == WL_CONNECTED){ //WIFI是否连接
if(mqtt_client.connect("ESP32Client")){ //连接服务器,客户端ID可以自定义
Serial.println("连接MQTT服务器成功"); //输出连接状态
mqtt_client.publish("ESP32", "ESP32已连接MQTT服务器"); //发布一个消息到ESP32主题
mqtt_client.subscribe("ESP32"); //订阅一个ESP32主题
}
}
return mqtt_client.connected(); //返回连接状态
}
当设备连接上服务器后,大多数情况下,我们会把设备的一些数据发布,比如ESP32的引脚状态,从传感器获取到的数据等。所以,我们还需要创建一个定时发布消息的函数。
/***********************************************************************************
* 函数:定时发布消息到指定主题
***********************************************************************************/
long lastSendTime = 0; //最后发送时间,该变量为全局变量
void regularPublish(){
long publishNow = millis(); //得到当前时间
if (publishNow - lastSendTime > 5000){ //当前时间-最后发送时间>5000时
lastSendTime = publishNow; //把最后发送时间设为当前时间
mqtt_client.publish("ESP32", "来自esp32定时发布的消息"); //发布一个消息到ESP32主题
}
}
除了发布消息,我们也需要ESP32响应其它客户端发布的消息,来控制ESP32或响应对方的指令,如从手机端或电脑发送一个指令来控制ESP32的引脚状态,从而实现多远端来实现远程控制。该函数设定为当ESP32收到一个第一个字符为1的消息后,设置ESP32的2号引脚设为高电平,而收到的字符为0时2号引脚设为低电平。
/***********************************************************************************
* 函数:回调函数,收到消息时的动作,参数是固定的
* 参数:
* char* topic:主题
* byte* payload:消息内容
* unsigned int length:消息内容长度
***********************************************************************************/
void callback(char* topic, byte* payload, unsigned int length) {
if(payload[0]=='1'){
digitalWrite(2,1); //2号引脚设为高电平
Serial.println("2号引脚设为高");
}else if(payload[0]=='0'){
Serial.println("2号引脚设为低");
digitalWrite(2,0); //2号引脚设为低电平
}else{
return;
}
}
该回调函数对应之前创建客户端实例里第三个参数。
PubSubClient mqtt_client(mqttServer,mqttPort,callback,espClient);
对于在线设备,我们需要考虑设备或网络异常的情况,让设备出现意外掉线等情况下自动可以重新连接服务器,这里用一个函数来调用之前连接MQTT服务器的函数,该函数在设备在线的情况下运行loop()函数,来接收已订阅的主题所收到的消息,如果设备出现异常,会每5秒调用之前连接MQTT服务器的函数来重新连接服务器。
/***********************************************************************************
* 函数:mqtt循环
* 如果连接断开,每5秒进行一次重连
***********************************************************************************/
long lastAttemptTime = 0; //该变量为全局变量,用来保存最后连接的时间
void mqtt_loop(){
if(!mqtt_client.connected()){ //如果未连接MQTT服务器
long now = millis(); //得到当前时间
if (now - lastAttemptTime > 5000){ //当前时间-最后连接时间>5000时
lastAttemptTime = now; //把最后连接时间设为当前时间
mqtt_client.setKeepAlive(60); //心跳时间设为60秒
if(connect_MQTT()){ //如果连接服务器成功
lastAttemptTime = 0; //最后连接时间设为0
}
}
}else{
regularPublish(); //定时发布消息
mqtt_client.loop(); //MQTT循环
}
}
完整实现代码
ESP32_web_server_12_mqtt.ino
#include "ESPAsyncWebServer.h"
AsyncWebServer server(80); //创建一个服务器对象,WEB服务器端口:80
void setup() {
Serial.begin(9600); //串口波特率初始化
LittleFS_begin(); //LittleFS文件系统初始化
connect_NET(); //网络初始化
web_server(); //WEB服务器初始化
GPIO_begin(); //引脚初始化
connect_MQTT(); //连接MQTT服务器
}
void loop() {
DNS_request_loop(); //DNS服务请求处理
mqtt_loop(); //mqtt服务循环
}
mqtt_server.ino
#include <WiFi.h>
#include <PubSubClient.h>
/***********************************************************************************
* 库:https://github.com/knolleary/pubsubclient
* 该库只能发布QoS0消息,可以订阅QoS0或QoS1消息
* 最大消息,包括头,默认为256个字节,可以通过PubSubClient::setBufferSize(size)重新配置
* 心跳间隔默认为15秒,可以通过PubSubClient::setKeepAlive(keepAlive)重新配置
* 默认mqtt版本为3.1.1。
***********************************************************************************/
const char* mqttServer = "broker.emqx.io"; //MQTT服务器地址,一个公共的免费MQTT服务器
const int mqttPort = 1883; //连接的端口
const char* mqttUser = "yourMQTTuser"; //公共的MQTT服务器一般都不要求帐号登陆,这里可以不设置
const char* mqttPassword = "yourMQTTpassword"; //公共的MQTT服务器一般都不要求帐号登陆,这里可以不设置
long lastAttemptTime = 0; //最后更新时间
long lastSendTime = 0; //最后发送时间
/***********************************************************************************
* 创建一个网络客户端,用来创建MQTT客户端实例
***********************************************************************************/
WiFiClient espClient;
/***********************************************************************************
* 创建一个完全配置的客户端实例。
* 参数
* mqttServer - 服务器地址
* mqttPort - 要连接的端口
* callback - 一个指向消息回调函数的指针,当消息到达此客户端创建的订阅时调用该函数
* espClient - 使用的网络客户端,例如WiFiClient
***********************************************************************************/
PubSubClient mqtt_client(mqttServer,mqttPort,callback,espClient);
/***********************************************************************************
* 函数:定时发布消息到指定主题
***********************************************************************************/
void regularPublish(){
long publishNow = millis(); //得到当前时间
if (publishNow - lastSendTime > 5000){ //当前时间-最后发送时间>5000时
lastSendTime = publishNow; //把最后发送时间设为当前时间
mqtt_client.publish("ESP32", "来自esp32定时发布的消息"); //发布一个消息到ESP32主题
}
}
/***********************************************************************************
* 函数:回调函数,收到消息时的动作,参数是固定的
* 参数:
* char* topic:主题
* byte* payload:消息内容
* unsigned int length:消息内容长度
***********************************************************************************/
void callback(char* topic, byte* payload, unsigned int length) {
/*
String str;
payload[length]='