加更2_这套PLC侧MQTTBroker_我是怎么从连不上掉线延迟一路修到稳定的

这套 Broker 不是一开始就稳。

真实过程更像这样:

编译先炸,连上再掉,订阅失败,发布断线,Retain 收不到,高频延迟,MQTT 5.0 闪烁,最后又发现老客户端发的是 MQIsdp

先给结论:

一个 PLC 侧 Broker 真正值钱的地方,不是"我写了个服务器"。

而是它经过了真实客户端、真实 PLC 网口、真实高频消息、真实故障变量的一轮轮打磨。


一、最开始的误区:以为会回 CONNACK 就差不多了

写 Client 时,我们关心的是:

  • CONNECT 怎么拼。
  • CONNACK 怎么读。
  • PUBLISH 怎么发。
  • ACK 怎么等。

刚开始写 Broker,很容易以为:

那我反过来解析 CONNECT,回一个 CONNACK,不就行了?

很快就会发现不是。

Broker 一上来就要面对:

问题 Client 侧有没有
多个客户端同端口接入 没有
每客户端独立槽位 没有
订阅表和路由 没有
Retain 表 没有
入站 / 出站 QoS 事务拆分 Client 侧简单得多
慢客户端隔离 没有
诊断快照 Broker 更需要

这就是服务器侧和客户端侧的分水岭。


二、第一轮先解决编译和工程规范

最早的错误很扎眼:

复制代码
未知的类型: FB_MqttClient
没有定义标识符 PRG_Test

这是因为示例入口还残留客户端工程对象。

后来又遇到大量:

复制代码
C0212: 应该采用 VAR, VAR_INPUT, VAR_OUTPUT 或 VAR_INOUT
C0135: 在声明部分不支持代码

根因是 ST 声明区和实现区边界没整理好。

还有一个规范坑:

复制代码
PROGRAM 中误加 {attribute 'hide_all_locals'}

最后固化的原则:

问题 修复
示例入口残留 Client 改成独立 PRG_MqttBrokerDemo
声明区混入实现代码 按 POU 结构重新拆
方法参数误用 明确输入、输出和内部状态边界
字符串长度不足 统一协议名、用户名、密码、ClientID 长度
PROGRAM 属性误用 PROGRAM 不加 hide_all_locals

三、第二轮解决连接闪烁

真实测试环境:

项目 内容
PLC 网口 192.168.20.100
PC 客户端 MQTTBox + 通信猫
端口 1883

一开始现象是:

复制代码
xTcpAcceptActive = TRUE 一下
aSnapshots[1].xUsed 置位一下又复位
uiAcceptFreeSlot 从 1 到 2 又回 1
客户端连接失败

这个坑最后不是"端口不能多客户端连接"。

而是:

NBS TCP 接入状态、Active 状态和 MQTT CONNECT 首包到达之间存在瞬态差异。

修复手段:

手段 价值
新连接首包等待窗口 避免 CONNECT 未到就释放
TCP Active 容忍窗口 避免底层瞬态 FALSE 误杀连接
槽位释放前快照 保留最后现场
错误码生命周期修复 避免 xError=FALSE 但错误码残留

这一轮之后,两个客户端都能稳定连上。


四、第三轮解决订阅和发布掉线

连接稳定后,新的问题来了:

订阅失败,一发布消息客户端就掉线。

这说明问题已经从 TCP 层进入 MQTT 协议层。

修复重点:

模块 修复内容
SUBSCRIBE 多 Topic 解析、Topic Filter 校验、SUBACK 返回
PUBLISH Topic / Payload 解析、QoS 处理
ACK PUBACK / PUBREC / PUBREL / PUBCOMP 闭环
Router 订阅表和通配符匹配
PacketId 目标连接重新分配,不能沿用发布者

这一轮完成后,MQTTBox 和通信猫可以互相发布订阅。


五、第四轮补上 Retain

用户测试发现:

客户端 A 对 CodeSys 发布一条勾选 Retain 的消息,客户端 B 重新订阅 CodeSys,没有收到任何消息。

这说明 Retain 没实现完整。

Retain 不是转发时带个标志位,而是要维护一张表:

补齐后,Retain 验收通过。

这也是产品亮点之一:

支持状态类主题最后值保留,新客户端订阅即可拿到当前状态。


六、第五轮解决性能延迟

稳定以后,开始对比 EMQX。

现象很明显:

  • EMQX 几十毫秒内回投。
  • 早期 PLC Broker 连续 10 条小消息出现尾部延迟。
  • 部分前发消息比后发消息还晚到。

根因最后落到:

一次 TCP_Write 只写一个 MQTT 帧。

修复手段:

手段 价值
Codec 支持 uiWriteOffset 多帧连续编码
M_PrepareNextTx 批量取帧 一次准备多条 MQTT 报文
单次 TCP_Write 合并多帧 降低写事务数量
投递队列 FIFO 修复 避免新消息插队
入站帧预算从 4 调到 8 提升高频互发实时性

