第6篇_Retain_Will_KeepAlive_工业现场为什么不能只会转发PUBLISH

📅 2026/8/1 1:15:53 👁️ 阅读次数 📝 编程学习
第6篇_Retain_Will_KeepAlive_工业现场为什么不能只会转发PUBLISH

一个 Broker 会转发 PUBLISH,只能说明它能跑通“实时消息”。

但工业现场还会问:

  • 新客户端上线后,能不能马上拿到设备当前状态?
  • 设备异常掉线,其他客户端能不能收到通知?
  • 客户端死了不发包,Broker 会不会一直占着槽位?

这三个问题对应:

Retain、Will、KeepAlive。

先给结论:

只会转发 PUBLISH 的 Broker,只能算半个 Broker。
工业现场要的是状态可恢复、异常可感知、死连接可清理。

但这里的 Retain、Will、KeepAlive 仍然服务于小型工业现场,不等同于带数据库持久化、集群复制和离线会话恢复的大型 Broker。


一、Retain 解决“最后值”问题

假设 PLC 发布:

Topic: line1/plc/status Payload: RUN Retain: TRUE

后来 HMI 才上线并订阅:

line1/plc/status

如果 Broker 支持 Retain,HMI 应该立刻收到RUN
如果不支持,HMI 只能等下一次发布。

Retain 生命周期:

Retain 表不是消息队列。
一个 Topic 只保存最后一条保留消息。


二、Retain 的操作表

操作Broker 行为
Retain=0 发布正常路由,不写 Retain 表
Retain=1 且 Payload 非空写入或覆盖该 Topic 的 Retain
Retain=1 且 Payload 为空清除该 Topic 的 Retain
新订阅命中 RetainSUBACK 后补发保留消息
Topic Filter 命中多个 Retain按预算逐条补发

现场最常见误区:

客户端勾选 Retain 发布成功,不代表 Broker 已经实现 Retain。

真正的验收方法是:

  1. 客户端 A 对CodeSys发布一条 Retain 消息。
  2. 客户端 B 重新连接并订阅CodeSys
  3. B 应该立即收到这条消息。

三、Will 解决异常离线问题

Will 是客户端在 CONNECT 时提前交给 Broker 的遗嘱消息。

如果客户端异常断线,Broker 代替它发布 Will。

关键区别:

断开方式是否触发 Will
客户端发送 DISCONNECT
KeepAlive 超时
TCP 异常断开
协议错误导致关闭通常是

这在工业现场非常有用。比如:

device/plc01/online = offline

如果 Will 再配合 Retain,就能让新上线的 HMI 也看到“最后离线状态”。


四、KeepAlive 解决死连接问题

MQTT KeepAlive 不是“定时发心跳”这么简单。

它是客户端和 Broker 对连接存活的约定:

如果在 KeepAlive 时间内没有任何控制报文,客户端应该发 PINGREQ;Broker 收到后回 PINGRESP。

Broker 判断超时时通常按 1.5 倍宽限:

如果客户端声明KeepAlive = 60,Broker 不应该 60 秒一到就立刻杀。
工业网络有抖动,1.5 倍宽限更稳。


五、Retain / Will / KeepAlive 三者会汇合

这三个功能不是孤立的。

这也是为什么 Broker 不能只在连接 FB 里“关掉 TCP”就结束。

异常关闭可能会触发 Will。
Will 可能会更新 Retain。
Retain 更新后还要路由给订阅者。


六、ST 代码入口

代码入口作用
FB_MqttBrokerRouter.M_UpdateRetain写入、覆盖或清除 Retain 表
FB_MqttBrokerRouter.M_FindNextRetain新订阅时查找匹配 Retain
FB_MqttBroker.M_ServiceConnections检查连接状态、KeepAlive、异常清理
FB_MqttBroker.M_HandlePublish处理普通发布和 Retain 更新
ST_MqttBrokerRetainedMessageRetain 表项结构

Retain 更新逻辑大概是:

IF stPublish.xRetain THEN fbRouter.M_UpdateRetain( sTopicName := stPublish.sTopicName, pPayload := stPublish.pPayload, uiPayloadLen := stPublish.uiPayloadLen, eQoS := stPublish.eQoS); END_IF

KeepAlive 检查逻辑大概是:

udiTimeoutMs := TO_UDINT(uiKeepAliveSec) * 1500; IF (udiNowMs - udiLastActivityMs) > udiTimeoutMs THEN xNeedClose := TRUE; xNeedPublishWill := xWillEnabled; END_IF

七、现场排障表

现象优先检查可能原因
Retain 勾选后新订阅收不到uiRetainCountsLastRetainTopic没写 Retain 表或没做订阅补发
Retain 清不掉Payload 长度是否为 0清除规则未实现
正常断开也发 Will是否收到 DISCONNECT正常断开标志没设置
异常断开不发 WillCONNECT 是否解析 WillWill 字段没保存
客户端死了还占槽位udiLastActivityMsKeepAlive 未检查或宽限过大

模型边界与验证路径

Retain、Will、KeepAlive 看起来是三个功能点,往上看其实是会话生命周期模型。

功能解决的边界验证路径
RetainTopic 最后状态由谁保存新客户端订阅后是否立即收到最后值
Will异常离线由谁声明拔掉客户端或异常断开后是否发布 Will
KeepAlive死连接由谁清理停止客户端发送后是否按 1.5 倍宽限清理

结论分级:

结论可信度边界
Retain 必须通过新订阅补发验证high只看发布成功不够
正常 DISCONNECT 不应触发 Willhigh这是 MQTT 会话语义
KeepAlive 超时要结合扫描周期和网络抖动设置medium不同 PLC 任务周期和客户端行为会影响观测结果

这里先不要把 Retain 理解成持久化数据库。当前轻量 Broker 保存的是 PLC 内存中的最后值,断电、下载或工程重启后的行为要按实际工程配置重新验证。


八、这一篇你最该记住的 5 句话

  1. Retain 解决新订阅者获取主题最后值的问题。
  2. Will 解决异常离线通知问题。
  3. KeepAlive 解决死连接清理问题。
  4. 正常 DISCONNECT 不应该触发 Will。
  5. Retain、Will、KeepAlive 最后都会回到 Broker 的路由和连接清理调度里。

下篇预告

下一篇讲性能优化。

重点是:

为什么 PLC Broker 会有明显延迟,以及为什么从“一帧一写”改成“批量粘包写出”能明显改善实时性。


完整 ST 代码

本篇涉及的完整代码入口:

  • MqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerRouter.M_UpdateRetain.st
  • MqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerRouter.M_FindNextRetain.st
  • MqttBroker/Device/Application/POUs/FBs/FB_MqttBroker.M_ServiceConnections.st
  • MqttBroker/Device/Application/DUTs/ST_MqttBrokerRetainedMessage.st

系列导航

  • 系列定位:第 6 篇
  • 上一篇:PUBLISH 不是收到就转发:Broker 怎么处理 QoS、PacketId 和多客户端 fanout
  • 下一篇:为什么 PLC Broker 会有延迟?从 TCP_Write 一帧一写到批量粘包写出

项目与资料

  • 开源项目名称:MqttBroker
  • 前置系列:MqttClient_V2_0
  • 核心关键词:Retain、Will、KeepAlive、最后值、异常离线

适合谁收藏

  • Retain 发布成功但新订阅收不到的人
  • 想用 MQTT 表示设备在线离线状态的人
  • 正在处理客户端死连接的人
  • 想把 Broker 从“能转发”提升到“现场可用”的人