news 2026/5/19 9:01:09

第2篇_写MQTTBroker第一关不是PUBLISH_而是怎么让多个客户端稳稳连上同一个端口

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第2篇_写MQTTBroker第一关不是PUBLISH_而是怎么让多个客户端稳稳连上同一个端口

写 Broker 最容易一上来就盯着PUBLISH

但实际测试时,第一关通常不是消息转发,而是:

两个客户端都连192.168.20.100:1883,为什么一个都连不上,或者槽位刚置位就释放?

先给结论:

MQTT Broker 不是每个客户端开一个端口。
Broker 是一个监听端口,后面挂多个独立连接槽位。每个槽位必须有自己的 TCP 连接状态、MQTT 会话状态、收发缓冲和诊断快照。

这里讲的是 PLC 侧轻量 Broker 的连接模型,目标是让5~8个工业现场客户端稳定接入,不是把 PLC 写成互联网高并发服务器。


一、多个客户端连同一个端口是正常的

很多 PLC 工程师第一次写服务器时会本能怀疑:

两个客户端都连1883,会不会端口冲突?

不会。

冲突只发生在“多个服务端同时绑定同一个 IP:Port”。客户端连接同一个服务端端口是 TCP 服务器最基本的模型。

监听端口只有一个。
连接槽位可以有多个。


二、监听句柄和连接句柄不是一个东西

在 PLC 里写 Broker,必须把这两件事分开:

对象作用生命周期
NBS.TCP_Server打开监听端口Broker 运行期间长期存在
NBS.TCP_Connection接收一个具体客户端连接每个客户端独立存在
FB_MqttBrokerConnection管理 MQTT 会话跟客户端槽位绑定

一个典型的接入链路是:

这里最重要的一点:

不能把所有客户端都塞进同一个TCP_Connection实例里复用。

那样会把 TCP 句柄、读写状态、错误状态、MQTT 会话全部搅在一起。


三、客户端槽位应该保存什么

FB_MqttBrokerConnection不是“一个读写工具”,它是一个客户端会话容器。

字段类别典型内容
TCP 层连接句柄、Active 状态、读写错误、最近读写时间
MQTT 层ClientID、协议版本、KeepAlive、Clean Session
收发缓冲aRxBufaTxBuf、当前帧长度、批量写出长度
队列协议响应队列、普通投递队列、QoS 事务表
诊断xUsedeStateeLastErrorudiLastBytesRead

状态机可以简化成这样:


四、为什么 xTcpAcceptActive 会闪

现场真实现象是:

  • xTcpAcceptActive = TRUE一下。
  • aSnapshots[1].xUsed置位一下。
  • 马上又复位。
  • uiAcceptFreeSlot从 1 到 2 又回 1。
  • 客户端最后连接失败。

这个现象不能简单判断为“端口没开”。

更像是:

现象可能原因优先看什么
xTcpAcceptActive闪一下TCP 层已经看到新连接hLastAcceptHandle是否变化
槽位xUsed闪一下槽位分配成功但后续释放stLastCleanupSnapshot
udiLastBytesRead = 0CONNECT 首包还没到首包等待窗口
udiLastBytesRead > 0但断开MQTT 解析失败eLastParseErrorbyLastConnectLevel
xError = FALSE但错误码不为 0错误生命周期没清干净xErroreLastError联动

五、两个关键容忍窗口

真实 PLC 网络不是理想状态机。

客户端连接后,NBS 层可能先给出句柄和短暂 Active,但 MQTT CONNECT 首包不一定已经到达。如果这时严格判断“没读到包就断”,槽位就会一闪而过。

所以当前 Broker 固化了两个关键窗口:

常量作用
cnConnectFirstReadDelayMs := 20新连接接入后,允许 CONNECT 首包有一个短暂到达窗口
cnConnectionInactiveGraceMs := 3000TCP Active 短暂掉 FALSE 时,不立即误杀连接

这两个参数不是为了“拖慢”,而是为了不误杀。