优化后,现场反馈:

明显改善,满足当前要求。

这次优化真正落到代码里,不是"加大缓存"四个字,而是让构包函数从固定起点写入变成支持偏移写入:

复制代码
// 第二帧、第三帧不再覆盖 aTxBuf[0],而是从当前批量写偏移继续编码。
xBuildOk := fbCodec.M_BuildPublish(
    pPublish := ADR(stDelivery),
    pBuffer := ADR(aTxBuf),
    udiBufferSize := SIZEOF(aTxBuf),
    uiWriteOffset := uiWriteOffset,
    uiFrameLen => uiFrameLen);

IF xBuildOk THEN
    uiWriteOffset := uiWriteOffset + uiFrameLen;
    uiBatchFrameCount := uiBatchFrameCount + 1;
END_IF

七、第六轮补 MQTT 5.0 和 MQTT 3.1 兼容

最后收尾时又遇到:

客户端通过 5.0 标准无法连接服务器,接入变量不断闪。

先修 MQTT 5.0:

修复 说明
CONNECT Properties 跳过 读取属性长度
CONNACK 零属性响应 20 03 00 00 00

然后又发现:

复制代码
byLastConnectLevel = 100
eLastParseError = uiUnsupportedProtocol

最后定位:

客户端实际发送的是 MQTT 3.1 老协议名 MQIsdp,字符 d 被误读成协议级别 100。

修复:

协议 支持方式
MQTT 3.1 MQIsdp + level 3
MQTT 3.1.1 MQTT + level 4
MQTT 5.0 MQTT + level 5 基础兼容

到这里,连接兼容性基本收口。


八、最终沉淀出的产品亮点

亮点 价值
PLC 内置轻量 MQTT Broker 小型现场不必额外部署外部 Broker
多客户端同端口连接 支持 MQTTBox、通信猫、HMI、上位机同时连接
MQTT 3.1 / 3.1.1 / 5.0 基础兼容 覆盖新旧客户端
QoS0 / QoS1 / QoS2 支持完整主链路
Retain / Will / KeepAlive 满足工业状态、离线、死连接处理
固定资源模型 适合 PLC 扫描周期和内存约束
批量 TCP 写出 高频小消息实时性明显改善
诊断快照和历史 现场能快速定位问题

工程成熟度怎么看

这套 Broker 的演进,不应该只看"功能越来越多"。更稳的判断是看成熟度从哪里提升了。

阶段 表面成果 成熟度提升
编译通过 工程能导入 ST 对象边界和声明 / 实现区边界清楚
客户端连上 TCP 可接入 监听、接入、槽位生命周期清楚
能订阅发布 主链路跑通 Topic、路由、PacketId、QoS 作用域清楚
Retain 通过 新订阅有最后值 Topic 状态保存边界清楚
性能改善 高频小消息更顺 发送事务和 PLC 周期预算清楚
5.0 / 3.1 兼容 新旧客户端可接入 协议版本边界清楚

这也是我觉得这轮开发有价值的地方:它不是堆功能,而是一层层把模型边界补齐。

如果后续继续做,优先级也应该按这个思路排:先把诊断、测试矩阵和边界文档补强,再考虑 TLS、持久化或更完整 MQTT 5.0。否则功能看起来更大,现场维护反而会变重。


九、这一篇你最该记住的 6 句话

  1. Broker 不是靠想象写稳的,是靠真实客户端和真实现场变量打磨出来的。
  2. 连接稳定性要处理 TCP 接入、Active 闪烁和 CONNECT 首包时序。
  3. 订阅发布稳定性要处理 Topic Filter、路由、QoS 和 PacketId 作用域。
  4. Retain 是否实现,必须用新订阅补发来验证。
  5. 性能优化的关键不是盲目加队列,而是减少 TCP 写事务。
  6. MQTT 5.0 基础兼容和完整 5.0 支持要明确区分。

下篇预告

Broker 系列主线到这里完整收口。

后续如果继续展开,可以进入:

  • Broker 源码逐文件讲解
  • QoS2 状态机专项
  • Retain / Will 工业主题设计专项
  • PLC Broker 与 EMQX 混合架构专项

完整 ST 代码

下面这段来自 FB_MqttBroker.M_ServiceConnections.st。它就是这次复盘后最终收敛出来的主调度形态:连接槽位周期调度,认证、订阅、取消订阅、QoS 确认、PUBLISH、清理全部在同一个连接服务循环里闭环。

