源码加更03_单连接槽位、TCP 字节流和发送队列
这一组源码加更只有一个目标:把 MqttBroker 的真实 ST 源码按工程阅读顺序讲完整。不是再补几段“看起来像源码”的片段,而是让读者能沿着源码对象理解这个 Broker 怎么组织、怎么运行、怎么排障。
适合谁收藏
已经读过 MqttBroker 主线教程,想继续看真实源码实现的工程师。 想学习 CodeSys ST 工程如何拆分 Broker、连接池、编解码、路由和 QoS 调度的人。 想把 MQTT Broker 移植到 PLC、边缘控制器或教学工程里的开发者。

先给结论
这一篇看一个客户端连接进入 Broker 后,如何在单连接槽位里完成收帧、验头、分发、排队和发送准备。
TCP 收到的是字节流,不是 MQTT 消息。现场很多 Broker 不稳定,本质不是 MQTT 理论没懂,而是完整帧、半包、粘包和发送队列边界没处理清楚。
这篇覆盖 14 个源码文件,合计约 1227 行 ST 代码。为了保持公开教程可读性,正文先讲源码阅读路径,再给完整源码。读代码时建议不要从第一个代码块一路机械读到底,而是按本篇的“读代码顺序”来抓主线。
从工程问题到代码职责
本篇源码覆盖表
FB_MqttBrokerConnection.M_AssignPacketId.st | ||
FB_MqttBrokerConnection.M_Attach.st | ||
FB_MqttBrokerConnection.M_ConsumeFrame.st | ||
FB_MqttBrokerConnection.M_EnqueueDelivery.st | ||
FB_MqttBrokerConnection.M_EnqueueProtocolAck.st | ||
FB_MqttBrokerConnection.M_EnqueueProtocolAckList.st | ||
FB_MqttBrokerConnection.M_HasCompleteFrame.st | ||
FB_MqttBrokerConnection.M_PrepareNextTx.st | ||
FB_MqttBrokerConnection.M_ProcessFrame.st | ||
FB_MqttBrokerConnection.M_RequestDisconnect.st | ||
FB_MqttBrokerConnection.M_Reset.st | ||
FB_MqttBrokerConnection.M_SetAuthenticated.st | ||
FB_MqttBrokerConnection.M_ValidateFixedHeader.st | ||
FB_MqttBrokerConnection.st |
推荐阅读顺序
先看连接槽位主体 FB_MqttBrokerConnection.st。再看 Attach/Reset/RequestDisconnect 等生命周期方法。 最后看 HasCompleteFrame、ConsumeFrame、ProcessFrame 和发送队列方法。
验证和排障边界
用多个客户端高频发布,重点观察是否丢帧、粘包误判或发送队列积压。 如果出现延迟或掉线,先查连接槽位,不要先怀疑主题路由。
本篇完整开源代码
下面代码来自对应 .st 源文件的连续完整内容。为方便公开阅读,只保留源码对象名,不放本机工程路径。
完整代码 01: FB_MqttBrokerConnection.M_AssignPacketId.st
/// =======================================================================/// 名称 : M_AssignPacketId/// 功能 : 为出站 QoS>0 发布帧分配 Packet Identifier/// 说明 : PacketId 按连接维度递增,0 为 MQTT 保留值,自动跳过。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_AssignPacketId : BOOLVAR_IN_OUTstPublish : ST_MqttBrokerPublishFrame; // 需要分配 PacketId 的出站发布帧END_VAR// === IMPLEMENTATION ===IF stPublish.eQoS = E_MqttQoS.byQoS0 THENM_AssignPacketId := TRUE;RETURN;END_IFIF stConnection.uiNextPacketId = 0 THENstConnection.uiNextPacketId := 1;END_IFstPublish.uiPacketId := stConnection.uiNextPacketId;stConnection.uiNextPacketId := stConnection.uiNextPacketId + 1;IF stConnection.uiNextPacketId = 0 THENstConnection.uiNextPacketId := 1;END_IFM_AssignPacketId := TRUE;
完整代码 02: FB_MqttBrokerConnection.M_Attach.st
/// =======================================================================/// 名称 : M_Attach/// 功能 : 把 TCP 连接句柄绑定到当前槽位/// 说明 : 监听层接受新连接后调用,连接进入等待 MQTT CONNECT 状态。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_Attach : BOOLVAR_INPUThConnection : NBS.CAA.HANDLE; // 新接入客户端的 TCP 连接句柄END_VAR// === IMPLEMENTATION ===M_Reset();IF hConnection = 0 THENeLastError := E_MqttBrokerError.uiTcpAcceptFailed;M_Attach := FALSE;RETURN;END_IFstConnection.xUsed := TRUE;stConnection.xMqttConnected := FALSE;stConnection.xCleanSession := TRUE;stConnection.xDisconnectRequested := FALSE;stConnection.xGracefulDisconnect := FALSE;stConnection.eState := E_MqttConnectionState.iWaitConnect;stConnection.eLastError := E_MqttBrokerError.uiNoError;stConnection.hConnection := hConnection;stConnection.uiSlot := uiSlot;stConnection.uiKeepAlive := GVL_MqttBroker.cnDefaultKeepAlive;stConnection.udiLastActivityMs := udiNowMs;stConnection.udiLastTcpActiveMs := udiNowMs;stConnection.udiConnectedAtMs := 0;stConnection.uiNextPacketId := 1;eLastError := E_MqttBrokerError.uiNoError;M_Attach := TRUE;
完整代码 03: FB_MqttBrokerConnection.M_ConsumeFrame.st
/// =======================================================================/// 名称 : M_ConsumeFrame/// 功能 : 从接收缓冲区移除已处理 MQTT 报文/// 说明 : 支持粘包场景,处理完一包后把剩余字节前移。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_ConsumeFrame : BOOLVAR_INPUTuiFrameLen : UINT; // 已处理的完整 MQTT 报文长度[byte]END_VARVARuiIndex : UINT; // 缓冲区前移复制索引[byte]uiRemain : UINT; // 消费后仍留在接收缓冲区的字节数[byte]END_VAR// === IMPLEMENTATION ===IF uiFrameLen = 0 THENM_ConsumeFrame := FALSE;RETURN;END_IFIF uiFrameLen > stConnection.uiRxLength THENeLastError := E_MqttBrokerError.uiProtocolMalformed;M_ConsumeFrame := FALSE;RETURN;END_IFuiRemain := stConnection.uiRxLength - uiFrameLen;IF uiRemain > 0 THENuiIndex := 0;WHILE uiIndex < uiRemain DOaRxBuf[uiIndex] := aRxBuf[uiFrameLen + uiIndex];uiIndex := uiIndex + 1;END_WHILEEND_IFstConnection.uiRxLength := uiRemain;M_ConsumeFrame := TRUE;
完整代码 04: FB_MqttBrokerConnection.M_EnqueueDelivery.st
/// =======================================================================/// 名称 : M_EnqueueDelivery/// 功能 : 将普通 PUBLISH 投递任务写入连接队列/// 说明 : 路由层匹配订阅后调用,本队列低于协议响应队列优先级。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_EnqueueDelivery : BOOLVAR_IN_OUTstDelivery : ST_MqttBrokerPublishFrame; // 需要投递给当前客户端的发布帧,QoS>0 且 PacketId 为 0 时会被分配编号END_VARVARuiIndex : UINT; // 普通投递队列扫描索引[1..cnDeliveryQueueSize]uiReadIndex : UINT; // 队列紧凑时读取旧队列项的索引[1..cnDeliveryQueueSize]uiWriteIndex : UINT; // 队列紧凑时写入新队列项的索引[1..cnDeliveryQueueSize]uiInsertIndex : UINT; // 本次新投递应写入的队尾索引,0 表示暂未找到可用尾部[1..cnDeliveryQueueSize]END_VAR// === IMPLEMENTATION ===IF NOT stDelivery.xValid THENM_EnqueueDelivery := FALSE;RETURN;END_IFIF stConnection.uiDeliveryQueueCount >= GVL_MqttBroker.cnDeliveryQueueHighWater THENIF stDelivery.eQoS = E_MqttQoS.byQoS0 THENeLastError := E_MqttBrokerError.uiQueueFull;IF udiTxQueueFullDropped < UDINT#16#FFFFFFFF THENudiTxQueueFullDropped := udiTxQueueFullDropped + 1;END_IFM_EnqueueDelivery := FALSE;RETURN;END_IFEND_IFIF stDelivery.uiTargetSlot <> uiSlot THENM_EnqueueDelivery := FALSE;RETURN;END_IFuiInsertIndex := 0;/// 队列发送端按低索引优先取帧,因此入队不能简单找第一个空洞。/// 否则低位旧帧刚被发走、高位旧帧尚未发完时,新帧会插到旧尾部前面,现场表现就是后发消息穿插到前发消息尾部之前。/// 这里先寻找当前有效队尾,把新消息追加到队尾之后;若尾部已顶到数组上限但中间存在空洞,则先做一次稳定紧凑。FOR uiIndex := 1 TO GVL_MqttBroker.cnDeliveryQueueSize DOIF aDeliveryQueue[uiIndex].xValid THENIF uiIndex < GVL_MqttBroker.cnDeliveryQueueSize THENuiInsertIndex := uiIndex + 1;ELSEuiInsertIndex := 0;END_IFEND_IFEND_FORIF stConnection.uiDeliveryQueueCount = 0 THENuiInsertIndex := 1;ELSIF uiInsertIndex = 0 THENuiWriteIndex := 1;FOR uiReadIndex := 1 TO GVL_MqttBroker.cnDeliveryQueueSize DOIF aDeliveryQueue[uiReadIndex].xValid THENIF uiReadIndex <> uiWriteIndex THENaDeliveryQueue[uiWriteIndex] := aDeliveryQueue[uiReadIndex];aDeliveryQueue[uiReadIndex].xValid := FALSE;END_IFIF uiWriteIndex < GVL_MqttBroker.cnDeliveryQueueSize THENuiWriteIndex := uiWriteIndex + 1;END_IFEND_IFEND_FORIF stConnection.uiDeliveryQueueCount < GVL_MqttBroker.cnDeliveryQueueSize THENuiInsertIndex := stConnection.uiDeliveryQueueCount + 1;END_IFEND_IFIF (uiInsertIndex >= 1) AND (uiInsertIndex <= GVL_MqttBroker.cnDeliveryQueueSize) THENIF stDelivery.eQoS = E_MqttQoS.byQoS0 THENstDelivery.uiPacketId := 0;ELSIF stDelivery.uiPacketId = 0 THEN// Packet Identifier 的作用域是单条 MQTT 客户端连接。// 入站 PUBLISH 的 PacketId 属于发布者连接,不能直接转发给订阅者;// 首次投递给订阅者时必须在自己的连接内重新分配 PacketId,避免 QoS1/QoS2 确认链路冲突。M_AssignPacketId(stPublish := stDelivery);ELSE// QoS 重发必须沿用首次投递的 PacketId。// 如果重发时重新分配 PacketId,客户端会 PUBACK 新编号,而事务表仍等待旧编号,// 结果就是每 2 秒重复投递一次,达到最大重试次数后 Broker 主动断开客户端。END_IFaDeliveryQueue[uiInsertIndex] := stDelivery;stConnection.uiDeliveryQueueCount := stConnection.uiDeliveryQueueCount + 1;IF stConnection.uiDeliveryQueueCount > uiMaxDeliveryQueueCountSeen THENuiMaxDeliveryQueueCountSeen := stConnection.uiDeliveryQueueCount;END_IFM_EnqueueDelivery := TRUE;RETURN;END_IFeLastError := E_MqttBrokerError.uiQueueFull;IF stDelivery.eQoS = E_MqttQoS.byQoS0 THENIF udiTxQueueFullDropped < UDINT#16#FFFFFFFF THENudiTxQueueFullDropped := udiTxQueueFullDropped + 1;END_IFEND_IFM_EnqueueDelivery := FALSE;
完整代码 05: FB_MqttBrokerConnection.M_EnqueueProtocolAck.st
/// =======================================================================/// 名称 : M_EnqueueProtocolAck/// 功能 : 将协议响应写入优先队列/// 说明 : 协议响应优先于普通 PUBLISH 投递,避免 ACK 被慢业务流量阻塞。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_EnqueueProtocolAck : BOOLVAR_INPUTePacketType : E_MqttPacketType; // 需要发送的 MQTT 协议响应类型uiPacketId : UINT; // 响应关联 Packet Identifier,无需时为 0byReturnCode : BYTE; // CONNACK/SUBACK 返回码,其他响应为 0END_VARVARuiIndex : UINT; // 协议响应队列扫描索引END_VAR// === IMPLEMENTATION ===FOR uiIndex := 1 TO GVL_MqttBroker.cnProtocolQueueSize DOIF NOT aProtocolQueue[uiIndex].xUsed THENaProtocolQueue[uiIndex].xUsed := TRUE;aProtocolQueue[uiIndex].ePacketType := ePacketType;aProtocolQueue[uiIndex].uiPacketId := uiPacketId;aProtocolQueue[uiIndex].byReturnCode := byReturnCode;aProtocolQueue[uiIndex].uiReturnCount := 0;stConnection.uiProtocolQueueCount := stConnection.uiProtocolQueueCount + 1;IF stConnection.uiProtocolQueueCount > uiMaxProtocolQueueCountSeen THENuiMaxProtocolQueueCountSeen := stConnection.uiProtocolQueueCount;END_IFM_EnqueueProtocolAck := TRUE;RETURN;END_IFEND_FOReLastError := E_MqttBrokerError.uiQueueFull;stConnection.xDisconnectRequested := TRUE;M_EnqueueProtocolAck := FALSE;
完整代码 06: FB_MqttBrokerConnection.M_EnqueueProtocolAckList.st
/// =======================================================================/// 名称 : M_EnqueueProtocolAckList/// 功能 : 将带多返回码的协议响应写入优先队列/// 说明 : 主要用于多 Topic SUBSCRIBE 的 SUBACK,保证每个 Topic 都有独立返回码。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_EnqueueProtocolAckList : BOOLVAR_INPUTePacketType : E_MqttPacketType; // 需要发送的 MQTT 协议响应类型,当前主要为 SUBACKuiPacketId : UINT; // 响应关联 Packet IdentifieruiReturnCount : UINT; // 多 Topic 返回码数量[条]END_VARVAR_IN_OUTaReturnCodes : ARRAY[*] OF BYTE; // 多 Topic 返回码数组,每个元素对应一个 Topic FilterEND_VARVARuiIndex : UINT; // 协议响应队列扫描索引uiCodeIndex : UINT; // 返回码复制索引[1..cnMaxTopicItemsPerPacket]END_VAR// === IMPLEMENTATION ===IF uiReturnCount = 0 THENM_EnqueueProtocolAckList := FALSE;RETURN;END_IFIF uiReturnCount > GVL_MqttBroker.cnMaxTopicItemsPerPacket THENM_EnqueueProtocolAckList := FALSE;RETURN;END_IFFOR uiIndex := 1 TO GVL_MqttBroker.cnProtocolQueueSize DOIF NOT aProtocolQueue[uiIndex].xUsed THENaProtocolQueue[uiIndex].xUsed := TRUE;aProtocolQueue[uiIndex].ePacketType := ePacketType;aProtocolQueue[uiIndex].uiPacketId := uiPacketId;aProtocolQueue[uiIndex].byReturnCode := 0;aProtocolQueue[uiIndex].uiReturnCount := uiReturnCount;FOR uiCodeIndex := 1 TO uiReturnCount DOaProtocolQueue[uiIndex].aReturnCodes[uiCodeIndex] := aReturnCodes[uiCodeIndex];END_FORstConnection.uiProtocolQueueCount := stConnection.uiProtocolQueueCount + 1;IF stConnection.uiProtocolQueueCount > uiMaxProtocolQueueCountSeen THENuiMaxProtocolQueueCountSeen := stConnection.uiProtocolQueueCount;END_IFM_EnqueueProtocolAckList := TRUE;RETURN;END_IFEND_FOReLastError := E_MqttBrokerError.uiQueueFull;stConnection.xDisconnectRequested := TRUE;M_EnqueueProtocolAckList := FALSE;
完整代码 07: FB_MqttBrokerConnection.M_HasCompleteFrame.st
/// =======================================================================/// 名称 : M_HasCompleteFrame/// 功能 : 判断接收缓冲区是否已有完整 MQTT 报文/// 说明 : 先解码 Remaining Length,再判断固定头、变长长度和载荷是否完整。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_HasCompleteFrame : BOOLVAR_OUTPUTuiFrameLen : UINT; // 完整 MQTT 报文总长度[byte]uiBodyOffset : UINT; // 可变头起始偏移[byte]udiRemainingLen : UDINT; // Remaining Length 解码值[byte]xNeedMore : BOOL; // TRUE 表示当前缓冲区还不足以形成完整报文END_VARVARuiRemainingBytes : UINT; // Remaining Length 字段实际占用字节数[byte]END_VAR// === IMPLEMENTATION ===uiFrameLen := 0;uiBodyOffset := 0;udiRemainingLen := 0;xNeedMore := FALSE;IF stConnection.uiRxLength < 2 THENxNeedMore := TRUE;M_HasCompleteFrame := FALSE;RETURN;END_IFIF NOT F_MqttDecodeRemainingLength(aBuffer := aRxBuf,uiStartIndex := 1,uiBufferLen := stConnection.uiRxLength,udiValue => udiRemainingLen,uiBytesUsed => uiRemainingBytes,xNeedMore => xNeedMore) THENM_HasCompleteFrame := FALSE;RETURN;END_IFIF udiRemainingLen > GVL_MqttBroker.cnMqttRemainingLengthMax THENeLastError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;M_HasCompleteFrame := FALSE;RETURN;END_IFuiBodyOffset := 1 + uiRemainingBytes;IF (TO_UDINT(uiBodyOffset) + udiRemainingLen) > TO_UDINT(GVL_MqttBroker.cnRxBufferSize) THENeLastError := E_MqttBrokerError.uiPacketTooLarge;stConnection.xDisconnectRequested := TRUE;M_HasCompleteFrame := FALSE;RETURN;END_IFuiFrameLen := uiBodyOffset + TO_UINT(udiRemainingLen);IF stConnection.uiRxLength < uiFrameLen THENxNeedMore := TRUE;M_HasCompleteFrame := FALSE;RETURN;END_IFM_HasCompleteFrame := TRUE;
完整代码 08: FB_MqttBrokerConnection.M_PrepareNextTx.st
/// =======================================================================/// 名称 : M_PrepareNextTx/// 功能 : 从发送队列批量取出 MQTT 报文并编码到发送缓冲区/// 说明 : 一次 TCP_Write 可以携带多个 MQTT 控制报文帧,显著降低高频小消息的尾部排队延迟。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.1/// ======================================================================={attribute 'hide_all_locals'}METHOD M_PrepareNextTx : BOOLVARuiIndex : UINT; // 队列扫描索引,协议响应队列和普通投递队列共用[1..容量]uiBuiltLen : UINT; // 当前单帧编码器构建出的 MQTT 报文长度[byte]uiTxOffset : UINT; // 本批次下一帧应写入 aTxBuf 的起始偏移[byte]uiBatchFrameCount : UINT; // 本批次已经成功合并到 aTxBuf 的 MQTT 帧数量[帧]stDelivery : ST_MqttBrokerPublishFrame; // 从普通投递队列取出的发布帧副本,避免编码过程中直接改队列项xStopBatch : BOOL; // TRUE 表示本批次已达到帧数或缓冲区预算,应停止继续取队列END_VAR// === IMPLEMENTATION ===M_PrepareNextTx := FALSE;IF NOT stConnection.xUsed THENRETURN;END_IFuiTxOffset := 0;uiBatchFrameCount := 0;xStopBatch := FALSE;/// =======================================================================/// 协议响应优先批量编码/// =======================================================================FOR uiIndex := 1 TO GVL_MqttBroker.cnProtocolQueueSize DOIF xStopBatch THENEXIT;END_IFIF aProtocolQueue[uiIndex].xUsed THENIF uiBatchFrameCount >= GVL_MqttBroker.cnMaxTxFramesPerWrite THENxStopBatch := TRUE;ELSIF (TO_UDINT(uiTxOffset) + TO_UDINT(GVL_MqttBroker.cnMinTxBufferFree)) >= SIZEOF(aTxBuf) THENxStopBatch := TRUE;ELSIF fbCodec.M_BuildSimpleAck(aBuffer := aTxBuf,aReturnCodes := aProtocolQueue[uiIndex].aReturnCodes,ePacketType := aProtocolQueue[uiIndex].ePacketType,byProtocolLevel := stConnection.byProtocolLevel,uiPacketId := aProtocolQueue[uiIndex].uiPacketId,byReturnCode := aProtocolQueue[uiIndex].byReturnCode,uiReturnCount := aProtocolQueue[uiIndex].uiReturnCount,uiWriteOffset := uiTxOffset,udiBufferSize := SIZEOF(aTxBuf),uiFrameLen => uiBuiltLen) THENaProtocolQueue[uiIndex].xUsed := FALSE;aProtocolQueue[uiIndex].ePacketType := E_MqttPacketType.byReserved;aProtocolQueue[uiIndex].uiPacketId := 0;aProtocolQueue[uiIndex].byReturnCode := 0;aProtocolQueue[uiIndex].uiReturnCount := 0;IF stConnection.uiProtocolQueueCount > 0 THENstConnection.uiProtocolQueueCount := stConnection.uiProtocolQueueCount - 1;END_IFuiTxOffset := uiTxOffset + uiBuiltLen;uiBatchFrameCount := uiBatchFrameCount + 1;ELSIF uiBatchFrameCount > 0 THEN// 已经成功编码过前序帧时,构建失败通常表示剩余缓冲不足。// 此时不清空前序帧,先发送已经打包好的内容,下个周期再处理当前队列项。xStopBatch := TRUE;ELSE// 第一帧就无法构建,说明队列项本身异常或配置缓冲区无法容纳该协议响应。// 释放该协议响应,避免无效队头永久阻塞后续 ACK/PUBLISH。aProtocolQueue[uiIndex].xUsed := FALSE;aProtocolQueue[uiIndex].ePacketType := E_MqttPacketType.byReserved;aProtocolQueue[uiIndex].uiPacketId := 0;aProtocolQueue[uiIndex].byReturnCode := 0;aProtocolQueue[uiIndex].uiReturnCount := 0;IF stConnection.uiProtocolQueueCount > 0 THENstConnection.uiProtocolQueueCount := stConnection.uiProtocolQueueCount - 1;END_IFeLastError := E_MqttBrokerError.uiPacketTooLarge;END_IFEND_IFEND_FOR/// =======================================================================/// 普通 PUBLISH 投递批量编码/// =======================================================================IF NOT xStopBatch THENFOR uiIndex := 1 TO GVL_MqttBroker.cnDeliveryQueueSize DOIF xStopBatch THENEXIT;END_IFIF aDeliveryQueue[uiIndex].xValid THENIF uiBatchFrameCount >= GVL_MqttBroker.cnMaxTxFramesPerWrite THENxStopBatch := TRUE;ELSIF (TO_UDINT(uiTxOffset) + TO_UDINT(GVL_MqttBroker.cnMinTxBufferFree)) >= SIZEOF(aTxBuf) THENxStopBatch := TRUE;ELSEstDelivery := aDeliveryQueue[uiIndex];IF fbCodec.M_BuildPublish(aBuffer := aTxBuf,stPublish := stDelivery,byProtocolLevel := stConnection.byProtocolLevel,uiWriteOffset := uiTxOffset,udiBufferSize := SIZEOF(aTxBuf),uiFrameLen => uiBuiltLen) THENaDeliveryQueue[uiIndex].xValid := FALSE;IF stConnection.uiDeliveryQueueCount > 0 THENstConnection.uiDeliveryQueueCount := stConnection.uiDeliveryQueueCount - 1;END_IFuiTxOffset := uiTxOffset + uiBuiltLen;uiBatchFrameCount := uiBatchFrameCount + 1;ELSIF uiBatchFrameCount > 0 THEN// 当前 PUBLISH 放不进本批次剩余缓冲时,先发已编码内容,保持队列顺序不变。xStopBatch := TRUE;ELSE// 第一帧就无法构建,多数是 Topic/Payload 超限或队列项被破坏。// 释放异常项,防止慢性死锁;QoS>0 事务最终会由重试/超时路径处理。aDeliveryQueue[uiIndex].xValid := FALSE;IF stConnection.uiDeliveryQueueCount > 0 THENstConnection.uiDeliveryQueueCount := stConnection.uiDeliveryQueueCount - 1;END_IFeLastError := E_MqttBrokerError.uiPacketTooLarge;END_IFEND_IFEND_IFEND_FOREND_IFIF uiBatchFrameCount > 0 THENstConnection.uiTxLength := uiTxOffset;uiLastTxFrameCount := uiBatchFrameCount;uiLastTxBytes := uiTxOffset;IF udiTxBatchCount < UDINT#16#FFFFFFFF THENudiTxBatchCount := udiTxBatchCount + 1;END_IFIF (UDINT#16#FFFFFFFF - udiTxFrameCount) >= TO_UDINT(uiBatchFrameCount) THENudiTxFrameCount := udiTxFrameCount + TO_UDINT(uiBatchFrameCount);ELSEudiTxFrameCount := UDINT#16#FFFFFFFF;END_IFbTcpWriteExecute := TRUE;bWriteBusy := TRUE;M_PrepareNextTx := TRUE;END_IF
完整代码 09: FB_MqttBrokerConnection.M_ProcessFrame.st
/// =======================================================================/// 名称 : M_ProcessFrame/// 功能 : 处理一条完整 MQTT 入站报文/// 说明 : 连接槽位只解析协议并产出事件,订阅表、路由和事务由顶层协调。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_ProcessFrame : BOOLVAR_INPUTuiFrameLen : UINT; // 当前完整 MQTT 报文总长度[byte]uiBodyOffset : UINT; // 当前 MQTT 报文可变头起始偏移[byte]END_VARVARbyPacketClass : BYTE; // 固定头高 4 位控制报文类型,不含低位标志byReturnCode : BYTE; // CONNACK/SUBACK 返回码END_VAR// === IMPLEMENTATION ===byPacketClass := aRxBuf[0] AND 16#F0;byReturnCode := 0;byLastPacketType := byPacketClass;eLastParseError := E_MqttBrokerError.uiNoError;IF NOT M_ValidateFixedHeader(uiFrameLen := uiFrameLen, uiBodyOffset := uiBodyOffset) THENeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;M_ProcessFrame := FALSE;RETURN;END_IFCASE byPacketClass OF16#10:xLastConnectParsed := FALSE;IF (uiBodyOffset + 2) >= uiFrameLen THENbyLastConnectLevel := 0;ELSIF ((TO_UINT(aRxBuf[uiBodyOffset]) * 256 + TO_UINT(aRxBuf[uiBodyOffset + 1])) = 4)AND ((uiBodyOffset + 6) < uiFrameLen) THENbyLastConnectLevel := aRxBuf[uiBodyOffset + 6];ELSIF ((TO_UINT(aRxBuf[uiBodyOffset]) * 256 + TO_UINT(aRxBuf[uiBodyOffset + 1])) = 6)AND ((uiBodyOffset + 8) < uiFrameLen) THENbyLastConnectLevel := aRxBuf[uiBodyOffset + 8];ELSEbyLastConnectLevel := 0;END_IFIF stConnection.xMqttConnected THENeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;ELSIF fbCodec.M_ParseConnect(aBuffer := aRxBuf,stConnection := stConnection,uiBodyOffset := uiBodyOffset,uiFrameLen := uiFrameLen,eError => eParseError) THENstConnection.xMqttConnected := TRUE;stConnection.eState := E_MqttConnectionState.iConnected;stConnection.udiConnectedAtMs := udiNowMs;stConnection.udiLastActivityMs := udiNowMs;xMqttConnected := TRUE;xLastConnectParsed := TRUE;M_EnqueueProtocolAck(ePacketType := E_MqttPacketType.byConnAck,uiPacketId := 0,byReturnCode := 0);ELSEeLastError := eParseError;eLastParseError := eParseError;IF stConnection.byProtocolLevel = GVL_MqttBroker.cnMqttProtocolLevel5 THEN// MQTT 5.0 CONNACK 使用 Reason Code,不再使用 3.1.1 的 1/2/3 返回码。// 若这里继续返回 1/2/3,部分 5.0 客户端会认为 Broker 响应非法而显示为无原因断开。IF eParseError = E_MqttBrokerError.uiUnsupportedProtocol THENbyReturnCode := 16#84;ELSIF eParseError = E_MqttBrokerError.uiInvalidClientId THENbyReturnCode := 16#85;ELSIF eParseError = E_MqttBrokerError.uiProtocolMalformed THENbyReturnCode := 16#81;ELSEbyReturnCode := 16#80;END_IFELSEbyReturnCode := 1;IF eParseError = E_MqttBrokerError.uiUnsupportedProtocol THENbyReturnCode := 1;ELSIF eParseError = E_MqttBrokerError.uiInvalidClientId THENbyReturnCode := 2;ELSEbyReturnCode := 3;END_IFEND_IFM_EnqueueProtocolAck(ePacketType := E_MqttPacketType.byConnAck,uiPacketId := 0,byReturnCode := byReturnCode);stConnection.xDisconnectRequested := TRUE;END_IF16#30:IF NOT stConnection.xMqttConnected THENeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;ELSIF fbCodec.M_ParsePublish(aBuffer := aRxBuf,stPublish := stPublishOut,uiSourceSlot := uiSlot,byProtocolLevel := stConnection.byProtocolLevel,uiBodyOffset := uiBodyOffset,uiFrameLen := uiFrameLen,eError => eParseError) THENxPublishReady := TRUE;ELSEeLastError := eParseError;eLastParseError := eParseError;stConnection.xDisconnectRequested := TRUE;END_IF16#40:IF (uiBodyOffset + 1) < uiFrameLen THENuiPubAckPacketId := TO_UINT(aRxBuf[uiBodyOffset]) * 256 + TO_UINT(aRxBuf[uiBodyOffset + 1]);xPubAckReady := TRUE;ELSEeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;END_IF16#50:IF (uiBodyOffset + 1) < uiFrameLen THENuiPubRecPacketId := TO_UINT(aRxBuf[uiBodyOffset]) * 256 + TO_UINT(aRxBuf[uiBodyOffset + 1]);xPubRecReady := TRUE;ELSEeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;END_IF16#60:IF (uiBodyOffset + 1) < uiFrameLen THENuiPubRelPacketId := TO_UINT(aRxBuf[uiBodyOffset]) * 256 + TO_UINT(aRxBuf[uiBodyOffset + 1]);xPubRelReady := TRUE;ELSEeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;END_IF16#70:IF (uiBodyOffset + 1) < uiFrameLen THENuiPubCompPacketId := TO_UINT(aRxBuf[uiBodyOffset]) * 256 + TO_UINT(aRxBuf[uiBodyOffset + 1]);xPubCompReady := TRUE;ELSEeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;END_IF16#80:IF fbCodec.M_ParseSubscribe(aBuffer := aRxBuf,aTopicItems := aSubItems,byProtocolLevel := stConnection.byProtocolLevel,uiBodyOffset := uiBodyOffset,uiFrameLen := uiFrameLen,uiPacketId => uiSubPacketId,uiItemCount => uiSubItemCount,eError => eParseError) THENIF uiSubItemCount > 0 THENsSubTopicFilter := aSubItems[1].sTopicFilter;eSubQoS := aSubItems[1].eQoS;END_IFxSubscribeReady := TRUE;ELSEeLastError := eParseError;eLastParseError := eParseError;M_EnqueueProtocolAck(ePacketType := E_MqttPacketType.bySubAck,uiPacketId := uiSubPacketId,byReturnCode := 16#80);END_IF16#A0:IF fbCodec.M_ParseUnsubscribe(aBuffer := aRxBuf,aTopicItems := aUnsubItems,byProtocolLevel := stConnection.byProtocolLevel,uiBodyOffset := uiBodyOffset,uiFrameLen := uiFrameLen,uiPacketId => uiUnsubPacketId,uiItemCount => uiUnsubItemCount,eError => eParseError) THENIF uiUnsubItemCount > 0 THENsUnsubTopicFilter := aUnsubItems[1].sTopicFilter;END_IFxUnsubReady := TRUE;ELSEeLastError := eParseError;eLastParseError := eParseError;stConnection.xDisconnectRequested := TRUE;END_IF16#C0:M_EnqueueProtocolAck(ePacketType := E_MqttPacketType.byPingResp,uiPacketId := 0,byReturnCode := 0);16#E0:stConnection.xGracefulDisconnect := TRUE;stConnection.xDisconnectRequested := TRUE;ELSEeLastError := E_MqttBrokerError.uiProtocolMalformed;eLastParseError := E_MqttBrokerError.uiProtocolMalformed;stConnection.xDisconnectRequested := TRUE;END_CASEM_ProcessFrame := TRUE;
完整代码 10: FB_MqttBrokerConnection.M_RequestDisconnect.st
/// =======================================================================/// 名称 : M_RequestDisconnect/// 功能 : 请求连接槽位断开/// 说明 : 父级 Broker 不能直接写 FB 输出结构成员,因此通过本方法设置释放请求。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_RequestDisconnect : BOOL// === IMPLEMENTATION ===stConnection.xDisconnectRequested := TRUE;M_RequestDisconnect := TRUE;
完整代码 11: FB_MqttBrokerConnection.M_Reset.st
/// =======================================================================/// 名称 : M_Reset/// 功能 : 复位连接槽位全部运行态/// 说明 : 槽位禁用或释放时调用,清空缓冲区、队列、事件和会话标志。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_Reset : BOOLVARuiIndex : UINT; // 队列清理索引END_VAR// === IMPLEMENTATION ===stConnection.xUsed := FALSE;stConnection.xMqttConnected := FALSE;stConnection.xCleanSession := TRUE;stConnection.xDisconnectRequested := FALSE;stConnection.xGracefulDisconnect := FALSE;stConnection.eState := E_MqttConnectionState.iFree;stConnection.eLastError := E_MqttBrokerError.uiNoError;stConnection.hConnection := 0;stConnection.uiSlot := uiSlot;stConnection.byProtocolLevel := GVL_MqttBroker.cnMqttProtocolLevel311;stConnection.uiKeepAlive := GVL_MqttBroker.cnDefaultKeepAlive;stConnection.udiLastActivityMs := 0;stConnection.udiLastTcpActiveMs := 0;stConnection.udiConnectedAtMs := 0;stConnection.uiRxLength := 0;stConnection.uiTxLength := 0;stConnection.uiNextPacketId := 1;stConnection.uiProtocolQueueCount := 0;stConnection.uiDeliveryQueueCount := 0;stConnection.sClientId := '';stConnection.sUsername := '';stConnection.sPassword := '';stConnection.xAuthenticated := FALSE;stConnection.xWillFlag := FALSE;stConnection.xWillRetain := FALSE;stConnection.eWillQoS := E_MqttQoS.byQoS0;stConnection.sWillTopic := '';stConnection.sWillPayload := '';FOR uiIndex := 1 TO GVL_MqttBroker.cnProtocolQueueSize DOaProtocolQueue[uiIndex].xUsed := FALSE;aProtocolQueue[uiIndex].ePacketType := E_MqttPacketType.byReserved;aProtocolQueue[uiIndex].uiPacketId := 0;aProtocolQueue[uiIndex].byReturnCode := 0;aProtocolQueue[uiIndex].uiReturnCount := 0;END_FORFOR uiIndex := 1 TO GVL_MqttBroker.cnDeliveryQueueSize DOaDeliveryQueue[uiIndex].xValid := FALSE;END_FORxActive := FALSE;xMqttConnected := FALSE;bTcpReadEnable := FALSE;bTcpWriteExecute := FALSE;bWriteBusy := FALSE;bWriteDoneLatched := FALSE;uiTcpReadErrorCount := 0;uiTcpReadErrorCountOut := 0;udiBytesRead := 0;uiFrameLen := 0;uiBodyOffset := 0;udiRemainingLen := 0;uiRemainingBytes := 0;xNeedMore := FALSE;xLastTcpReadError := FALSE;udiLastBytesRead := 0;udiLastNonZeroBytesRead := 0;uiLastTxFrameCount := 0;uiLastTxBytes := 0;udiTxBatchCount := 0;udiTxFrameCount := 0;uiMaxDeliveryQueueCountSeen := 0;uiMaxProtocolQueueCountSeen := 0;udiTxQueueFullDropped := 0;uiLastFrameLen := 0;byLastPacketType := 0;byLastConnectLevel := 0;xLastConnectParsed := FALSE;eLastParseError := E_MqttBrokerError.uiNoError;xTcpReadError := FALSE;xTcpWriteError := FALSE;xWriteBusy := FALSE;xWriteExecute := FALSE;xConnectionActive := FALSE;eLastError := E_MqttBrokerError.uiNoError;M_Reset := TRUE;
完整代码 12: FB_MqttBrokerConnection.M_SetAuthenticated.st
/// =======================================================================/// 名称 : M_SetAuthenticated/// 功能 : 标记当前 MQTT 会话已通过认证/// 说明 : 父级 Broker 完成基础认证后调用,连接槽位内部保存认证状态。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_SetAuthenticated : BOOL// === IMPLEMENTATION ===stConnection.xAuthenticated := TRUE;M_SetAuthenticated := TRUE;
完整代码 13: FB_MqttBrokerConnection.M_ValidateFixedHeader.st
/// =======================================================================/// 名称 : M_ValidateFixedHeader/// 功能 : 校验 MQTT 固定头标志与剩余长度/// 说明 : 第二阶段严格拒绝非法固定头,防止错误客户端扰乱连接状态机。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_ValidateFixedHeader : BOOLVAR_INPUTuiFrameLen : UINT; // 当前完整 MQTT 报文总长度[byte]uiBodyOffset : UINT; // 当前 MQTT 报文可变头起始偏移[byte]END_VARVARbyFixedHeader : BYTE; // MQTT 固定头第 1 字节,包含控制类型和标志位udiBodyLen : UDINT; // MQTT Remaining Length 解码后的报文体长度[byte]END_VAR// === IMPLEMENTATION ===IF uiFrameLen < uiBodyOffset THENM_ValidateFixedHeader := FALSE;RETURN;END_IFbyFixedHeader := aRxBuf[0];udiBodyLen := TO_UDINT(uiFrameLen - uiBodyOffset);CASE byFixedHeader AND 16#F0 OF16#10:M_ValidateFixedHeader := byFixedHeader = 16#10;16#30:// PUBLISH 低 4 位携带 DUP/QoS/Retain,只额外拒绝 QoS 位为 3 的非法组合。M_ValidateFixedHeader := ((byFixedHeader AND 16#06) <> 16#06);16#40,16#50,16#70:// MQTT 3.1.1 的确认包 Remaining Length 固定为 2。// MQTT 5.0 允许在 Packet Identifier 后携带原因码和属性;// 连接层只需要 Packet Identifier 闭环事务,因此这里放宽为至少 2 字节。IF stConnection.byProtocolLevel = GVL_MqttBroker.cnMqttProtocolLevel5 THENM_ValidateFixedHeader := ((byFixedHeader AND 16#0F) = 0) AND (udiBodyLen >= 2);ELSEM_ValidateFixedHeader := ((byFixedHeader AND 16#0F) = 0) AND (udiBodyLen = 2);END_IF16#60:// PUBREL 在 MQTT 5.0 中同样可能携带原因码和属性。IF stConnection.byProtocolLevel = GVL_MqttBroker.cnMqttProtocolLevel5 THENM_ValidateFixedHeader := (byFixedHeader = 16#62) AND (udiBodyLen >= 2);ELSEM_ValidateFixedHeader := (byFixedHeader = 16#62) AND (udiBodyLen = 2);END_IF16#80:M_ValidateFixedHeader := byFixedHeader = 16#82;16#A0:M_ValidateFixedHeader := byFixedHeader = 16#A2;16#C0:M_ValidateFixedHeader := (byFixedHeader = 16#C0) AND (udiBodyLen = 0);16#E0:// MQTT 5.0 DISCONNECT 可带 Reason Code 和 Properties,当前只把它视为正常断开。IF stConnection.byProtocolLevel = GVL_MqttBroker.cnMqttProtocolLevel5 THENM_ValidateFixedHeader := byFixedHeader = 16#E0;ELSEM_ValidateFixedHeader := (byFixedHeader = 16#E0) AND (udiBodyLen = 0);END_IFELSEM_ValidateFixedHeader := FALSE;END_CASE
完整代码 14: FB_MqttBrokerConnection.st
/// =======================================================================/// 名称 : FB_MqttBrokerConnection/// 功能 : MQTT Broker 单客户端连接槽位/// 说明 : 封装一个 TCP 连接的读取、协议解析、协议优先响应和普通消息投递队列。/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}FUNCTION_BLOCK FB_MqttBrokerConnectionVAR_INPUTbEnable : BOOL; // 当前槽位使能,FALSE 时释放运行态但不主动分配 TCP 连接uiSlot : UINT; // 当前槽位编号[1..cnMaxClientSlots]hConnectionIn : NBS.CAA.HANDLE; // 监听层交给本槽位的 TCP 连接句柄xConnectionActiveIn : BOOL := FALSE; // 监听层确认该 TCP 连接仍处于活动状态,FALSE 时不执行 TCP_ReadudiNowMs : ULINT; // 顶层传入的当前系统时间戳[ms]END_VARVAR_OUTPUTxActive : BOOL; // 当前槽位是否占用xMqttConnected : BOOL; // 当前槽位 MQTT 会话是否已建立xNeedCleanup : BOOL; // 当前槽位请求顶层清理订阅和事务资源xPublishReady : BOOL; // 本周期解析出一条可路由 PUBLISHxSubscribeReady : BOOL; // 本周期解析出一条有效 SUBSCRIBExUnsubReady : BOOL; // 本周期解析出一条有效 UNSUBSCRIBExPubAckReady : BOOL; // 本周期收到订阅者返回的 PUBACKxPubRecReady : BOOL; // 本周期收到订阅者返回的 PUBREC,用于 QoS2 出站事务推进xPubRelReady : BOOL; // 本周期收到发布者返回的 PUBREL,用于 QoS2 入站事务推进xPubCompReady : BOOL; // 本周期收到订阅者返回的 PUBCOMP,用于 QoS2 出站事务完成eLastError : E_MqttBrokerError; // 当前连接槽位最近错误码stConnection : ST_MqttBrokerConnection; // 当前连接槽位公开会话数据stPublishOut : ST_MqttBrokerPublishFrame; // 本周期输出给路由层的发布帧sSubTopicFilter : STRING(GVL_MqttBroker.cnMaxTopicLen); // 本周期订阅请求 Topic FiltereSubQoS : E_MqttQoS; // 本周期订阅请求最大 QoSuiSubPacketId : UINT; // 本周期订阅请求 Packet IdentifieruiSubItemCount : UINT; // 本周期 SUBSCRIBE 中解析出的 Topic 条目数量[条]aSubItems : ARRAY[1..GVL_MqttBroker.cnMaxTopicItemsPerPacket] OF ST_MqttBrokerTopicItem; // 本周期 SUBSCRIBE 多 Topic 条目sUnsubTopicFilter : STRING(GVL_MqttBroker.cnMaxTopicLen); // 本周期取消订阅请求 Topic FilteruiUnsubPacketId : UINT; // 本周期取消订阅请求 Packet IdentifieruiUnsubItemCount : UINT; // 本周期 UNSUBSCRIBE 中解析出的 Topic 条目数量[条]aUnsubItems : ARRAY[1..GVL_MqttBroker.cnMaxTopicItemsPerPacket] OF ST_MqttBrokerTopicItem; // 本周期 UNSUBSCRIBE 多 Topic 条目uiPubAckPacketId : UINT; // 本周期收到的 PUBACK Packet IdentifieruiPubRecPacketId : UINT; // 本周期收到的 PUBREC Packet IdentifieruiPubRelPacketId : UINT; // 本周期收到的 PUBREL Packet IdentifieruiPubCompPacketId : UINT; // 本周期收到的 PUBCOMP Packet IdentifierxTcpReadError : BOOL; // 在线诊断:当前槽位 TCP_Read 是否报错xTcpWriteError : BOOL; // 在线诊断:当前槽位 TCP_Write 是否报错xWriteBusy : BOOL; // 在线诊断:当前槽位是否有报文正在等待 TCP_Write 完成xWriteExecute : BOOL; // 在线诊断:当前槽位当前扫描周期是否触发 TCP_WritexConnectionActive : BOOL; // 在线诊断:监听层当前是否确认本槽位 TCP 连接仍 ActivexLastTcpReadError : BOOL; // 在线诊断:当前槽位本次生命周期内是否曾经捕获 TCP_Read 报错uiTcpReadErrorCountOut : UINT; // 在线诊断:当前槽位连续 TCP_Read 错误次数,达到阈值才释放连接udiLastBytesRead : UDINT; // 在线诊断:最近一次 TCP_Read 读到的字节数[byte]udiLastNonZeroBytesRead : UDINT; // 在线诊断:当前槽位生命周期内最近一次非零 TCP_Read 字节数[byte]uiLastTxFrameCount : UINT; // 在线诊断:最近一次 TCP_Write 合并写出的 MQTT 控制报文帧数量[帧]uiLastTxBytes : UINT; // 在线诊断:最近一次 TCP_Write 写出的总字节数[byte]udiTxBatchCount : UDINT; // 在线诊断:当前槽位累计启动 TCP 批量写出的次数[次]udiTxFrameCount : UDINT; // 在线诊断:当前槽位累计写出的 MQTT 控制报文帧数量[帧]uiMaxDeliveryQueueCountSeen : UINT; // 在线诊断:当前槽位普通投递队列历史最高水位[条]uiMaxProtocolQueueCountSeen : UINT; // 在线诊断:当前槽位协议优先队列历史最高水位[条]udiTxQueueFullDropped : UDINT; // 在线诊断:当前槽位因慢客户端保护丢弃的 QoS0 投递数量[条]uiLastFrameLen : UINT; // 在线诊断:最近一次识别到的 MQTT 完整帧长度[byte]byLastPacketType : BYTE; // 在线诊断:最近一次处理的 MQTT 控制报文类型高 4 位,例如 16#10 表示 CONNECTbyLastConnectLevel : BYTE; // 在线诊断:最近一次 CONNECT 协议级别字节,3 表示 3.1,4 表示 3.1.1,5 表示 5.0 基础兼容模式xLastConnectParsed : BOOL; // 在线诊断:最近一次 CONNECT 是否成功解析并进入会话建立流程eLastParseError : E_MqttBrokerError; // 在线诊断:最近一次 MQTT 报文解析失败原因,uiNoError 表示最近解析未失败eTcpReadErrorID : NBS.ERROR; // 在线诊断:TCP_Read 原始错误码eLastTcpReadErrorID : NBS.ERROR; // 在线诊断:当前槽位生命周期内首次/最近一次 TCP_Read 报错原始错误码,避免禁用读 FB 后被清零eTcpWriteErrorID : NBS.ERROR; // 在线诊断:TCP_Write 原始错误码END_VARVARfbTcpRead : NBS.TCP_Read; // NBS TCP 读取功能块实例,必须每扫描周期调用fbTcpWrite : NBS.TCP_Write; // NBS TCP 写入功能块实例,必须每扫描周期调用fbCodec : FB_MqttBrokerCodec; // MQTT 报文编解码器实例tonWrite : TON; // 当前 TCP 写操作超时计时器aRxBuf : ARRAY[0..GVL_MqttBroker.cnRxBufferSize - 1] OF BYTE; // 当前连接接收缓冲区[byte]aTxBuf : ARRAY[0..GVL_MqttBroker.cnTxBufferSize - 1] OF BYTE; // 当前连接发送缓冲区[byte]aProtocolQueue : ARRAY[1..GVL_MqttBroker.cnProtocolQueueSize] OF ST_MqttBrokerProtocolAck; // 协议响应优先队列aDeliveryQueue : ARRAY[1..GVL_MqttBroker.cnDeliveryQueueSize] OF ST_MqttBrokerPublishFrame; // 普通 PUBLISH 投递队列bTcpReadEnable : BOOL; // TCP 读取使能,当前槽位在线时保持 TRUExTcpReadAllowed : BOOL; // TCP 读取许可,底层 Active 短暂闪断但仍在宽限期内时保持 TRUE,避免首包读取被闪断打碎bTcpWriteExecute : BOOL; // TCP 写执行脉冲,发送队列取出报文后置 TRUEbWriteBusy : BOOL; // 当前是否有一包 MQTT 报文正在等待 TCP_Write 完成bWriteDoneLatched : BOOL; // TCP 写完成锁存,避免上层错过 xDone 短脉冲uiTcpReadErrorCount : UINT; // 当前连接连续 TCP_Read 错误次数,超过阈值才释放槽位udiBytesRead : UDINT; // 本周期从 TCP 读取到的字节数[byte]uiFrameLen : UINT; // 当前接收缓冲区中已凑齐的完整 MQTT 报文长度[byte]uiBodyOffset : UINT; // 当前完整 MQTT 报文可变头起始偏移[byte]udiRemainingLen : UDINT; // 当前 MQTT 报文 Remaining Length 数值[byte]uiRemainingBytes : UINT; // Remaining Length 字段占用字节数[byte]xNeedMore : BOOL; // Remaining Length 解码时是否需要更多 TCP 数据uiQueueIndex : UINT; // 队列扫描索引uiPayloadOffset : UINT; // 用于解析 PUBACK PacketId 的偏移[byte]uiFramesProcessed : UINT; // 本扫描周期当前连接已处理的 MQTT 报文数量[帧]eParseError : E_MqttBrokerError; // 编解码器解析失败时返回的错误码END_VAR// === IMPLEMENTATION ===xPublishReady := FALSE;xSubscribeReady := FALSE;xUnsubReady := FALSE;xPubAckReady := FALSE;xPubRecReady := FALSE;xPubRelReady := FALSE;xPubCompReady := FALSE;xNeedCleanup := FALSE;stPublishOut.xValid := FALSE;sSubTopicFilter := '';sUnsubTopicFilter := '';uiSubPacketId := 0;uiUnsubPacketId := 0;uiSubItemCount := 0;uiUnsubItemCount := 0;uiPubAckPacketId := 0;uiPubRecPacketId := 0;uiPubRelPacketId := 0;uiPubCompPacketId := 0;IF NOT bEnable THENM_Reset();fbTcpRead(xEnable := FALSE, hConnection := hConnectionIn, szSize := 0, pData := ADR(aRxBuf));fbTcpWrite(xExecute := FALSE, hConnection := hConnectionIn, szSize := 0, pData := ADR(aTxBuf));xTcpReadError := fbTcpRead.xError;xTcpWriteError := fbTcpWrite.xError;eTcpReadErrorID := fbTcpRead.eError;eTcpWriteErrorID := fbTcpWrite.eError;RETURN;END_IFIF NOT stConnection.xUsed AND (hConnectionIn <> 0) THENM_Attach(hConnection := hConnectionIn);END_IFxActive := stConnection.xUsed;xMqttConnected := stConnection.xMqttConnected;xConnectionActive := xConnectionActiveIn;xTcpReadAllowed := xConnectionActiveIn;IF stConnection.xUsed THENIF xConnectionActiveIn THENstConnection.udiLastTcpActiveMs := udiNowMs;ELSE// TCP_Connection.xActive 是 NBS 对句柄生命期的底层判断,但现场测试发现它可能出现短暂 FALSE。// 这里不能一拍 FALSE 就释放 MQTT 会话,否则两个客户端同连 1883 时会出现 rc -1、断开后又能重连。// 只有超过容忍窗口仍未恢复 Active,才判定 TCP 连接确实失效。IF (udiNowMs - stConnection.udiLastTcpActiveMs) >= TO_ULINT(GVL_MqttBroker.cnConnectionInactiveGraceMs) THENeLastError := E_MqttBrokerError.uiTcpReadFailed;stConnection.xDisconnectRequested := TRUE;ELSE// 只要仍处于底层 Active 闪断宽限期,就继续允许 TCP_Read 尝试读取。// 之前这里虽然不释放会话,但 bTcpReadEnable 会跟随 xActive 变 FALSE,// 导致 MQTT CONNECT 首包在客户端重连抖动时迟迟读不到,表现为 xTcpAcceptActive / uiActiveSlotCount 闪烁。xTcpReadAllowed := TRUE;END_IFEND_IFEND_IFbTcpReadEnable := stConnection.xUsed AND xTcpReadAllowed AND NOT stConnection.xDisconnectRequested;IF bTcpReadEnableAND (stConnection.uiRxLength < GVL_MqttBroker.cnRxBufferSize)AND ((udiNowMs - stConnection.udiLastActivityMs) >= TO_ULINT(GVL_MqttBroker.cnConnectFirstReadDelayMs)) THEN// 新 TCP 连接刚入槽时,底层可能已经 Active 但首个 MQTT CONNECT 尚未到达。// 首包延时窗口能过滤 NBS 首次空读/瞬态错误,避免 aSnapshots[x].xUsed 刚置位又立刻复位。fbTcpRead(xEnable := TRUE,hConnection := stConnection.hConnection,szSize := TO_UDINT(GVL_MqttBroker.cnRxBufferSize - stConnection.uiRxLength),pData := ADR(aRxBuf[stConnection.uiRxLength]));IF fbTcpRead.xReady AND NOT fbTcpRead.xError THENudiBytesRead := TO_UDINT(fbTcpRead.szCount);udiLastBytesRead := udiBytesRead;IF udiBytesRead > 0 THENuiTcpReadErrorCount := 0;END_IFIF udiBytesRead > 0 THENudiLastNonZeroBytesRead := udiBytesRead;END_IFIF (TO_UDINT(stConnection.uiRxLength) + udiBytesRead) <= TO_UDINT(GVL_MqttBroker.cnRxBufferSize) THENstConnection.uiRxLength := stConnection.uiRxLength + TO_UINT(udiBytesRead);IF udiBytesRead > 0 THENstConnection.udiLastActivityMs := udiNowMs;END_IFELSEeLastError := E_MqttBrokerError.uiPacketTooLarge;stConnection.xDisconnectRequested := TRUE;END_IFELSIF fbTcpRead.xError THENxLastTcpReadError := TRUE;eLastTcpReadErrorID := fbTcpRead.eError;IF uiTcpReadErrorCount < GVL_MqttBroker.cnTcpReadMaxConsecutiveError THENuiTcpReadErrorCount := uiTcpReadErrorCount + 1;END_IFIF uiTcpReadErrorCount >= GVL_MqttBroker.cnTcpReadMaxConsecutiveError THENeLastError := E_MqttBrokerError.uiTcpReadFailed;stConnection.xDisconnectRequested := TRUE;END_IFEND_IFELSEfbTcpRead(xEnable := FALSE, hConnection := stConnection.hConnection, szSize := 0, pData := ADR(aRxBuf));END_IFxTcpReadError := fbTcpRead.xError;eTcpReadErrorID := fbTcpRead.eError;uiTcpReadErrorCountOut := uiTcpReadErrorCount;uiFramesProcessed := 0;WHILE (uiFramesProcessed < GVL_MqttBroker.cnMaxFramesPerConnectionScan)AND M_HasCompleteFrame(uiFrameLen => uiFrameLen, uiBodyOffset => uiBodyOffset, udiRemainingLen => udiRemainingLen, xNeedMore => xNeedMore) DOuiLastFrameLen := uiFrameLen;M_ProcessFrame(uiFrameLen := uiFrameLen, uiBodyOffset := uiBodyOffset);M_ConsumeFrame(uiFrameLen := uiFrameLen);uiFramesProcessed := uiFramesProcessed + 1;END_WHILEIF stConnection.xMqttConnected AND (stConnection.uiKeepAlive > 0) THENIF (udiNowMs - stConnection.udiLastActivityMs) > (TO_ULINT(stConnection.uiKeepAlive) * 1000 * TO_ULINT(GVL_MqttBroker.cnKeepAliveGracePercent) / 100) THENeLastError := E_MqttBrokerError.uiKeepAliveTimeout;stConnection.xDisconnectRequested := TRUE;END_IFEND_IFIF NOT bWriteBusy THENM_PrepareNextTx();END_IFfbTcpWrite(xExecute := bTcpWriteExecute,hConnection := stConnection.hConnection,szSize := TO_UDINT(stConnection.uiTxLength),pData := ADR(aTxBuf));xTcpWriteError := fbTcpWrite.xError;eTcpWriteErrorID := fbTcpWrite.eError;xWriteBusy := bWriteBusy;xWriteExecute := bTcpWriteExecute;IF NOT bTcpWriteExecute THENbWriteDoneLatched := FALSE;ELSIF fbTcpWrite.xError THENbWriteDoneLatched := FALSE;eLastError := E_MqttBrokerError.uiTcpWriteFailed;stConnection.xDisconnectRequested := TRUE;ELSIF fbTcpWrite.xDone THENbWriteDoneLatched := TRUE;END_IFtonWrite(IN := bWriteBusy, PT := GVL_MqttBroker.cnWriteTimeout);IF bWriteBusy AND bWriteDoneLatched THENbTcpWriteExecute := FALSE;bWriteBusy := FALSE;stConnection.uiTxLength := 0;bWriteDoneLatched := FALSE;ELSIF bWriteBusy AND tonWrite.Q THENeLastError := E_MqttBrokerError.uiTcpWriteFailed;stConnection.xDisconnectRequested := TRUE;END_IFxWriteBusy := bWriteBusy;xWriteExecute := bTcpWriteExecute;IF stConnection.xDisconnectRequested THENstConnection.eState := E_MqttConnectionState.iClosed;IF (stConnection.uiProtocolQueueCount = 0)AND (stConnection.uiDeliveryQueueCount = 0)AND NOT bWriteBusyAND NOT bTcpWriteExecuteAND (stConnection.uiTxLength = 0) THENxNeedCleanup := TRUE;END_IFEND_IF
这一篇你最该记住的几句话
Broker 源码不要按“文件夹顺序”读,要按“入口、状态、数据、报文、路由、事务”读。 CodeSys ST 工程最容易失控的不是语法,而是对象职责边界混乱。 只要你能把本篇源码对象和在线变量对应起来,后续排查连接、订阅、发布和 QoS 问题就不会乱。
系列导航
第 1 篇:源码加更01_Broker 工程入口、容量边界和数据模型 第 2 篇:源码加更02_FB_MqttBroker 顶层调度、连接池和权限边界 第 3 篇:源码加更03_单连接槽位、TCP 字节流和发送队列 第 4 篇:源码加更04_MQTT 编解码器和字节工具函数 第 5 篇:源码加更05_订阅表、Retain、PUBLISH 路由和业务事件 第 6 篇:源码加更06_QoS 事务调度、重试和生产级闭环
夜雨聆风