第三章:遗嘱消息配置实战

好,咱们今天来点真格的。

上一章我们聊了遗嘱消息是什么、为什么需要它。这一章,我带你亲手配置一个。用Python的paho-mqtt库,把遗嘱主题、消息内容、QoS等级、还有那个容易搞混的Retain标志,一个一个敲出来。

说实话,我刚开始学MQTT的时候,遗嘱消息这块也踩过坑。尤其是Retain标志,当时想当然地以为「遗嘱消息肯定要保留啊,不然断线了别人怎么知道?」结果呢?后面我跟你讲。

3.1 准备工作:安装与导入

先确保你装了paho-mqtt。没装的话,一行命令搞定:

pip install paho-mqtt

然后导入它:

import paho.mqtt.client as mqtt
import time
import json

嗯,json也顺手导入,因为实际项目中遗嘱消息往往是个结构化的数据,比如设备状态、错误码之类的。我个人习惯用JSON格式,可读性好,解析也方便。

3.2 设置遗嘱消息的核心方法

paho-mqtt客户端里,设置遗嘱消息用的是 will_set() 方法。它的签名长这样:

client.will_set(topic, payload=None, qos=0, retain=False)

四个参数,咱们一个一个拆开讲。

3.2.1 遗嘱主题(topic)

主题就是别人订阅的「频道」。比如你的设备叫 sensor/temp_01,那遗嘱主题可以设为 sensor/temp_01/status。这样其他服务订阅了这个主题,就能第一时间知道设备掉线了。

我在项目中遇到过一个问题:有人把遗嘱主题设成了和普通数据发布一样的主题。结果设备正常运行时发「温度25度」,设备掉线时发「设备离线」。你想想看,订阅端收到消息还得判断是正常数据还是遗嘱消息,逻辑就乱了。所以我建议:遗嘱主题最好和业务数据主题分开,比如加个 /status/will 后缀。

3.2.2 遗嘱消息(payload)

消息内容可以是字符串、字节串,甚至JSON。我一般用JSON,因为可以带更多信息:

will_payload = json.dumps({
    "device_id": "sensor_temp_01",
    "status": "offline",
    "timestamp": int(time.time()),
    "reason": "unexpected_disconnect"
})

你看,这样订阅端拿到消息后,不光知道设备离线了,还知道什么时间、什么原因。调试的时候特别有用。

3.2.3 遗嘱QoS

QoS等级决定了消息传递的可靠性。遗嘱消息的QoS可以设0、1、2。怎么选?

QoS 含义 适用场景
0 最多一次 丢了也无所谓,比如日志
1 至少一次 必须通知到,但可以重复
2 恰好一次 严格不能丢也不能重复

我个人习惯:遗嘱消息至少用QoS 1。为什么?因为设备掉线这件事本身就不常见,一旦发生,你肯定希望订阅方能收到。QoS 0的话,万一网络抖动一下,遗嘱消息丢了,那整个机制就白设了。我曾经在一个工业项目里吃过这个亏,后来全部改成了QoS 1。

3.2.4 Retain标志

这里要重点说一下。Retain标志的意思是:这条消息要不要被Broker保留下来,给后来的订阅者?

很多人会想:「遗嘱消息当然要保留啊,不然新上线的客户端怎么知道设备掉线了?」

嗯,这个想法有道理,但不全对。

举个例子:设备A掉线了,Broker发布了遗嘱消息,Retain设为True。然后设备A又上线了,正常运行。但Broker上还保留着那条「设备A离线」的遗嘱消息。这时候新订阅者B上线,一订阅就收到一条旧消息:「设备A离线」。但实际上设备A在线啊!这不就乱了吗?

所以我的建议是:

  • 如果设备会重新上线,遗嘱消息的Retain设为False。或者设备上线时主动发布一条Retain消息覆盖掉。
  • 如果设备永久离线(比如传感器报废了),Retain设为True,让后来者知道这个设备已经没了。

