第三章:遗嘱消息配置实战
好,咱们今天来点真格的。
上一章我们聊了遗嘱消息是什么、为什么需要它。这一章,我带你亲手配置一个。用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:订阅端,订阅遗嘱主题,看看能不能收到消息。
- 终端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是最容易搞混的,记住一句话:设备会回来就别保留,设备回不来就保留。
下一章,我们会聊保留消息的实战应用。保留消息和遗嘱消息经常被放在一起比较,但其实它们是完全不同的东西。到时候我跟你讲讲它们各自的最佳实践。