
订阅成功了但发布后收不到消息。这个问题在 Broker 开发里非常常见。原因通常不是客户端没订阅而是 Broker 把 SUBSCRIBE 当成了“存一个字符串”。先给结论客户端发布的是 Topic Name客户端订阅的是 Topic Filter。Broker 必须保存 Topic Filter并在每次 PUBLISH 时用 Topic Name 去匹配所有 Filter。一、Topic Name 和 Topic Filter 不是一个东西这张表先记住名称出现位置是否允许通配符示例Topic NamePUBLISH不允许CodeSys、device/1/statusTopic FilterSUBSCRIBE允许CodeSys、device//status、device/#很多问题都出在这里。如果客户端订阅device//status然后另一个客户端发布device/plc01/statusBroker 不能做字符串相等判断。它必须做 Topic Filter 匹配。二、订阅链路完整流程SUBSCRIBE 至少要做 4 件事解析 PacketId。逐项读取 Topic Filter 和请求 QoS。校验 Topic Filter 合法性。每一项生成 SUBACK 返回码。三、Topic Filter 校验规则通配符不是随便放。Filter是否合法原因CodeSys是普通主题device//status是占一个层级device/#是#位于最后一层device/#/status否#必须是最后一个层级device/a否必须独占一个层级device/#abc否#必须独占一个层级否空 Filter 不合法所以 Broker 里需要独立函数F_MqttIsValidTopicFilter(sTopicFilter : sFilter)不要把这个校验散落在 SUBSCRIBE 解析里。四、订阅表应该保存什么当前轻量 Broker 使用固定资源模型不搞动态链表。订阅表可以理解为字段说明xUsed表项是否有效uiSlotIndex属于哪个客户端槽位sClientId客户端标识便于诊断sTopicFilter订阅过滤器eMaxQoS客户端请求的最大 QoSudiLastUpdateMs最近更新时间结构关系大概是五、匹配规则示例Topic FilterTopic Name是否匹配CodeSysCodeSys是CodeSysCodeSys/a否CodeSys/#CodeSys是CodeSys/#CodeSys/a/b是/statusplc01/status是/statusarea/plc01/status否device//statusdevice/plc01/status是device//statusdevice/plc01/run/status否核心函数就是xMatch : F_MqttTopicMatch( sTopicFilter : stSubscription.sTopicFilter, sTopicName : stPublish.sTopicName);这类函数必须写得很克制层级扫描、通配符规则、边界长度别为了几台客户端搞复杂树结构。六、多 Topic SUBSCRIBE 不能只回一个结果客户端可能一次订阅多个主题SUBSCRIBE PacketId7 1. CodeSys QoS0 2. device//status QoS1 3. bad/#/topic QoS1Broker 应该返回多个结果Topic Filter结果CodeSysGranted QoS0device//statusGranted QoS1bad/#/topicFailure所以 SUBACK 不是简单一个成功码。90 05 00 07 00 01 80拆开看字节含义90SUBACK05Remaining Length00 07PacketId00第 1 项 Granted QoS001第 2 项 Granted QoS180第 3 项 Failure七、ST 代码入口代码入口作用FB_MqttBrokerCodec.M_ParseSubscribe解析 SUBSCRIBE提取 PacketId 和 Topic FilterFB_MqttBrokerRouter.M_AddSubscription写入或更新订阅表FB_MqttBrokerRouter.M_FindNextRoutePUBLISH 到来时查找下一个命中订阅者F_MqttIsValidTopicFilter校验 Topic Filter 合法性F_MqttTopicMatch判断 Topic Name 是否命中 Topic Filter逻辑可以压缩成IF F_MqttIsValidTopicFilter(sTopicFilter : sFilter) THEN fbRouter.M_AddSubscription( uiSlotIndex : uiSlotIndex, sClientId : sClientId, sTopicFilter : sFilter, eMaxQoS : eRequestQoS); byReturnCode : byGrantedQoS; ELSE byReturnCode : 16#80; END_IF八、现场排障表现象先看什么可能原因客户端显示订阅失败SUBACK 返回码Topic Filter 非法或 ACL 拒绝订阅成功但收不到订阅表是否存在该 FilterRouter 没写入或写错槽位普通主题能收通配符收不到F_MqttTopicMatch/#规则实现错误多主题订阅只有第一项生效uiSubItemCount解析循环只处理第一项取消订阅后还收到订阅表清理UNSUBSCRIBE 没删除对应 Filter模型边界与验证路径SUBSCRIBE 的本质是路由规则注册不是字符串保存。从模型上看Topic Filter 是规则Topic Name 是事实。Broker 的职责就是把事实拿去匹配规则再把结果映射到客户端槽位。结论可信度依据验证路径Topic Name 和 Topic Filter 必须分开处理highMQTT 订阅与发布语义用device//status订阅再发布device/plc01/status通配符校验应独立封装high源码可维护性和边界复用用合法 / 非法 Filter 对比 SUBACK 返回当前固定表路由适合小规模 PLC Brokermedium当前目标是5~8个客户端增加客户端和订阅项后观察扫描周期和队列水位如果以后订阅规模明显扩大问题就不再是“这个函数怎么写”而是订阅索引模型要不要升级。当前文章不把树形索引作为默认方案因为这不是当前轻量 Broker 的主要瓶颈。九、这一篇你最该记住的 5 句话PUBLISH 使用 Topic NameSUBSCRIBE 使用 Topic Filter。Broker 路由不能只做字符串相等必须支持和#。多 Topic SUBSCRIBE 必须逐项返回 SUBACK。订阅表要绑定客户端槽位而不是只保存主题字符串。订阅成功不等于路由正确现场一定要看订阅表和匹配函数。下篇预告下一篇讲 PUBLISH。重点是PUBLISH 不是收到就转发Broker 必须处理 QoS、PacketId 和多客户端 fanout。我们会重点拆 PacketId 为什么不能直接沿用发布者的。完整 ST 代码本篇涉及的完整代码入口MqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerCodec.M_ParseSubscribe.stMqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerRouter.M_AddSubscription.stMqttBroker/Device/Application/POUs/FBs/FB_MqttBrokerRouter.M_FindNextRoute.stMqttBroker/Device/Application/POUs/Functions/F_MqttIsValidTopicFilter.stMqttBroker/Device/Application/POUs/Functions/F_MqttTopicMatch.st系列导航系列定位第 4 篇上一篇CONNECT 解析别写死MQTT 3.1、3.1.1、5.0 为什么会让 Broker 反复断开下一篇PUBLISH 不是收到就转发Broker 怎么处理 QoS、PacketId 和多客户端 fanout项目与资料开源项目名称MqttBroker前置系列MqttClient_V2_0核心关键词SUBSCRIBE、Topic Filter、通配符、订阅表、路由适合谁收藏订阅显示成功但收不到消息的人想把 MQTT 通配符匹配写对的人正在实现多客户端路由的人想从 Client 视角切到 Broker 视角的人