六、错误码必须和错误标志同步

曾经出现过一个很误导人的现象:

xBrokerError = FALSE eBrokerError = uiClientSlotsFull

这显然不对。

如果xError = FALSE,当前错误码就应该回到uiNoError。否则在线监控会让人误以为槽位满了。

正确原则:

状态xErroreLastError
当前周期无错误FALSEuiNoError
当前周期槽位满TRUEuiClientSlotsFull
错误已恢复FALSEuiNoError
历史需要保留aDiagHistory不污染当前错误

这就是为什么 Broker 同时保留“当前错误”和“诊断历史”。


七、ST 代码入口

这一篇重点看这些入口:

代码入口作用
FB_MqttBroker.M_AcceptNewConnection从监听层接入新 TCP 客户端,并分配空闲槽位
FB_MqttBrokerConnection.M_Attach把 TCP 句柄挂到连接槽位
FB_MqttBrokerConnection单客户端连接状态机
ST_MqttBrokerConnectionSnapshot在线诊断快照

典型接入逻辑可以压缩成这几步:

// 1. 顶层只负责发现新连接和寻找空闲槽位。 uiFreeSlot := M_FindFreeConnectionSlot(); // 2. 找到槽位后,把 TCP 连接句柄交给对应连接 FB。 aConnections[uiFreeSlot].M_Attach( hConnection := hLastAcceptHandle, udiNowMs := udiNowMs); // 3. 后续读写和 MQTT 会话状态都由该槽位独立维护。 aConnections[uiFreeSlot]();

注意这里的设计边界:

顶层 Broker 不应该直接替连接槽位读写 MQTT 报文。
顶层负责调度,槽位负责会话。


八、现场排障表

现场现象第一优先级检查第二优先级检查常见修复方向
客户端连不上xRunninghListenHandle绑定 IP、端口、防火墙先用0.0.0.0验证监听
xTcpAcceptActiveuiAcceptFreeSlothLastAcceptHandle槽位快照看是否首包未到就释放
槽位一闪即没stLastCleanupSnapshotudiLastBytesRead增加首包等待和 Active 容忍
两客户端只能连一个cnMaxClientSlots每槽位 TCP 句柄是否独立不要复用同一连接实例
uiClientSlotsFulluiActiveSlotCount是否有僵尸槽位清理关闭态和诊断历史

模型边界与验证路径

这一篇本质上讲的是连接生命周期模型。

表面上看,问题是xTcpAcceptActive闪了一下。往上看一层,它其实是监听句柄、接入句柄、连接槽位和 MQTT 会话状态没有被拆清楚。

结论可信度依据验证路径
多客户端连接同一个1883是正常 TCP 服务端模型highTCP 服务端基本模型和现场双客户端测试两个客户端同时连接并观察uiActiveSlotCount
每个客户端必须有独立槽位high源码中的FB_MqttBrokerConnection槽位模型分别观察aSnapshots[1]aSnapshots[2]
Active 瞬态变化需要容忍窗口mediumNBS 现场表现和用户测试现象对比首包等待窗口开启前后的连接稳定性

这里不要把所有闪烁都直接定罪为网络问题。至少要先看三类对象:TCP 接入句柄、槽位快照、CONNECT 解析结果。缺任何一个,结论都只能算假设。


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

  1. 多个 MQTT 客户端连同一个1883端口是 Broker 的正常模型。
  2. 一个监听端口后面必须分配多个独立连接槽位。
  3. 每个客户端槽位都要独立保存 TCP 句柄、MQTT 会话、缓冲、队列和诊断。
  4. xTcpAcceptActive闪烁不等于端口没开,通常要看槽位生命周期。
  5. 新连接首包等待窗口和 Active 容忍窗口,是解决 PLC 真实网络瞬态误断的关键。
  6. 当前错误码和错误标志必须同步,历史错误应该放诊断历史里。

下篇预告

下一篇讲 CONNECT 解析。

