参考教程:https://www.bilibili.com/video/BV1L7411c7jw/?spm_id_from=333.1387.favlist.content.click
十二、MQTT遗嘱
1、意外掉线与遗嘱公布
(1)心跳机制可以让MQTT服务端随时掌握MQTT客户端连接情况,当检测到心跳停止,MQTT服务端就会判定MQTT客户端意外断线(相当于意外死亡),如果在意外断线前MQTT客户端曾在MQTT服务端"立遗嘱",那么MQTT服务端就会把意外断线客户端的遗嘱公布给订阅了该客户端遗嘱消息的其它客户端。

(2)当MQTT客户端正常断开连接时,会向MQTT服务端发送DISCONNECT报文,MQTT服务端接收到该报文后,就知道MQTT客户端是正常断开连接,而并非意外断开连接,也就不会公布该客户端的遗嘱消息了。

(3)MQTT服务端识别MQTT客户端意外断开连接的情况:
①在MQTT客户端与MQTT服务端之间的运输层连接(比如TCP连接)未断开的时候,超过1.5倍心跳时间间隔,MQTT服务端未收到MQTT客户端发布任何消息,也没收到MQTT客户端发送的心跳请求,则判定MQTT客户端意外断开连接。
②在MQTT服务端收到DISCONNECT报文前,MQTT客户端与MQTT服务端之间的运输层连接(比如TCP连接)断开,MQTT服务端将感知到运输层连接被异常关闭,同样会判定MQTT客户端意外断开连接。(比如直接关闭电脑端的MQTTfx进程,而不先断开与MQTT客户端的连接,就会导致未发送DISCONNECT报文即断开TCP连接)

2、客户端将遗嘱消息发送给服务端
(1)MQTT客户端在向MQTT服务端发送CONNECT报文时,就可以在报文中添加遗嘱信息配置。

(2)CONNECT报文中遗嘱信息的相关配置项:
①lastWillTopic ------遗嘱主题:
遗嘱消息和普通的MQTT消息很相似,也有主题和正文内容,lastWillTopic的作用正是告知MQTT服务端,本MQTT客户端的遗嘱主题是什么,只有那些订阅了这一遗嘱主题的MQTT客户端才会收到本MQTT客户端的遗嘱消息
②lastWillMessage------遗嘱消息:
lastWillTopic定义了遗嘱消息内容,也就是遗嘱正文
③lastWillQoS------遗嘱QoS:
对于遗嘱消息来说,同样可以使用服务质量来控制遗嘱消息的传递和接收,这里的服务质量与普通MQTT消息的服务质量是一样的概念,也可以设置为0、1、2
④lastWillRetain------遗嘱保留:
置为True,当MQTT客户端异常断线且仍未重连时,MQTT服务端会将遗嘱消息保存,只要有其它MQTT客户端订阅该主题,MQTT服务端马上将该主题信息发给它;置为False,当MQTT客户端异常断线且仍未重连时,MQTT服务端不会将遗嘱消息保存,只会在该客户端掉线的时候把其遗嘱消息公布给当时订阅了该客户端遗嘱消息的其它客户端,然后丢弃遗嘱消息
(3)MQTTfx中,可以在MQTT连接详情页的"LWT"选项卡下设置电脑MQTT客户端遗嘱消息。

