重点:
- QoS = 0 – 最多发一次
- QoS = 1 – 最少发一次
- QoS = 2 – 保证收一次
Qos服务质量简介
一个物联网系统中有些信息非常重要,我们需要确保这类重要信息可以准确无误的发送和接收,而有些信息则相对不那么重要,这类信息如果在传输中丢失不会影响系统的运行。
MQTT服务质量(Quality of Service 缩写 QoS)正是用于告知物联网系统,哪些信息是重要信息需要准确无误的传输,而哪些信息不那么重要,即使丢失也没有问题。
MQTT协议有三种服务质量级别:
QoS = 0 – 最多发一次
QoS = 1 – 最少发一次
QoS = 2 – 保证收一次
以上三种不同的服务质量级别意味着不同的MQTT传输流程。对于较为重要的MQTT消息,我们通常会选择QoS>0的服务级别(即QoS 为1或2)。
另外这里提到的“发”与“收”有两种可能。一种是客户端发布消息时,将消息发送给服务端。一种是客户端订阅了某一主题消息后,服务端将消息发送给客户端。因此发布消息和接收消息的可能是服务端也可能是客户端。
为了避免造成混淆,后面的描述中将使用“发送端”来描述发送MQTT消息的设备,而使用“接收端”来描述接收MQTT消息的设备。
接下来我们仔细看一下这三种服务质量级别的具体含义。
QoS = 0 – 最多发一次
0是服务质量QoS的最低级别。当QoS为0级时,MQTT协议并不保证所有信息都能得以传输。也就是说,QoS=0的情况下,MQTT服务端和客户端不会对消息传输是否成功进行确认和检查。消息能否成功传输全看网络环境是否稳定。
也就是说,在QoS为0时。发送端一旦发送完消息后,就完成任务了。发送端不会检查发出的消息能否被正确接收到。
在网络环境稳定的情况下,信息传输一般是不会出现问题的。但是在环境不稳定的情况下,可能会在传输过程中出现MQTT消息丢失的情况。
QoS = 1 – 最少发一次
当QoS级别为1时,发送端在消息发送完成后,会检查接收端是否已经成功接收到了消息。但是发送端是如何实现这一检查的呢?看下图:
发送端将消息发送给接收端后,会等待接收端的确认。接收端成功接收消息后,会发送一条确认报文PUBACK给发送端。如果发送端收到了这条PUBACK确认报文,那么它就知道消息已经成功接收。
假如过了一段时间后,发送端没有收到PUBACK报文,那么发送端会再次发送消息,然后再次等待接收端的PUBACK确认报文。因此,当QoS=1时,发送端在没有收到接收端的PUBACK确认报文以前,会重复发送同一条消息。
所以QoS = 1时,每一条消息都至少传输一次。
另外回忆一下PUBLISH报文的内容。
当发送端重复发送一条消息时,PUBLISH报文中的dupFlag会被设置为True(如上图黑色横线所标注的部分)。这是为了告诉接收端,此消息为重复发送的消息。
QoS = 2 – 保证收一次
MQTT服务质量最高级是2级,即QoS = 2。当MQTT服务质量为2级时,MQTT协议可以确保接收端只接收一次消息。(我给你发(PUBLISH),你给我回一个你收到了(PUBREC),我再给你发一个你确定你收到了吗(PUBREL),你再给我回一个收到了别发了求你了(PUBCOMP))
如下图所示,QoS=2的收发相对更加复杂。发送端需要接收端进行两次消息确认。因此,2级MQTT服务质量是最安全的服务级别,也是最慢的服务级别。
下面我们来分步看一下Q0S=2时的消息发送和接收基本流程。
接收端收到QoS为2的消息后,会返回PUBREC报文作为应答。
发送端收到PUBREC报文后,会把此报文进行存储,并且返回PUBREL报文作为应答。
当接收端收到PUBREL报文后,会应答发送端一条PUBCOMP报文。至此,一次QoS2的MQTT消息传输就结束了。
以上是QoS=2时的MQTT通讯基本过程。这里只列出了基本流程,而没有过多的讲解MQTT协议是如何控制接收端只接收一次消息。这么做是因为本文的重点是MQTT应用。关于QoS=2的MQTT服务端内部控制机制,我们在实际开发MQTT物联网的过程中是不会涉及到的(想想库的作用,不是为了站在巨人的肩膀上,而是为了站在巨人的肩膀上看得更远)。
所以我们只需要牢记一点,那就是QoS=2可以保证接收端只收一次消息。
设置QoS
了解了QoS的含义后,我们该如何在MQTT通讯中设置QoS呢?下面我们来分别看下客户端在发布消息和订阅消息时如何设置QoS。
发布消息
如下图所示,客户端发布信息时,PUBLISH数据包中专有一个信息为qos。该信息正是用于设置客户端发布MQTT消息的QoS等级。
订阅消息
同样的,在客户端订阅MQTT主题时,SUBSCRIBE数据包中也同样有一个信息用于设置订阅主题的QoS级别。客户端正是通过该主题来设置订阅主题的QoS级别的。
换句话说,无论是发布(PUBLISH)还是订阅(SUBSCRIBE),都可以使用数据包中的qos消息设置服务质量级别。
接收端连接服务端
另外,要想实现QoS>0的MQTT通讯,客户端在连接服务端时必须要将cleanSession设置为false。如果这一步没有实现,那么客户端是无法实现QoS>0的MQTT通讯。这一点非常关键,请务必要留意。
服务质量降级
总有几个喜欢捣蛋的……假如客户端在发布和订阅信息时使用不同级别的QoS,将会发生什么情况。如下图所示,假如客户端A发布到主题1的消息是采用QoS = 2,然而客户端B订阅主题1采用QoS = 1。那么服务端该如何来应对这一情况呢?
在这种情况下,服务端会使用较低级别来提供服务。如下图所示,虽然A发送到主题1的消息采用QoS为2,但是服务端发送主题1的消息给B时,采用的QoS为1。这是因为B在订阅主题1时采用的QoS为1。
下面我们再来看一种情况。
如下图所示,假如客户端A发布主题1消息时使用QoS为0,而客户端B订阅主题1消息时使用QoS为1。
在这种情况下,虽然客户端B订阅主题1消息时QoS为1,但是由于客户端A发送主题1消息时QoS为0,所以服务端发送消息给B的QoS为0。
通过以上两个示例我们可以看到:对于发布和订阅消息的客户端,服务端会主动采用较低级别的QoS来实现消息传输。
QoS=1通讯时的注意事项
如想在MQTT通讯中实现服务质量等级为1级(QoS=1),我们要分别对消息的发布端和接收端进行相应的设置。以下内容是具体需要采取的措施。
- 接收端连接服务端时cleanSession设置为false
- 接收端订阅主题时QoS=1
- 发布端发布消息时QoS=1
QoS=2通讯时的注意事项
如想在MQTT通讯中实现服务质量等级为2级(QoS=2),我们要分别对消息的发布端和接收端进行相应的设置。以下内容是具体需要采取的措施。
- 接收端连接服务端时cleanSession设置为false
- 接收端订阅主题时QoS=2
- 发布端发布消息时QoS=2
小结
- 若想实现QoS>0,订阅端连接服务端时cleanSession需要设置为false,订阅端订阅主题时QoS>0,发布端发布消息时的QoS>0。
- 服务端会选择发布消息和订阅消息中较低的QoS来实现消息传输,这也被称作“服务降级”。
- QoS = 0, 占用的网络资源最低,但是接收端可能会出现无法接收消息的情况,所以适用于传输重要性较低的信息。
- QoS = 1, MQTT会确保接收端能够接收到消息,但是有可能出现接收端反复接收同一消息的情况。
- QoS = 2, MQTT会确保接收端只接收到一次消息。但是QoS为2时消息传输最慢,另外消息传输需要多次确认,因此所占用的网络资源也是最多的。此类服务等级适用于重要消息传输。
- 由于QoS1和QoS2都能确保客户端接收到消息,但是QoS1所占用的资源较QoS2占用资源更小。因此建议使用QoS1来实现网络资源较为珍贵的环境下传输重要信息。
验证测试
老规矩,使用MQTTfx软件验证下,思路如下:PubSubClient库目前只支持1级QoS订阅,因此将仅介绍如何使用ESP8266接收QoS=1的MQTT消息
要使用QoS=1订阅MQTT消息,需要满足以下要求:
1 接收端要有确定的clientID
2 接收端连接服务器时 cleanSession=False
3 发送端发布消息时 QoS=1或QoS=2
4 接收端订阅消息时 QoS=1
以下是ESP8266订阅QoS=1消息的示例程序。将此程序上传ESP8266开发板并且通过以下步骤测试ESP8166开发板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信息。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
| /********************************************************************** 项目名称/Project : 零基础入门学用物联网 程序名称/Program name : subscribe_qos1 团队/Team : 太极创客团队 / Taichi-Maker (www.taichi-maker.com) 作者/Author : CYNO朔 日期/Date(YYYYMMDD) : 20200813 程序目的/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/ ***********************************************************************/
// 设置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); 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的消息,ESP8266板子通电后串口输出显示截图如下: