一个 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
新订阅命中 Retain SUBACK 后补发保留消息
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_MqttBrokerRetainedMessage Retain 表项结构

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 正常断开标志没设置
异常断开不发 Will CONNECT 是否解析 Will Will 字段没保存
客户端死了还占槽位 udiLastActivityMs KeepAlive 未检查或宽限过大

模型边界与验证路径

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

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

结论分级:

结论 可信度 边界
Retain 必须通过新订阅补发验证 high 只看发布成功不够
正常 DISCONNECT 不应触发 Will high 这是 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 从“能转发”提升到“现场可用”的人
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