3、ESP8266 MQTT遗嘱基本应用
(1)先前已经介绍了PubSubClient类connect函数的其中两种形式,实际上该函数还有第三种形式,函数参数依次为客户端ID、遗嘱主题、遗嘱QoS、遗嘱保留、遗嘱信息。
(2)PubSubClient类提供了成员函数setKeepAlive,该函数的函数参数为心跳时间间隔,调用它可以设置与MQTT服务器连接的心跳时间间隔。
(3)ESP8266 MQTT遗嘱基本应用示例代码:
cpp
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
const char* ssid = "Zevalin_Computer";
const char* password = "00114514";
const char* mqttServer = "test.mosquitto.org"; //如无法使用,可更换为其它公用MQTT服务器地址
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
// 遗嘱设置
const char* willMsg = "CLIENT-OFFLINE"; // 遗嘱消息内容
const int willQoS = 0; // 遗嘱QoS
const bool willRetain = false; // 遗嘱保留
void setup() {
Serial.begin(9600);
WiFi.mode(WIFI_STA); connectWifi();
mqttClient.setServer(mqttServer, 1883); // 设置MQTT服务器和端口号
mqttClient.setKeepAlive(10); // 设置心跳时间间隔
connectMQTTserver(); // 连接MQTT服务器
}
void loop() {
// 如果开发板未能成功连接服务器,则尝试连接服务器
if (!mqttClient.connected()) {
connectMQTTserver();
}
// 处理信息以及心跳
mqttClient.loop();
}
// 连接MQTT服务器并订阅信息
void connectMQTTserver(){
// 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
String clientId = "esp8266-" + WiFi.macAddress();
// 建立遗嘱主题,主题名称以Taichi-Maker-为前缀,后面添加设备的MAC地址,最后以"-Will"结尾,这是为确保不同ESP8266客户端的遗嘱主题名称各不相同
String willString = "Taichi-Maker-" + WiFi.macAddress() + "-Will";
char willTopic[willString.length() + 1];
strcpy(willTopic, willString.c_str());
// 连接MQTT服务器,在连接过程中提供以下参数:客户端ID、遗嘱主题、遗嘱QoS、遗嘱保留、遗嘱信息
if (mqttClient.connect(clientId.c_str(), willTopic, willQoS, willRetain, willMsg)){
Serial.println("MQTT Server Connected.");
Serial.print("Server Address: ");Serial.println(mqttServer);
Serial.print("ClientId: ");Serial.println(clientId);
Serial.print("Will Topic: ");Serial.println(willTopic);
}
else{
Serial.print("MQTT Server Connect Failed. Client State:");
Serial.println(mqttClient.state());
delay(5000);
}
}
void connectWifi(){
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.println("");
}
4、利用MQTT遗嘱实现设备在线状态发布
(1)方案策略说明:
①假设现在有一台MQTT客户端,它的客户端ID是"client",它的遗嘱主题是"client-will"。
②当client连接服务端时,CONNECT报文中的遗嘱消息是"offline",并且它的遗嘱保留设置为true。
③当client成功连接服务端后,立即向遗嘱主题"client-will"发布消息"online",同时在发布此消息时,保留标志设置为true,这样,只要client在线,那么任何设备一订阅(或者已订阅)"client-will"就能收到设备在线的消息"online"。
④如果client发生意外离线,那么任何设备一订阅(或者已订阅)"client-will"就会收到设备离线的消息"offline"。
⑤如果client-1恢复连接,那么它会将遗嘱主题"client-will"的保留消息更改为"online",这样任何设备一订阅(或者已订阅)"client-will"就能收到设备在线的消息"online"。(相当于跳回②,重复执行②③④)
(2)ESP8266利用MQTT遗嘱实现设备在线状态发布示例代码:
cpp
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
const char* ssid = "Zevalin_Computer";
const char* password = "00114514";
const char* mqttServer = "test.mosquitto.org"; //如无法使用,可更换为其它公用MQTT服务器地址
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
// 遗嘱设置
const char* willMsg = "CLIENT-OFFLINE"; // 遗嘱消息内容
const int willQoS = 0; // 遗嘱QoS
const bool willRetain = false; // 遗嘱保留
void setup() {
Serial.begin(9600);
WiFi.mode(WIFI_STA); connectWifi();
mqttClient.setServer(mqttServer, 1883); // 设置MQTT服务器和端口号
mqttClient.setKeepAlive(10); // 设置心跳时间间隔
connectMQTTserver(); // 连接MQTT服务器
}
void loop() {
// 如果开发板未能成功连接服务器,则尝试连接服务器
if (!mqttClient.connected()) {
connectMQTTserver();
}
// 处理信息以及心跳
mqttClient.loop();
}
// 连接MQTT服务器并订阅信息
void connectMQTTserver(){
// 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
String clientId = "esp8266-" + WiFi.macAddress();
// 建立遗嘱主题,主题名称以Taichi-Maker-为前缀,后面添加设备的MAC地址,最后以"-Will"结尾,这是为确保不同ESP8266客户端的遗嘱主题名称各不相同
String willString = "Taichi-Maker-" + WiFi.macAddress() + "-Will";
char willTopic[willString.length() + 1];
strcpy(willTopic, willString.c_str());
// 连接MQTT服务器,在连接过程中提供以下参数:客户端ID、遗嘱主题、遗嘱QoS、遗嘱保留、遗嘱信息
if (mqttClient.connect(clientId.c_str(), willTopic, willQoS, willRetain, willMsg)){
Serial.println("MQTT Server Connected.");
Serial.print("Server Address: ");Serial.println(mqttServer);
Serial.print("ClientId: ");Serial.println(clientId);
Serial.print("Will Topic: ");Serial.println(willTopic);
publishOnlineStatus(); //发布在线状态
}
else{
Serial.print("MQTT Server Connect Failed. Client State:");
Serial.println(mqttClient.state());
delay(5000);
}
}
void publishOnlineStatus(){
// 建立遗嘱主题,主题名称以Taichi-Maker-为前缀,后面添加设备的MAC地址,最后以"-Will"结尾,这是为确保不同ESP8266客户端的遗嘱主题名称各不相同
String willString = "Taichi-Maker-" + WiFi.macAddress() + "-Will";
char willTopic[willString.length() + 1];
strcpy(willTopic, willString.c_str());
// 建立设备在线的消息,此信息将以保留形式向遗嘱主题发布
String onlineMessageString = "CLIENT-ONLINE";
char onlineMsg[onlineMessageString.length() + 1];
strcpy(onlineMsg, onlineMessageString.c_str());
// 向遗嘱主题发布设备在线消息
if(mqttClient.publish(willTopic, onlineMsg, true)){
Serial.print("Published Online Message: ");Serial.println(onlineMsg);
}
else{
Serial.println("Online Message Publish Failed.");
}
}
void connectWifi(){
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.println("");
}
十三、MQTT用户密码认证
1、用户名与密码
(1)有些MQTT服务端需要MQTT客户端在连接时提供用户名和密码,只有MQTT客户端正确提供了用户名和密码后,才能连接MQTT服务端,否则MQTT服务端将会拒绝MQTT客户端连接,那么MQTT客户端也就无法发布和订阅消息了。
(2)MQTT客户端可以将用户名(username)和密码(password)通过CONNECT报文发送给MQTT服务端。需要注意的是,用户名和密码是可选信息),也就是说,有些MQTT服务端开启了客户端用户密码认证,这种MQTT服务端需要MQTT客户端在连接时正确提供认证信息才能连接,当然,那些没有开启用户密码认证的MQTT服务端则无需客户端提供用户名和密码认证信息。

