跳转到内容

遗嘱消息

本节深入介绍 MQTT 遗嘱消息(Last Will and Testament, LWT)的机制、配置和故障处理。学习完成后,您将能够:

  • 理解遗嘱消息的工作原理和触发条件
  • 在 ESP32 中配置遗嘱消息
  • 设计健壮的设备在线检测机制
  • 处理遗嘱消息的限制和边界情况

在开始本节之前,请确保:

  • 理解出生和死亡消息
  • 了解 MQTT CONNECT 报文结构
  • Mosquitto Broker 已运行

遗嘱消息(LWT)是客户端在建立连接时注册的”遗言”。当客户端意外断开时,Broker 会代其发布这条消息。

关键特点

  • 在 CONNECT 时注册(连接建立之前)
  • 客户端正常 DISCONNECT 不会触发
  • 由 Broker 检测到连接中断后发布
  • 无法 100% 保证发送

会触发的场景

1. 网络断开 (TCP RST)
- 网线被拔掉
- WiFi 信号丢失
- 交换机/路由器宕机
2. Broker 检测到 Keep Alive 超时
- 客户端 1.5 倍 keepAlive 间隔内未发送任何数据
- Broker 发送 PINGREQ 无响应
3. 客户端崩溃
- 电源断电
- 固件崩溃 (panic)
- 硬件故障
不会触发的场景:
- 客户端发送 DISCONNECT 报文后正常断开
- Broker 主动踢出客户端(如 ACL 变更后断开连接)
CONNECT 报文结构(包含 LWT 字段):
┌──────────────────────────────────────┐
│ Protocol Name: "MQTT" │
│ Protocol Level: 4 (v3.1.1) │
│ Connect Flags: │
│ ├── Clean Session: 1 │
│ ├── Will Flag: 1 ◄── LWT 启用 │
│ ├── Will QoS: 1 │
│ ├── Will Retain: 1 │
│ ├── Password Flag: 0 │
│ └── Username Flag: 0 │
│ Keep Alive: 60 seconds │
│ Client ID: esp32_001 │
│ Will Topic: devices/esp32_001/status │
│ Will Message: {"status":"offline"} │
└──────────────────────────────────────┘
#include <PubSubClient.h>
WiFiClient espClient;
PubSubClient mqttClient(espClient);
#define DEVICE "esp32_001"
void connectToBroker() {
// 尝试连接,配置 Last Will
String clientId = DEVICE;
String willTopic = "devices/" + clientId + "/status";
String willMessage = "{\"status\":\"offline\",\"device\":\"" + clientId + "\"}";
// connect(clientID, user, pass, willTopic, willQoS, willRetain, willMessage)
if (mqttClient.connect(
clientId.c_str(), // Client ID
"", // Username
"", // Password
willTopic.c_str(), // Will Topic (设备状态 Topic)
1, // Will QoS (推荐 1)
true, // Will Retain (覆盖在线状态)
willMessage.c_str() // Will Message (离线)
)) {
Serial.println("Connected with LWT registered");
// 发布上线状态(覆盖保留的离线消息)
mqttClient.publish(
willTopic.c_str(),
("{\"status\":\"online\",\"device\":\"" + clientId + "\"}").c_str(),
true
);
} else {
Serial.print("Connection failed, rc=");
Serial.println(mqttClient.state());
}
}
// Node-RED Flow: 监控设备离线事件
// 订阅状态 Topic
// Topic: devices/+/status
var statusData = msg.payload;
var topic = msg.topic;
var deviceId = topic.split('/')[1];
// 判断是否为 LWT 触发的离线
var isLwt = false;
var isBirth = false;
if (statusData.status === "offline") {
// 通过时间戳判断:如果是突然离线(缺少正常断开标记)
// 可以认为是 LWT 触发
var deviceStatus = flow.get("device_" + deviceId) || {};
if (deviceStatus.lastSeen) {
var elapsed = Date.now() - deviceStatus.lastSeen;
if (elapsed < 30000) { // 30 秒内
isLwt = true; // 突然离线
}
}
msg.alert = {
level: isLwt ? "warning" : "info",
message: isLwt
? ("设备 " + deviceId + " 意外离线(可能是断电/断网)")
: ("设备 " + deviceId + " 正常离线"),
device: deviceId,
timestamp: Date.now()
};
}
// 更新设备状态
flow.set("device_" + deviceId, {
status: statusData.status,
lastSeen: Date.now(),
isLwt: isLwt,
ip: statusData.ip
});
return msg;

