MQTT QoS的原理与应用
学习QoS的原理与应用
一、什么是服务质量?
MQTT服务质量(Quality of Service 缩写 QoS)正是用于告知物联网系统,哪些信息是重要信息需要准确无误的传输,而哪些信息不那么重要,即使丢失也没有问题。
MQTT协议有三种服务质量级别:
QoS = 0 – 最多发一次
QoS = 1 – 最少发一次
QoS = 2 – 保证收一次
以上三种不同的服务质量级别意味着不同的MQTT传输流程。
对于较为重要的MQTT消息,我们通常会选择QoS>0的服务级别(即QoS 为1或2)。
QoS = 0 – 最多发一次
0是服务质量QoS的最低级别。当QoS为0级时,MQTT协议并不保证所有信息都能得以传输。也就是说,QoS=0的情况下,MQTT服务端和客户端不会对消息传输是否成功进行确认和检查。消息能否成功传输全看网络环境是否稳定。
也就是说,在QoS为0时。发送端一旦发送完消息后,就完成任务了。发送端不会检查发出的消息能否被正确接收到。
在网络环境稳定的情况下,信息传输一般是不会出现问题的。但是在环境不稳定的情况下,可能会在传输过程中出现MQTT消息丢失的情况。
QoS = 1 – 最少发一次
发送端将消息发送给接收端后,会等待接收端的确认。接收端成功接收消息后,会发送一条确认报文PUBACK给发送端。如果发送端收到了这条PUBACK确认报文,那么它就知道消息已经成功接收。
QoS = 2 – 保证收一次
MQTT服务质量最高级是2级,即QoS = 2。当MQTT服务质量为2级时,MQTT协议可以确保接收端只接收一次消息。
接收端收到QoS为2的消息后,会返回PUBREC报文作为应答。
发送端收到PUBREC报文后,会把此报文进行存储,并且返回PUBREL报文作为应答。
当接收端收到PUBREL报文后,会应答发送端一条PUBCOMP报文。至此,一次QoS2的MQTT消息传输就结束了。
2. 设置QoS
发布消息
客户端发布信息时,PUBLISH数据包中专有一个信息为qos。该信息正是用于设置客户端发布MQTT消息的QoS等级
订阅消息
同样的,在客户端订阅MQTT主题时,SUBSCRIBE数据包中也同样有一个信息用于设置订阅主题的QoS级别。客户端正是通过该主题来设置订阅主题的QoS级别的。
接收端连接服务端
另外,要想实现QoS>0的MQTT通讯,客户端在连接服务端时必须要将cleanSession设置为false。如果这一步没有实现,那么客户端是无法实现QoS>0的MQTT通讯。这一点非常关键,请您务必要留意。
3. 服务质量降级
对于发布和订阅消息的客户端,服务端会主动采用较低级别的QoS来实现消息传输。
4. 使用步骤
1.引入PubSubClient库
PubSubClient库目前只支持1级QoS订阅,因此我们将仅介绍如何使用ESP8266接收QoS=1的MQTT消息。
将QoS设置为1。 接收到服务端的信息代码如下(示例):
/**********************************************************************
程序目的/Purpose :
本程序旨在演示如何使用PubSubClient库使用ESP8266向MQTT服务器订阅信息。
订阅QoS级别为1。
- 在此程序控制下,ESP8266启动后将会尝试连接MQTT服务端。
连接时cleanSession=false。
- 接下来ESP8266在订阅主题时,将QoS设置为1。
- 保持ESP8266在线,使用MQTTfx向ESP8266所订阅的主题发布QoS=1的信息。
- ESP8266将会收到信息(至此还没有体现QoS=1的优势)
- 将ESP8266断电,然后再次使用MQTTfx向ESP8266订阅主题发布QoS=1的信息。
此时由于ESP8266未通电,所以无法接收到MQTTfx发送的信息。因此MQTT服务端
将会保存此信息。
- 将ESP8266再次通电,ESP8266连接到MQTT服务端后,将会马上收到了MQTTfx在ESP8266断电时所发送的信息。(这就是QoS=1的优势,即客户端断电再通电后依然可以收到QoS=1信息。)
要使用QoS=1订阅MQTT消息,需要满足以下要求:
1 接收端要有确定的clientID
2 接收端连接服务器时 cleanSession=False
3 发送端发布消息时 QoS=1或QoS=2
4 接收端订阅消息时 QoS=1
-----------------------------------------------------------------------
本示例程序为太极创客团队制作的《零基础入门学用物联网》中示例程序。
该教程为对物联网开发感兴趣的朋友所设计和制作。如需了解更多该教程的信息,请参考以下网页:
http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/esp8266-nodemcu-web-client/http-request/
***********************************************************************/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "dajiating";
const char* password = "xxxx";
const char* mqttServer = "test.ranye-iot.net";
// 如以上MQTT服务器无法正常连接,请前往以下页面寻找解决方案
// http://www.taichi-maker.com/public-mqtt-broker/
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
const int subQoS = 1; // 客户端订阅主题时使用的QoS级别(截止2020-10-07,仅支持QoS = 1,不支持QoS = 2)
const bool cleanSession = false; // 清除会话(如QoS>0必须要设为false)
const char* willTopic = "willTopic"; // 遗嘱主题名称
const char* willMsg = "willMsg"; // 遗嘱主题信息
const int willQos = 0; // 遗嘱QoS
const int willRetain = false; // 遗嘱保留
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // 设置板上LED引脚为输出模式
digitalWrite(LED_BUILTIN, HIGH); // 启动后关闭板上LED
Serial.begin(9600); // 启动串口通讯
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
// 连接WiFi
connectWifi();
// 设置MQTT服务器和端口号
mqttClient.setServer(mqttServer, 1883);
mqttClient.setCallback(receiveCallback);
// 连接MQTT服务器
connectMQTTserver();
}
void loop() {
// 如果开发板未能成功连接服务器,则尝试连接服务器
if (!mqttClient.connected()) {
connectMQTTserver();
}
// 处理信息以及心跳
mqttClient.loop();
}
// 连接MQTT服务器并订阅信息
void connectMQTTserver(){
// 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
String clientId = "client-" + WiFi.macAddress();
/* 连接MQTT服务器
boolean connect(const char* id, const char* user,
const char* pass, const char* willTopic,
uint8_t willQos, boolean willRetain,
const char* willMessage, boolean cleanSession);
若让设备在离线时仍然能够让qos1工作,则connect时的cleanSession需要设置为false
*/
if (mqttClient.connect(clientId.c_str(), NULL, NULL, willTopic, willQos, willRetain, willMsg, cleanSession)) {
Serial.print("MQTT Server Connected. ClientId: ");
Serial.println(clientId);
subscribeTopic(); // 订阅指定主题
} else {
Serial.print("MQTT Server Connect Failed. Client State:");
Serial.println(mqttClient.state());
delay(5000);
}
}
// 收到信息后的回调函数
void receiveCallback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message Received [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println("");
Serial.print("Message Length(Bytes) ");
Serial.println(length);
if ((char)payload[0] == '1') { // 如果收到的信息以“1”为开始
digitalWrite(BUILTIN_LED, LOW); // 则点亮LED。
} else {
digitalWrite(BUILTIN_LED, HIGH); // 否则熄灭LED。
}
}
// 订阅指定主题
void subscribeTopic(){
// 建立订阅主题。主题名称以Taichi-Maker-Sub为前缀,后面添加设备的MAC地址。
// 这么做是为确保不同设备使用同一个MQTT服务器测试消息订阅时,所订阅的主题名称不同
String topicString = "Taichi-Maker-Sub-" + WiFi.macAddress();
char subTopic[topicString.length() + 1];
strcpy(subTopic, topicString.c_str());
// 通过串口监视器输出是否成功订阅主题以及订阅的主题名称
// 请注意subscribe函数第二个参数数字为QoS级别。这里为QoS = 1
if(mqttClient.subscribe(subTopic, subQoS)){
Serial.print("Subscribed Topic: ");
Serial.println(subTopic);
} else {
Serial.print("Subscribe Fail...");
}
}
// ESP8266连接wifi
void connectWifi(){
WiFi.begin(ssid, password);
//等待WiFi连接,成功连接后输出成功信息
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.println("");
}
开发板服务质量为QoS1等级,不同的服务等级发送端在线,接收端,离线个在线接收结果如下表格
服务等级 |
0 |
1 |
2 |
发送端 |
在线 |
在线 |
在线 |
接收端 |
断电不能接收 |
都可以接收 |
都可以接收 |
*都可以接收 :重新上线后也可以接收发送端在离线(断电)前发送的内容。
二、 保留消息
保留消息得到介绍
保留消息的作用
无论显示客户端在任何时间订阅室温主题,都会马上收到该主题中的“保留消息”,也就是客户端发布的最新室温消息。
发布保留消息的方法
MQTT设备发布的保留消息的流程与发布普通消息的流程十分类似。唯一区别是,在发布保留消息时,MQTT设备需要将PUBLISH报文中retainFlag设置为true(如上图所示)。
修改保留消息的方法
每一个主题只能有一个“保留消息”,如果客户端想要更新“保留消息”,就需要向该主题发送一条新的“保留消息”,这样服务端会将新的“保留消息”覆盖旧的“保留消息”。当有客户端订阅该主题时,服务端就会将最新的“保留消息”发送给订阅客户端了。
删除保留消息的方法
如果要删除主题的“保留消息”,可以通过向该主题发布一条空的“保留消息”,也就是发送一条0字节payload的“保留消息”
保留消息应用
示例1 – 发布保留消息
/**********************************************************************
项目名称/Project : 零基础入门学用物联网
程序名称/Program name : publish_retained_msg
团队/Team : 太极创客团队 / Taichi-Maker (www.taichi-maker.com)
作者/Author : CYNO朔
日期/Date(YYYYMMDD) : 20201014
程序目的/Purpose :
本程序旨在演示如何使用PubSubClient库使用ESP8266向MQTT服务器发布保留信息。
此程序在a_publish_ranye_url程序基础上修改。重点修改内容是publish函数添加了
第三个参数,用于设置发布信息是否是保留信息(retained msg)
-----------------------------------------------------------------------
本示例程序为太极创客团队制作的《零基础入门学用物联网》中示例程序。
该教程为对物联网开发感兴趣的朋友所设计和制作。如需了解更多该教程的信息,请参考以下网页:
http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-c/esp8266-nodemcu-web-client/http-request/
***********************************************************************/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "taichi-maker";
const char* password = "12345678";
const char* mqttServer = "test.ranye-iot.net";
// 如以上MQTT服务器无法正常连接,请前往以下页面寻找解决方案
// http://www.taichi-maker.com/public-mqtt-broker/
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
void setup() {
Serial.begin(9600);
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
// 连接WiFi
connectWifi();
// 设置MQTT服务器和端口号
mqttClient.setServer(mqttServer, 1883);
// 连接MQTT服务器
connectMQTTServer();
if (mqttClient.connected()) { // 如果开发板成功连接服务器
pubRetMQTTmsg(); // 发布信息
}
}
void loop() {
mqttClient.loop(); // 保持心跳
}
void connectMQTTServer(){
// 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
String clientId = "esp8266-" + WiFi.macAddress();
// 连接MQTT服务器
if (mqttClient.connect(clientId.c_str())) {
Serial.println("MQTT Server Connected.");
Serial.println("Server Address: ");
Serial.println(mqttServer);
Serial.println("ClientId:");
Serial.println(clientId);
} else {
Serial.print("MQTT Server Connect Failed. Client State:");
Serial.println(mqttClient.state());
delay(3000);
}
}
// 发布信息
void pubRetMQTTmsg(){
// 建立发布主题。主题名称以Taichi-Maker-为前缀,后面添加设备的MAC地址。
// 这么做是为确保不同用户进行MQTT信息发布时,ESP8266客户端名称各不相同,
String topicString = "Taichi-Maker-Ret-" + WiFi.macAddress();
char publishTopic[topicString.length() + 1];
strcpy(publishTopic, topicString.c_str());
// 建立即将发布的保留消息。消息内容为"Retained Msg"。
String messageString = "Retained Msg";
char publishMsg[messageString.length() + 1];
strcpy(publishMsg, messageString.c_str());
// 实现ESP8266向主题发布retained信息
// 以下publish函数第三个参数用于设置保留信息(retained message)
if(mqttClient.publish(publishTopic, publishMsg, true)){
Serial.println("Publish Topic:");Serial.println(publishTopic);
Serial.println("Publish Retained message:");Serial.println(publishMsg);
} else {
Serial.println("Message Publish Failed.");
}
}
// ESP8266连接wifi
void connectWifi(){
WiFi.begin(ssid, password);
//等待WiFi连接,成功连接后输出成功信息
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.println("");
}
总结
QoS=1通讯时的注意事项
如想在MQTT通讯中实现服务质量等级为1级(QoS=1),我们要分别对消息的发布端课接收端进行相应的设置。以下列表中的内容是具体需要采取的措施。
接收端连接服务端时cleanSession设置为false
接收端订阅主题时QoS=1
发布端发布消息时QoS=1
QoS=2通讯时的注意事项
如想在MQTT通讯中实现服务质量等级为2级(QoS=2),我们要分别对消息的发布端和接收端进行相应的设置。以下列表中的内容是具体需要采取的措施。
接收端连接服务端时cleanSession设置为false
接收端订阅主题时QoS=2
发布端发布消息时QoS=2
PubSubClient 库只能使用QoS 1 级 来订阅消息。
QoS 1 开发板断电后,上线依然可以接收消息
QoS 1 发送和接受有确认请求,来确保消息接受
//MQTT使用保留消息的使用
retainFlag设置为true
const int subQoS = 1; // 客户端订阅主题时使用的QoS级别(截止2020-10-07,仅支持QoS = 1,不支持QoS = 2)
const bool cleanSession = false; // 清除会话(如QoS>0必须要设为false)
/* 连接MQTT服务器
boolean connect(const char* id, const char* user,
const char* pass, const char* willTopic,
uint8_t willQos, boolean willRetain,
const char* willMessage, boolean cleanSession);
若让设备在离线时仍然能够让qos1工作,则connect时的cleanSession需要设置为false */
mqttClient.connect(clientId.c_str(), NULL, NULL, willTopic, willQos, willRetain, willMsg, cleanSession)
// 通过串口监视器输出是否成功订阅主题以及订阅的主题名称
// 请注意subscribe函数第二个参数数字为QoS级别。这里为QoS = 1
mqttClient.subscribe(subTopic, subQoS);
// 实现ESP8266向主题发布retained信息
// 以下publish函数第三个参数用于设置保留信息(retained message)
mqttClient.publish(publishTopic, publishMsg, true)
//删除保留消息,再发一次内容为空就可以了