(3)用户名和密码除了有以上功能外,有些公用MQTT服务端也利用此信息来识别MQTT客户端属于哪一个用户,从而对MQTT客户端进行管理,比如用户可以拥有私人主题,这些主题只有该用户可以发布和订阅,对于私人主题,MQTT服务端可以利用MQTT客户端连接时的用户名和密码来判断该客户端是否有发布或订阅该用户私人主题的权限。
(4)MQTTfx中,可以在MQTT连接详情页的"User Credentials"选项卡下设置MQTT连接的用户名和密码(如无需配置则空着即可)。

2、ESP8266进行用户密码认证
(1)先前已经介绍了PubSubClient类connect函数的其中三种形式,实际上该函数还有第四种形式,函数参数依次为客户端ID、用户名和密码。
(2)ESP8266进行用户密码认证示例代码:
cpp
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <Ticker.h>
// 设置Wi-Fi接入信息
const char* ssid = "Zevalin_Computer";
const char* password = "00114514";
const char* mqttServer = "test.mosquitto.org"; //如无法使用,可更换为其它公用MQTT服务端地址
// MQTT服务端连接用户名密码
const char* mqttUserName = "test-user";
const char* mqttPassword = "mosquitto-iot";
Ticker ticker;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
int count; // Ticker计数用变量
void setup() {
Serial.begin(9600);
WiFi.mode(WIFI_STA); //设置ESP8266工作模式为无线终端模式
connectWifi(); //连接Wi-Fi
mqttClient.setServer(mqttServer, 1883); //设置MQTT服务端和端口号
connectMQTTServer(); //连接MQTT服务端
ticker.attach(1, tickerCount); //每隔1秒,count计数加1
}
void loop() {
if (mqttClient.connected()) { // 如果开发板成功连接服务器
// 每隔3秒钟发布一次信息
if (count >= 3){
pubMQTTmsg();
count = 0;
}
// 保持心跳
mqttClient.loop();
}
else { // 如果开发板未能成功连接服务器
connectMQTTServer(); // 不断尝试连接服务器
}
}
void tickerCount(){
count++;
}
void connectMQTTServer(){
// 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
String clientId = "esp8266-" + WiFi.macAddress();
// 连接MQTT服务端,此处使用了程序首部定义的用户名和密码来实现MQTT服务端认证
if (mqttClient.connect(clientId.c_str(), mqttUserName, mqttPassword)) {
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 pubMQTTmsg(){ // 发布信息
static int value; // 客户端发布信息的内容之一
// 建立发布主题,主题名称以Taichi-Maker-为前缀,后面添加设备的MAC地址
// 这么做是为确保不同用户进行MQTT信息发布时,ESP8266客户端名称各不相同
String topicString = "Taichi-Maker-Pub-" + WiFi.macAddress();
char publishTopic[topicString.length() + 1]; //创建字符数组(末位为'/0')
strcpy(publishTopic, topicString.c_str()); //字符串拷贝到字符数组中
// 建立发布信息,信息内容以Hello World为起始,后面添加发布次数
String messageString = "Hello World " + String(value++);
char publishMsg[messageString.length() + 1]; //创建字符数组(末位为'/0')
strcpy(publishMsg, messageString.c_str()); //字符串拷贝到字符数组中
// ESP8266向主题发布信息
if(mqttClient.publish(publishTopic, publishMsg)) {
Serial.println("Publish Topic:");Serial.println(publishTopic);
Serial.println("Publish message:");Serial.println(publishMsg);
}
else {
Serial.println("Message Publish Failed.");
}
}
void connectWifi(){ // ESP8266连接Wi-Fi
WiFi.begin(ssid, password);
//等待Wi-Fi连接,成功连接后输出成功信息
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.println("");
}