遗嘱消息的核心限制:

1. 不是 100% 可靠的
- Broker 本身崩溃 → LWT 不会触发
- 网络分区 → LWT 可能延迟触发
- 极端情况:Broker 和客户端同时宕机
2. 触发有延迟
- 取决于 Keep Alive 设置
- 默认延迟 = 1.5 × keepAlive 秒
- 通常 60-90 秒
3. 正常断开 vs 异常断开
- Broker 无法区分"正常关机"和"意外断电"
- 如果客户端发送了 DISCONNECT,LWT 不会触发
// keepAlive 设置影响 LWT 响应速度
// 快速检测(但增加网络流量)
mqttClient.setKeepAlive(10); // 每 10 秒心跳
// 断线检测时间: ~15 秒 (1.5×10)
// 标准设置
mqttClient.setKeepAlive(60); // 每 60 秒心跳
// 断线检测时间: ~90 秒 (1.5×60)
// 慢速检测(省电模式)
mqttClient.setKeepAlive(300); // 每 5 分钟心跳
// 断线检测时间: ~7.5 分钟 (1.5×300)

除了 LWT,还可以通过应用层心跳实现更精确的离线检测:

// ESP32 定时发送心跳消息
unsigned long lastHeartbeat = 0;
const unsigned long HEARTBEAT_INTERVAL = 30000; // 30 秒
void loop() {
mqttClient.loop();
if (millis() - lastHeartbeat > HEARTBEAT_INTERVAL) {
lastHeartbeat = millis();
// 发布心跳
mqttClient.publish(
"devices/esp32_001/heartbeat",
"{\"timestamp\":" + String(millis()) + "}",
false
);
}
}
// Node-RED: 心跳超时检测
// 使用定时器检查设备心跳
// 每 60 秒检查一次
var devices = flow.get("device_heartbeats") || {};
var now = Date.now();
var offlineDevices = [];
for (var deviceId in devices) {
var lastBeat = devices[deviceId];
// 如果超过 90 秒未收到心跳
if (now - lastBeat > 90000) {
offlineDevices.push(deviceId);
}
}
if (offlineDevices.length > 0) {
msg.offlineDevices = offlineDevices;
return msg;
}
return null;
Terminal window
# 测试步骤
# 1. 终端 1: 订阅设备状态
mosquitto_sub -h localhost -t "devices/esp32_001/status" -v
# 2. 终端 2: 模拟有 LWT 的客户端连接
mosquitto_pub -h localhost \
-t "devices/esp32_001/status" \
--will-topic "devices/esp32_001/status" \
--will-payload '{"status":"offline","device":"esp32_001"}' \
--will-retain \
-r -m '{"status":"online","device":"esp32_001"}' \
-d
# 3. 在终端 2 按 Ctrl+C 强制断开
# 预期: 终端 1 收到离线消息
  • 推荐: LWT 使用保留消息,确保状态持久化
  • 推荐: 设置合理的 Keep Alive 间隔(10-60 秒)
  • 推荐: 在应用层补充心跳检测机制
  • 避免: 完全依赖 LWT 作为唯一在线检测手段
  • 避免: 设置过长的 Keep Alive(检测延迟大)
  • 避免: 在 LWT 中包含过多数据

本节要点总结:

  1. LWT 定义:客户端 CONNECT 时注册,意外断开时 Broker 发布
  2. 触发条件:网络断开、Keep Alive 超时、客户端崩溃
  3. 关键限制:非 100% 可靠,有检测延迟(1.5×keepAlive)
  4. 最佳实践:LWT + 保留消息 + 应用层心跳三重保障
  5. Keep Alive:影响检测速度和网络负载的平衡