复制代码
FOR uiIndex := 1 TO GVL_MqttBroker.cnMaxClientSlots DO
	aConnections[uiIndex](
		bEnable := TRUE,
		uiSlot := uiIndex,
		hConnectionIn := 0,
		xConnectionActiveIn := aTcpAccept[uiIndex].xActive,
		udiNowMs := udiNowMs,
		stConnection => aConnectionStates[uiIndex]);

	IF aConnections[uiIndex].xActive THEN
		stMetrics.udiCurrentConnections := stMetrics.udiCurrentConnections + 1;
	END_IF

	IF aConnections[uiIndex].xMqttConnected THEN
		stMetrics.udiMqttSessions := stMetrics.udiMqttSessions + 1;

		IF NOT aConnectionStates[uiIndex].xAuthenticated THEN
			IF M_CheckAuth(
				sUsername := aConnectionStates[uiIndex].sUsername,
				sPassword := aConnectionStates[uiIndex].sPassword) THEN
				aConnections[uiIndex].M_SetAuthenticated();
				M_HandleDuplicateClientId(uiNewSlot := uiIndex);
				M_LogDiag(uiSlot := uiIndex, eError := E_MqttBrokerError.uiNoError, sMessage := 'MQTT session authenticated');
			ELSE
				stMetrics.udiAuthRejected := stMetrics.udiAuthRejected + 1;
				aConnections[uiIndex].M_RequestDisconnect();
				M_LogDiag(uiSlot := uiIndex, eError := E_MqttBrokerError.uiUnsupportedProtocol, sMessage := 'MQTT session authentication failed');
			END_IF
		END_IF
	END_IF

	IF aConnections[uiIndex].xSubscribeReady THEN
		stMetrics.udiSubscribeReceived := stMetrics.udiSubscribeReceived + 1;
		M_HandleSubscribe(uiSlot := uiIndex);
	END_IF

	IF aConnections[uiIndex].xUnsubReady THEN
		stMetrics.udiUnsubscribeReceived := stMetrics.udiUnsubscribeReceived + 1;
		M_HandleUnsubscribe(uiSlot := uiIndex);
	END_IF
END_FOR

高频延迟问题最后落在发送侧。M_PrepareNextTx 不再"一帧一写",而是把协议 ACK 和业务 PUBLISH 尽量合并到一次 TCP_Write。

复制代码
IF uiBatchFrameCount > 0 THEN
	stConnection.uiTxLength := uiTxOffset;
	uiLastTxFrameCount := uiBatchFrameCount;
	uiLastTxBytes := uiTxOffset;

	IF udiTxBatchCount < UDINT#16#FFFFFFFF THEN
		udiTxBatchCount := udiTxBatchCount + 1;
	END_IF

	IF (UDINT#16#FFFFFFFF - udiTxFrameCount) >= TO_UDINT(uiBatchFrameCount) THEN
		udiTxFrameCount := udiTxFrameCount + TO_UDINT(uiBatchFrameCount);
	ELSE
		udiTxFrameCount := UDINT#16#FFFFFFFF;
	END_IF

	bTcpWriteExecute := TRUE;
	bWriteBusy := TRUE;
	M_PrepareNextTx := TRUE;
END_IF

系列导航

  • 系列定位:加更 2
  • 上一篇:PLC Broker 要不要完整支持 MQTT 5.0?工业现场别被"新版本"带偏
  • 下一篇:Broker 源码逐文件讲解,待续

项目与资料

  • 开源项目名称:MqttBroker
  • 前置系列:MqttClient_V2_0
  • 核心关键词:项目复盘、连接稳定、Retain、性能优化、MQTT 5.0、MQIsdp

适合谁收藏

  • 想知道一个 PLC Broker 是怎么从不稳定修到稳定的人
  • 正在做工业通信标准件的人
  • 需要写产品手册、项目复盘、技术白皮书的人
  • 想把 MQTT 从协议概念真正落到 PLC 源码里的人
相关推荐
创世宇图5 小时前
Hermes AI 入门指南:从概念解析到实战应用
开源
昇腾CANN6 小时前
从一张查找表到 4GB/s:HiFloat8 Cast 算子的工程化之路
人工智能·开源·昇腾·cann
FIT2CLOUD飞致云6 小时前
里程碑丨MaxKB开源企业级智能体平台v2.10 LTS版本发布
人工智能·ai·开源·智能体·maxkb
提子拌饭1336 小时前
路径动画用鸿蒙PC用Electron框架实现流动效果技术详解
华为·electron·开源·harmonyos·鸿蒙
小橙讲编程6 小时前
一键给 AI Agent 装上「互联网眼睛」:Agent Reach 深度解析与实战指南
人工智能·开源·github·ai编程
X54先生(人文科技)8 小时前
《元创力》纪实录·桥段陶罐的测绘:当“表演性安全”吞噬星辰
人工智能·开源·开源协议·零知识证明
码途漫谈8 小时前
Compound Engineering:让每一次开发都给下一次铺路
开源·ai编程
江湖有缘9 小时前
五款优秀开源任务管理工具对比与分享
开源
KaiwuDB9 小时前
KaiwuDB 开源校园行扬州大学站 | 点亮开源成长之路
数据库·开源