MQTT
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种基于发布/订阅模式的轻量级通讯协议,构建于TCP/IP协议之上。它最初由IBM在1999年发布,主要用于在硬件性能受限和网络状况不佳的情况下,为远程设备提供可靠的消息传输服务。MQTT协议简单易用、可靠性高、延迟低,因此广泛应用于物联网(IoT)、机器人、智能城市管理、农业物联网以及能源监测与管理等领域。
MQTT协议由三个主要部分组成:客户端、服务器和主题。客户端是发送和接收消息的应用程序,可以是发布者或订阅者。服务器(也称为代理)负责处理消息,接收来自发布者的消息并将其传递给已订阅该主题的订阅者。主题是消息的路径,用于区分不同类型的消息。发布者将消息发布到特定主题,而订阅者则订阅感兴趣的主题以接收消息。
MQTT协议的工作原理如下:
- 连接建立:客户端(发布者或订阅者)与代理之间建立TCP连接。客户端需要提供客户端ID以及连接到代理的凭据(如用户名和密码)。
- 主题订阅:订阅者向代理发送订阅请求,以订阅特定的主题。
- 消息发布:发布者将消息发布到特定的主题。代理接收到消息后,会将其传递给已订阅该主题的订阅者。
- 消息传递:一旦代理接收到发布者发布的消息,并确认订阅者已订阅该主题,代理就会将消息传递给订阅者。订阅者收到消息后可以进行相应的处理。
- 断开连接:在通信结束后,客户端可以选择断开与代理的连接。断开连接时,客户端需要发送断开连接请求给代理。
MQTT协议的优点包括:
- 轻量级:MQTT协议规范简单,易于实现,对硬件资源要求低,适用于资源受限的设备。
- 高可靠性:使用TCP协议进行传输,保证了消息的可靠传递。
- 低延迟:基于发布/订阅模式,减少了消息传递的延迟,提高了实时性。
- 灵活性:MQTT协议支持多种消息传递方式,如QoS(服务质量)等级设置,以满足不同应用场景的需求。
MQTT协议在物联网领域的应用尤为广泛,可以帮助设备与云平台或中心服务器进行高效的数据交互。设备通过MQTT协议将采集到的数据发布到指定的主题,云平台或中心服务器订阅相应的主题即可实时获取数据。同时,云平台或中心服务器也可以通过MQTT向设备发送控制指令,实现对设备的远程监控与控制。
以上介绍来自文心一言。
我之前的文章中也有提到MQTT,当时用的Arduino和MicroPython写的ESP32的程序,我们需要找第三方库才能实现MQTT,但是这次我们使用的是ESP-IDF,人家官方自带了MQTT啦,我们就不需要去找第三方库。
除了MQTT,还有Modbus,TLS,HTTP之类的我们也都可以直接使用官方提供的API。
ESP-MQTT
#include "mqtt_client.h"
从编程指南的介绍可以看出官方提供的MQTT库支持MQTT v5.0版本的(当前编程指南的ESP-IDF是5.1版本的,不同IDF版本可能支持的MQTT版本不一样)。
并且该有的都有,不过MQTT本身也不复杂。
初始化句柄
esp_mqtt_client_handle_t esp_mqtt_client_init ( const esp_mqtt_client_config_t * config )
首先自然是初始化,然后返回给我们MQTT客户端句柄。
问题在于传入的参数,esp_mqtt_client_config_t这个结构体相当复杂,结构体里嵌套结构体再嵌套结构体,是我目前为止见过最复杂的配置参数了,定义结构体的代码包括注释足足有一百多行。
因此我们只挑几个常用的成员变量说。
esp_mqtt_client_config_t emcct = {
.broker.address.uri="mqtt://xxx.xxx.xxx.xxx",
.broker.address.port=1883,
.credentials.client_id="xxxxxx",
.credentials.username="xxx",
.credentials.authentication.password='xxx',
.session.keepalive=120,
.buffer.size=1024,
.buffer.out_size=1024
};
esp_mqtt_client_init(&emcct);
根据我上面的例子,我们按照顺序来介绍。
开头两个分别是mqtt服务器的uri和端口,端口一般默认都是1883,因此除非是自己搭的mqtt服务器,并且端口还改成乱七八糟的,一般都1883,在上面结构体中是不用配置的。
第三个是连接mqtt服务器所需要用到的ID,在同一个MQTT服务器同时连接的客户端中,不允许ID相同,如果是自己的服务器那无所谓,如果连接的公用的MQTT服务器,那么就需要保证这个ID不会和别人重复。这个不配置也问题不大,因为ESP-MQTT帮我们把这个ID默认设置为“ESP_xxxxxx”,其中xxxxxx是我们的MAC地址的后6位(16进制形式)
可以从上图看到如果我们不配置这个ID,那么会帮我们配置一个默认的ID。
默认的ID就是"ESP_xxxxxx",xxxxxx就是MAC地址的后三位。
我们再回到例子中第四和第五个结构体成员,看名字也可以知道是连接MQTT服务器用的用户名和密码。如果是使用的公用的MQTT服务器那么大概率是不需要的,但是如果有小伙伴为了得到更高质量的MQTT服务器的服务,那么提供MQTT服务器的服务商都是要求连接MQTT服务器需要用户名和密码的。
倒数第三个是心跳时间,也就是我们最多每隔多久就需要给MQTT服务器发送一个心跳包以证明我们还连接着,默认是120s,我们也可以修改,单位为s。
最后两个分别是我们接收数据的缓冲区大小和发送数据的缓冲区大小。接收的缓冲区大小默认为1024,发送的缓冲区大小默认和接收的一致,也就是说如果都不配置的话,那么默认都是1024。
一般情况下是够用的,但是如果发送或者接收的数据比较大时就需要修改了。
之前有个小项目,使用MQTT来传输图片,然后死活传不出去,整了一个下午才发现是图片的大小超过了缓冲区大小。
我们MQTT理论上最多可以传输256MB的数据,因此只要不是太离谱,都是可以发送的。
剩下还有一个遗嘱我在上面的例子中没有设置,但是还是提一下。
在esp_mqtt_client_config_t下的session_t下的last_will_t(套了三层结构体)可以配置遗嘱信息,看成员名字都可以看懂,我就不多介绍了,这里就提一下。
修改uri
esp_err_t esp_mqtt_client_set_uri(esp_mqtt_client_handle_t client, const char *uri)
除了一开始初始化,我们还可以后面再修改。
启动&停止MQTT客户端
esp_err_t esp_mqtt_client_start(esp_mqtt_client_handle_t client)
esp_err_t esp_mqtt_client_stop(esp_mqtt_client_handle_t client)
注册MQTT事件
和WiFI一样的是MQTT同样会产生很多事件,例如连接或是断开MQTT服务器,收到了订阅的消息等,因此我们需要注册MQTT的事件处理函数。
不一样的是MQTT拥有一套独立的注册函数。
esp_err_t esp_mqtt_client_register_event(esp_mqtt_client_handle_t client, esp_mqtt_event_id_t event, esp_event_handler_t event_handler, void *event_handler_arg)
关于参数怎么填写可以参考我下面的例子。
void mqtt_event_fun(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data){
//处理逻辑
}
esp_mqtt_client_register_event(emcht,ESP_EVENT_ANY_ID,mqtt_event_fun,NULL);
处理函数的格式需要跟上面一样(函数名自己随便起)。
参数一是MQTT客户端的句柄。
参数二是-1的宏定义,表示我们处理所有关于MQTT的所有ID的事件。
发布信息
int esp_mqtt_client_publish ( esp_mqtt_client_handle_t client , const char * topic , const char * data , int len , int qos , int keep )
这里简单提一下qos这个参数,是信息的等级0~2,0是最多发送一次消息,1是至少发送一次消息,2是必定能让订阅这个主题的人都收到消息,收不到就一直发。
因此等级越高,对资源的消耗越大,我们常用的就是0和1。很少用到2,除非是非常非常重要的消息。
最后一个参数没啥用,塞个0就行。
订阅主题
int esp_mqtt_client_subscribe_single ( esp_mqtt_client_handle_t client , const char * topic , int qos )
int esp_mqtt_client_subscribe_multiple ( esp_mqtt_client_handle_t client , const esp_mqtt_topic_t * topic_list , int size )
两个函数都可以订阅主题,区别在于第一个函数一次订阅一个主题,而第二个函数可以一次性订阅多个主题。
那么当我们订阅主题之后,肯定是希望我们能够第一时间收到信息的对吧,那么我们如何接收处理信息呢
在Arduino和MicroPython中,我们都是订阅了一个回调函数去处理,但是在ESP-IDF中,我们直接在MQTT事件中接收数据。
MQTT事件的ID为MQTT_EVENT_DATA的事件就是接收到订阅信息的事件,我们在触发了这个事件的逻辑中处理收到的数据。
这个数据我们从处理函数的最后一个形参里获取,由于它是void*类型的参数,我们无法直接使用,我们需要将其的类型进行强转,强转成esp_mqtt_event_t*类型的参数。
我们需要的信息就在上图我红框里,分别是数据和数据长度,以及主题名和主题名的长度。
重新连接&断开连接
esp_err_t esp_mqtt_client_reconnect(esp_mqtt_client_handle_t client)
小伙伴可能会感到疑惑,为什么是重新连接,没有连接的函数吗。
这是因为在我们启动MQTT客户端的时候就已经帮我们进行一次连接了。所以这个重新连接函数是用于我们已经连接过服务器后面又断开了,然后还需要再连接的时候使用的。
esp_err_t esp_mqtt_client_disconnect(esp_mqtt_client_handle_t client)
心跳信息
不用发送心跳信息!!!ESP-MQTT帮我们自动发送。
完整实操代码
了解了上面的API之后,我们就可以开始写代码进行MQTT的通信了。
在进行MQTT服务器的连接之前,需要先连上网,可以参考我上一篇文章。
下面的代码我都写上了注释,大家应该都能看得懂。直接拿走只需要把WiFI的名称和密码以及MQTT服务器的uri改掉就可以用了。
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
void Z_WiFi_Init(void);
void Z_Mqtt_Init(void);
bool Z_mqtt_connect_flag=false; //记录是否连接上MQTT服务器的一个标志,如果连接上了才可以发布信息
esp_mqtt_client_handle_t emcht; //MQTT客户端句柄
//WiFI事件处理函数
void wifi_event_fun(void* handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data){
printf("%s,%ld\r\n",event_base,event_id);
if(event_id==WIFI_EVENT_STA_START){ //如果是STA开启了,那么尝试连接
esp_wifi_connect();
}else if(event_id==WIFI_EVENT_STA_CONNECTED){ //连接上WiFI之后
Z_Mqtt_Init(); //开始连接MQTT服务器
}else if(event_id==WIFI_EVENT_STA_DISCONNECTED){ //断开WiFi之后
esp_wifi_connect(); //尝试重连WiFi
}
}
//MQTT事件处理函数
void mqtt_event_fun(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data){
printf("%s,%ld\r\n",event_base,event_id);
if(event_id==MQTT_EVENT_CONNECTED){ //连接上MQTT服务器
Z_mqtt_connect_flag=true;
esp_mqtt_client_subscribe_single(emcht,"Z_topic",1); //订阅一个测试主题
printf("success connect mqtt\r\n");
}else if(event_id==MQTT_EVENT_DISCONNECTED){ //断开MQTT服务器连接
Z_mqtt_connect_flag=false;
printf("lose connect mqtt\r\n");
}else if(event_id==MQTT_EVENT_DATA){ //收到订阅信息
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t )event_data; //强转获取存放订阅信息的参数
printf("receive data : %.*s from %.*s\r\n",event->data_len,event->data,event->topic_len,event->topic);
}
}
void Z_WiFi_Init(void){
nvs_flash_init(); //初始化nvs
esp_netif_init(); //初始化TCP/IP堆栈
esp_event_loop_create_default(); //创建默认事件循环
esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifi_event_fun,NULL); //绑定事件处理函数
esp_netif_create_default_wifi_sta(); //创建STA
wifi_init_config_t wict = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wict); //初始化WiFI
esp_wifi_set_mode(WIFI_MODE_STA); //设为STA模式
wifi_config_t wct = {
.sta = {
.ssid="xxx",
.password="xxx"
}
};
esp_wifi_set_config(WIFI_IF_STA,&wct); //设置WiFi
esp_wifi_start(); //启动WiFi
}
void Z_Mqtt_Init(void){
esp_mqtt_client_config_t emcct = {
.broker.address.uri="mqtt://xxx.xxx.xxx.xxx", //MQTT服务器的uri
.broker.address.port=1883 //MQTT服务器的端口
};
emcht = esp_mqtt_client_init(&emcct); //初始化MQTT客户端获取句柄
if(!emcht) printf("mqtt init error!\r\n");
//注册MQTT事件处理函数
if(esp_mqtt_client_register_event(emcht,ESP_EVENT_ANY_ID,mqtt_event_fun,NULL)!=ESP_OK) printf("mqtt register error!\r\n");
//开启MQTT客户端
if(esp_mqtt_client_start(emcht) != ESP_OK) printf("mqtt start errpr!\r\n");
}
void app_main(void){
Z_WiFi_Init();
char* data="Hello World";
while(1){
//每隔5S发布一次测试消息
if(Z_mqtt_connect_flag) esp_mqtt_client_publish(emcht,"test",data,strlen(data),1,0);
vTaskDelay(3000/portTICK_PERIOD_MS);
}
}
从结果上看,我们是可以正常地接收和发送数据的。
上图中我用的MQTT软件是MQTTfx1.7.1版本的(更高版本要收费。。。)
大家可以关注我的公众号“折途想要敲代码”回复关键词“ESP32”即可免费下载啦。