我曾经在一个智能楼宇项目里,就是因为没处理好这个Retain标志,导致运维人员看到一堆「离线告警」,但实际上设备都在线。排查了半天才发现是遗嘱消息的Retain没清理。

3.3 完整代码示例

好了,理论说完了,咱们直接上代码。这是一个完整的客户端,设置了遗嘱消息:

import paho.mqtt.client as mqtt
import time
import json

# 遗嘱消息内容
will_payload = json.dumps({
    "device_id": "sensor_temp_01",
    "status": "offline",
    "timestamp": int(time.time()),
    "reason": "unexpected_disconnect"
})

# 创建客户端
client = mqtt.Client(client_id="sensor_temp_01")

# 设置遗嘱消息
client.will_set(
    topic="sensor/temp_01/status",
    payload=will_payload,
    qos=1,
    retain=False
)

# 连接Broker
client.connect("broker.emqx.io", 1883, 60)

# 启动网络循环
client.loop_start()

# 模拟正常运行
try:
    while True:
        # 正常发布温度数据
        temp_data = json.dumps({
            "device_id": "sensor_temp_01",
            "temperature": 25.3,
            "humidity": 60.1
        })
        client.publish("sensor/temp_01/data", temp_data, qos=1)
        print(f"发布温度数据: {temp_data}")
        time.sleep(5)
except KeyboardInterrupt:
    print("手动断开连接...")
    client.disconnect()
    client.loop_stop()

这段代码里,如果程序被Ctrl+C中断,或者网络突然断开,Broker就会自动发布遗嘱消息到 sensor/temp_01/status 主题。

3.4 验证遗嘱消息是否生效

光设置不行,你得验证。我一般开两个终端:

  1. 终端1:订阅端,订阅遗嘱主题,看看能不能收到消息。
  2. 终端2:发布端,运行上面的代码,然后强制断开(比如拔网线、或者直接关掉进程)。

订阅端的代码很简单:

import paho.mqtt.client as mqtt

def on_message(client, userdata, msg):
    print(f"收到遗嘱消息: {msg.payload.decode()}")

client = mqtt.Client()
client.on_message = on_message
client.connect("broker.emqx.io", 1883, 60)
client.subscribe("sensor/temp_01/status", qos=1)
client.loop_forever()

当你强制断开发布端时,订阅端应该会打印出类似这样的内容:

收到遗嘱消息: {"device_id": "sensor_temp_01", "status": "offline", "timestamp": 1691234567, "reason": "unexpected_disconnect"}

如果没收到,检查几点:

  • Broker是否支持遗嘱消息?大部分都支持,但有些受限环境可能关闭了。
  • QoS是否匹配?发布端和订阅端的QoS要一致,或者订阅端QoS不低于发布端。
  • 是否真的断开了?优雅退出(比如调用disconnect)不会触发遗嘱消息,只有非正常断开才会。

3.5 避坑指南

我曾经踩过的坑,你千万别再踩:

  • 遗嘱消息和普通消息主题混用:订阅端收到消息后,分不清是正常数据还是遗嘱。解决方案:分开主题。
  • Retain标志滥用:设备会重新上线的情况下,Retain设为True会导致旧遗嘱消息误导新订阅者。解决方案:设备上线时主动发布一条Retain=False的清除消息,或者干脆Retain=False。
  • 遗嘱消息太大:有些Broker对消息大小有限制,比如1024字节。遗嘱消息里别塞太多东西,够用就行。
  • 忘记设置遗嘱消息:代码写完了,测试时发现设备掉线了没任何通知。一查,哦,will_set()忘了调。嗯,我干过这事。

3.6 小结

这一章我们实战了遗嘱消息的配置。核心就四点:主题、消息、QoS、Retain。其中Retain是最容易搞混的,记住一句话:设备会回来就别保留,设备回不来就保留

下一章,我们会聊保留消息的实战应用。保留消息和遗嘱消息经常被放在一起比较,但其实它们是完全不同的东西。到时候我跟你讲讲它们各自的最佳实践。