文章目录
- @[toc]
- 一、前言;
- 二、`MQTT`的常识;
- 三、官方核心代码;
- 四、二次修改完善断开连接;
一、前言;
- 由于乐鑫的
MQTT
代码工程存在些不足,本博文已根据部分修正部分代码。具体的刨坑链接:
https://github.com/espressif/ESP8266_RTOS_SDK/issues/285 ,修订时间:2018/8/27
esp8266
的实时系统rtos
是后面才出来支持的,其最后的调用也是调用乐鑫提供的API
接口,所以,如果你已经玩转了NONOS
下的编程,那么移植rtos
代码是非常迅捷的,因为你已经对其的API
接口非常熟悉,当然了,熟透一款芯片开发,当然不是一天半天的事情,需要长时间的积累。- 那么本博文是基于
rtos
的MQTT
协议的实现,优化了官方的代码示范,而且带你走一走MQTT
协议的世界。
二、MQTT
的常识;
众所周知,
MQTT
是一种轻捷快速的协议,基于TCP
之上,所以为长连接
的一种协议,非常适合那些短小消息发送的数据交互的用途,比如APP
的推送新闻用途,最常见的用在我们现在物联网领域;毕竟是小且快;
- 在进行彼此通讯时候,必须确保底层提供了有序、可靠、双向连接的网络连接。比如可以建立
TCP/TLS
连接。所以基本的通讯如下:
- 那么设备之间怎么样通讯呢?这就涉及到一些术语;要想指定某一个设备收到此条消息,那么就必须根据
topic
主题来识别,这个是服务器的事情了;下面列下一些常见的专用名词:
①:
ClientID
客户端唯一标识,服务端用于关联一个Session。
只能包含这些 大写字母,小写字母 和 数字(0-9a-zA-Z),23个字符以内,同一时间内 Server 和同一个 ClientID 只能保持一个 TCP 连接,再次连接会踢掉前一个连接的客户端。②:
Keep Alive
顾名思义,目的是保持长连接的可靠性,以及双方对彼此是否在线的确认。
客户端在Connect的时候设置 Keep Alive 时长。如果服务端在 1.5 * KeepAlive 时间内没有收到客户端的报文,它必须断开客户端的网络连接。③:
Will
遗嘱,遗愿;遗嘱消息(Will Message)存储在服务端,当网络连接关闭时,服务端必须发布这个遗嘱消息,所以被形象地称之为遗嘱,可用于通知异常断线。
④:
retain
0: 服务端不能存储这个消息,也不能移除或替换任何 现存的保留消息 。
1: 服务端必须存储这个应用消息和它的QoS等级,以便它可以被分发给未来的订阅者,所以如果后面未来有客户端订阅了这个主题,那么这个客户端一上线就会收到此消息。⑤:
qos
0: 【最多一次】 没有回复,不需要存储。有可能丢失(网络异常断开,业务层繁忙或者错误) 。
1: 【至少一次】确保消息到达,但消息重复可能会发生。
2: 【只有一次】确保消息到达一次;⑥:
poyload
用来传输用户的数据,最大允许 256MB ,发布的消息的
Payload
允许为空。在很多场合下,代表将持久消息(或者遗嘱消息)清空;格式为UTF-8
编码;
三、官方核心代码;
- 乐鑫已经针对
rtos
移植了eclipse
的标准的paho mqtt
,在官方的GitHub
已经看到了源码:点我查看,这个库非常出名,很多嵌入式的芯片都是移植这个库。
#define MQTT_CLIENT_THREAD_NAME "mqtt_client_thread"
#define MQTT_CLIENT_THREAD_STACK_WORDS 2048
#define MQTT_CLIENT_THREAD_PRIO 8
LOCAL xTaskHandle mqttc_client_handle;
static void messageArrived(MessageData* data)
{
printf("Message arrived: %s\n", data->message->payload);
}
static void mqtt_client_thread(void* pvParameters)
{
printf("mqtt client thread starts\n");
MQTTClient client;
Network network;
//指定缓存区的大小
unsigned char sendbuf[80], readbuf[80] = {0};
int rc = 0, count = 1;
MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
pvParameters = 0;
//初始化TCP连接
NetworkInit(&network);
//初始化客户端,注意后面都是发送和接收数据的缓存区,一定要加大这个缓存区的大小;否则后面发送不成功!
MQTTClientInit(&client, &network, 30000, sendbuf, sizeof(sendbuf), readbuf, sizeof(readbuf));
char* address = MQTT_BROKER;
//底层的TCP开始连接
if ((rc = NetworkConnect(&network, address, MQTT_PORT)) != 0) {
printf("Return code from network connect is %d\n", rc);
}
//后天任务:如果这个不成功执行,就不会自动进去回调方法:messageArrived;
#if defined(MQTT_TASK)
if ((rc = MQTTStartTask(&client)) != pdPASS) {
printf("Return code from start tasks is %d\n", rc);
} else {
printf("Use MQTTStartTask\n");
}
#endif
//定义mqtt版本: 3 = 3.1 , 4 = 3.1.1
connectData.MQTTVersion = 3;
//定义客户端ID(必须唯一): 大伙们可以定义mac地址作为ID
connectData.clientID.cstring = "ESP8266_sample";
//定义连接的账户名,这个根据服务器的选型来弄;【可有可无】
connectData.username.cstring= "admin";
//定义连接的账户名密码,这个根据服务器的选型来弄;【可有可无】
connectData.password.cstring="admin123456";
//定义连接心跳;
connectData.keepAliveInterval = 40;
//清楚会话
connectData.cleansession = true;
//连接MQTT服务器
if ((rc = MQTTConnect(&client, &connectData)) != 0) {
printf("Return code from MQTT connect is %d\n", rc);
} else {
printf("MQTT Connected\n");
}
//订阅主题 MQTTSubscribe --->【ESP8266/sample/pub】
if ((rc = MQTTSubscribe(&client, "ESP8266/sample/pub", 2, messageArrived)) != 0) {
printf("Return code from MQTT subscribe is %d\n", rc);
} else {
printf("fuck MQTT subscribe to topic \"ESP8266/sample/pub\"\n");
}
//死循环,时隔一秒发送一则消息
while (count++) {
//初始化一则消息结构体
MQTTMessage message;
char payload[80];
message.qos = QOS2;
message.retained = 0;
message.payload = payload;
sprintf(payload,
"{\"uuid\":\"dsaasdad22\",\"token\":\"saddsa412\",\"ver\":1.0,\"statusCode\":0,\"skill\":%d}",
count);
message.payloadlen = strlen(payload);
printf("MQTT publish to payloadlen :%s\n", message.payload);
if ((rc = MQTTPublish(&client, "ESP8266/sample/pub", &message)) != 0) {
printf("Return code from MQTT publish is %d\n", rc);
} else {
printf(
"MQTT publish topic \"ESP8266/sample/pub\", message number is %d\n",
count);
}
vTaskDelay(1000 / portTICK_RATE_MS); //send every 1 seconds
}
printf("mqtt_client_thread going to be deleted\n");
vTaskDelete(NULL);
return;
}
四、二次修改完善断开连接;
- 这个库和乐鑫自己做的那份
NONOS
代码不一样,这个是不会自动重连服务器的,假如你的路由器突然没了外网,导致这个连接不成功,那么就会永远发布不了消息;所以优化如下,代码略多,主要原理:
1、通过判断是否发布消息成功的标志,是否重新连接服务器和订阅主题;
2、 如果把发布消息的任务独立开来,就相当于开启了新的线程,我看了一些高质量的代码,都是创建一则消息队列,处于阻塞等待,直到有消息要发布,则在此死循环内发布,如果不发布,那么重新连接则发布;
3、连接和订阅主题的代码都是在死循环内的,但是初始化客户端的代码千万别在死循环内,因为这个初始化相当于开辟了内存,会占据内存,多次了连接了 ,就相当于开辟多个内存了!
static void Task_MqttClient_Connect(void* pvParameters) {
bool isNeedQueue = true;
Network network;
unsigned char sendbuf[2048], readbuf[2048] = { 0 };
int rc = 0, count = 0;
MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
pvParameters = 0;
NetworkInit(&network);
MQTTClientInit(&client, &network, 30000, sendbuf, sizeof(sendbuf), readbuf,
sizeof(readbuf));
//!!!!!不要把初始化放在里面
for (;;) {
//判断是否已经获取了路由器分配的IP
while (wifi_station_get_connect_status() != STATION_GOT_IP) {
vTaskDelay(1000 / portTICK_RATE_MS);
}
char* address = MQTT_SERVICE;
connectData.MQTTVersion = 3;
connectData.clientID.cstring = checkTopic;
connectData.username.cstring = MQTT_USER_NAME;
connectData.password.cstring = MQTT_USER_PAW;
connectData.keepAliveInterval = 40;
connectData.cleansession = true;
if ((rc = NetworkConnect(&network, address, MQTT_PORT)) != 0) {
printf("MClouds NetworkConnect connect is %d\n", rc);
}
if ((rc = MQTTStartTask(&client)) != pdPASS) {
printf("Return code from start tasks is %d\n", rc);
} else {
printf("Use MQTTStartTask\n");
}
if ((rc = MQTTConnect(&client, &connectData)) != 0) {
printf("[SY] MClouds connect is %d\n", rc);
network.disconnect(&network);
vTaskDelay(1000 / portTICK_RATE_MS);
}
if ((rc = MQTTSubscribe(&client, subTopic, QOS0, MessageArrived))
!= 0) {
printf("[SY] MClouds sub fail is %d\n", rc);
network.disconnect(&network);
vTaskDelay(1000 / portTICK_RATE_MS);
}
printf("MQTT subscribe to topic -> %s\n", subTopic);
xQueueReset(MqttMessageQueueHandler);
while (1) {
char payload[2048];
struct esp_mqtt_msg_type *pMsg;
printf("MqttMessageQueueHandler waitting ..\n");
//阻塞等待
xQueueReceive(MqttMessageQueueHandler, &pMsg, portMAX_DELAY);
sprintf(payload, "%s", pMsg->allData);
//printf("MQTT publish payload: %s\n", payload);
os_printf(" [SY] 1 MQTT get freeHeap: %d\n",system_get_free_heap_size());
MQTTMessage message;
message.qos = QOS0;
message.retained = false;
message.payload = (void*) payload;
message.payloadlen = strlen(payload) + 1;
if ((rc = MQTTPublish(&client, pubTopic, &message)) != 0) {
printf("Return code from MQTT publish is %d\n", rc);
} else {
printf("MQTT publish succeed ..\n");
}
if (rc != 0) {
break;
}
}
network.disconnect(&network);
}
printf("mqtt_client_thread going to be deleted\n");
vTaskDelete(NULL);
return;
}
(注意要填写服务器地址,还要熟悉
rtos
的消息队列。)
1.一定要用最新版的SDK
包的工程,而且要看博文前面的刨坑的连接里面的库文件是否更新到您的工程。
2. 由于下面的硬件代码链接不可以修改了,大家下载之后,修改下静态库文件和上面的Task_MqttClient_Connect
方法即可。之后通过不断轮询服务器是否断开,如果是则发送消息重连即可。
3.目前2018.8.27
为止,v2.0.0的SDK的MQTT还是蛮稳定的。断开连接的可能性较低。