重点不是“CONNECT 有哪些字段”,而是:

MQTT 3.1、3.1.1、5.0 的协议名和协议级别如果写死,为什么会让 Broker 反复断开。

里面会讲一个很真实的坑:byLastConnectLevel = 100不是协议等级 100,而是误读到了MQIsdp里的字符d


完整 ST 代码

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

  • MqttBroker/Device/Application/POUs/FBs/FB_MqttBroker.M_AcceptNewConnection.st
  • MqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerConnection.M_Attach.st
  • MqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerConnection.st
  • MqttBroker/Device/Application/DUTs/ST_MqttBrokerConnectionSnapshot.st

系列导航

  • 系列定位:第 2 篇
  • 上一篇:客户端写完了,为什么我还要在 PLC 里写一个 MQTT Broker?
  • 下一篇:CONNECT 解析别写死:MQTT 3.1、3.1.1、5.0 为什么会让 Broker 反复断开

项目与资料

  • 开源项目名称:MqttBroker
  • 前置系列:MqttClient_V2_0
  • 核心关键词:单端口多客户端、连接槽位、TCP 接入、诊断快照

适合谁收藏

  • 客户端连接 PLC Broker 时反复失败的人
  • 不确定多个客户端能否共用1883端口的人
  • 想把 PLC TCP Server 写成稳定服务端的人
  • 正在看xTcpAcceptActiveuiAcceptFreeSlotuiActiveSlotCount闪烁的人
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/19 8:59:31

ARM DS-5调试中IMG53警告的解决方案

1. 问题现象解析当使用ARM DS-5开发套件进行嵌入式调试时,开发者可能会在加载调试信息时遇到"WARNING(IMG53): No line debug information in the image"的警告提示。这个警告通常出现在以下两种典型场景中:在Eclipse集成开发环境中&#xff0…

作者头像 李华
网站建设 2026/5/19 8:59:30

突破60帧限制:genshin-fps-unlocker让你的《原神》体验更流畅!

突破60帧限制:genshin-fps-unlocker让你的《原神》体验更流畅! 【免费下载链接】genshin-fps-unlock unlocks the 60 fps cap 项目地址: https://gitcode.com/gh_mirrors/ge/genshin-fps-unlock 还在为《原神》60帧的限制感到困扰吗?g…

作者头像 李华
网站建设 2026/5/19 8:53:04

【USB3.0协议探秘】实战篇·三种复位事件的触发机制与链路状态变迁

1. 认识USB3.0的三种复位机制 刚接触USB3.0协议时,很多人会被各种复位类型绕晕。在实际开发中,我就遇到过因为混淆PowerOn Reset和Warm Reset导致设备无法正常初始化的情况。今天我们就来彻底搞懂这三种复位机制的区别和应用场景。 USB3.0协议定义了三种…

作者头像 李华
网站建设 2026/5/19 8:50:43

firerpa/lamda:代码优先的桌面自动化框架,重塑RPA开发体验

1. 项目概述:从“firerpa/lamda”看自动化流程的平民化革命最近在GitHub上闲逛,发现一个名为“firerpa/lamda”的项目,名字挺有意思,乍一看像是“Lambda”的变体,但拼写又有点不同。点进去一看,果然&#x…

作者头像 李华
网站建设 2026/5/19 8:50:02

猫抓插件:3分钟学会网页视频下载,告别观看限制的智能工具

猫抓插件:3分钟学会网页视频下载,告别观看限制的智能工具 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 还在为无法下载心…

作者头像 李华
网站建设 2026/5/19 8:48:16

ARM Thumb指令集内存屏障详解:DMB、DSB与ISB

1. ARM Thumb指令集中的内存屏障指令概述在嵌入式系统和移动设备开发中,ARM处理器占据着主导地位。作为RISC架构的代表,ARM提供了多种指令集以适应不同场景的需求,其中Thumb指令集以其高代码密度著称。在多核处理器和并发编程场景下&#xff…

作者头像 李华