这一篇完全从工程排障视角切入,把最常见的 MQTT 现场故障按连接层、ACK 链、状态机和会话恢复几条主线拆开,帮助你快速定位到底是网络、Broker 还是客户端时序出了问题。
适合谁收藏
- 正在做 CODESYS / PLC / MQTT 项目的人
- 想把 MQTT 从报文真正看到 ST 代码的人
- 正在排查 QoS1 / QoS2 超时、掉线、重连问题的人
前面几篇,我们一直在讲:
- 报文怎么长
- ACK 怎么走
- 状态机怎么分
但对现场工程师来说,最有价值的问题其实是:
出问题了,我该先查哪?
这一篇就完全站在排障视角来说。
先给结论:
MQTT 现场大多数问题,最后都能落到 4 条主线里:
一、先给一张排障总图
flowchart TD
A[现象] --> B{连不上还是连上后出问题}
B -->|连不上| C[TCP层 / CONNECT层]
B -->|连上后出问题| D{QoS0还是QoS1/QoS2}
D -->|QoS0也异常| E[接收链 / Topic / Broker配置]
D -->|QoS1 QoS2异常| F[ACK链 / inflight / 状态机]
F --> G{是否重连后恢复}
G -->|没有恢复| H[会话与订阅恢复逻辑]
G -->|恢复但不稳定| I[高频调度 / 即时ACK / 超时参数]这张图的意思很简单:
不要一出问题就一锅端。
二、TcpConnect timeout 先查什么
如果报的是:
TcpConnect timeout先不要上来怀疑 MQTT 报文。 因为这时候大概率还没进 MQTT 层。
优先检查 4 项
| 检查项 | 说明 |
|---|---|
| Broker IP 是否可达 | ping 通不代表端口一定可连,但至少先排 IP |
| Broker 端口是否正确 | 1883 / 自定义端口 |
| Windows 防火墙 / 网卡网络配置 | 这类环境问题非常常见 |
| PLC 到 Broker 的 TCP 路是否真通 | 能在线监控 PLC,不等于 PLC 到 Broker 的 TCP 也通 |
结论
TcpConnect timeout 大概率属于:
- TCP 层问题
- 端口连通性问题
- Broker 没监听
- PC 防火墙 / 网卡配置问题
而不是 MQTT 报文问题。
三、CONNACK 失败先查什么
如果 TCP 已经通了,但卡在连接确认阶段,就往 CONNECT / CONNACK 这条链看。
常见原因
| 现象 | 优先怀疑点 |
|---|---|
| 协议版本不支持 | eVersion 配错,Broker 不支持 |
| Client ID 被拒绝 | Client ID 非法或冲突 |
| 用户名密码错误 | 凭据问题 |
| 5.0 能连,3.1.1 不连,或反过来 | Broker 配置与协议版本不匹配 |
排障思路
- 看
M_HandleConnAck是否拿到非 0 原因码 - 看
sDiagMsg最后落的是什么 - 先排 Broker 是否真支持你选择的 MQTT 版本
四、为什么 Immediate response timeout 很容易让人误判
这是现场特别容易把人绕进去的一个问题。
因为用户常常会说:
但消息明明收到了啊。
这句话通常是真的。 但它只说明:
- 数据帧进来了
- 业务层可能也看到了
它不说明:
- 你的协议响应发及时了
这类超时最常见的根因是:
| 场景 | 必须及时发的响应 |
|---|---|
| 收到 QoS1 PUBLISH | PUBACK |
| 收到 QoS2 PUBLISH | PUBREC |
收到 PUBREL | PUBCOMP |
也就是说:
你可以“收到消息”,但只要 ACK 回晚了,对端照样可以按超时处理你。
五、为什么 QoS0 正常而 QoS1 / QoS2 容易炸
这类现象基本可以直接判:
重点不要再查 Topic 和普通 TCP 收发了,直接查 ACK 链和 inflight。
原因很简单
| 维度 | QoS0 | QoS1 / QoS2 |
|---|---|---|
| 需要 ACK | 否 | 是 |
| 需要 inflight | 否 | 是 |
| 需要超时重发 | 否 | 是 |
| 需要状态释放 | 简单 | 复杂 |
所以一旦出现:
- QoS0 全绿
- QoS1 / QoS2 高频就掉线
优先去看:
- 等待状态有没有及时释放
- inflight 有没有积压
- ACK 有没有被接收链及时消化
- 超时参数是不是过紧
六、如果状态机掉进 iTcpDisconnect,怎么看
很多人一看到 iTcpDisconnect 就以为:
网络断了。
其实不一定。
前面我们已经讲过,iTcpDisconnect 是统一异常收口。 也就是说,下面这些问题都可能把你送进去:
- TCP 真的断了
- CONNACK 超时
PUBACK / PUBREC / PUBCOMP超时SUBACK / UNSUBACK超时- 即时 ACK 发送失败
所以正确问法不是:
为什么进了 iTcpDisconnect?
而是:
进 iTcpDisconnect 之前,我最后卡在哪个等待状态?
这个顺序非常重要。
七、为什么退订有时会“看起来像卡死”
退订链的问题很典型:
- 订阅能成功
- 退订一触发,状态机卡在
iUnsubAck - 拉低
connect和enable都没立刻恢复
这类问题通常优先看:
UNSUBSCRIBE报文 Packet ID 是否正确- 发送后是否真的进入了等待
UNSUBACK - 接收路径是否正确识别了
UNSUBACK - 收到
UNSUBACK后等待标志是否释放
也就是说,别把它当“退订字符串问题”。 它本质上还是 ACK 等待链问题。
八、为什么高频多客户端同主题场景最容易把问题打出来
这个场景之所以值钱,是因为它同时压了几层:
- 接收面吞吐
- 即时 ACK 时序
- inflight 管理
iConnected调度能力- KeepAlive 与业务流的共存
如果这个场景能长期稳定跑:
- 多客户端
- 同主题
- QoS1 / QoS2
- 高频
那基本说明你的 MQTT 客户端已经不是“演示级”,而是进入可用级了。
九、为什么重连后订阅会丢
这个问题现场非常常见,而且很多人第一反应会怪 Broker。
实际上要分两层看:
1. 协议层
重连后订阅要不要还在,跟下面这些有关:
- Clean Session / Clean Start
- Session Expiry
- Broker 的会话保留策略
2. 客户端实现层
就算 Broker 会话没保住,客户端要不要自动重建订阅,也取决于你本地有没有:
- 订阅列表
- 重连后重放策略
所以“重连后订阅丢失”不是一个单点问题,而是:
协议语义 + Broker 行为 + 客户端恢复策略
十、排障时最推荐的 6 个观察点
这个清单很实用,建议你真正在现场用。
| 观察点 | 你要看什么 |
|---|---|
| 当前主状态 | 卡在哪个状态 |
| 等待标志 | xWaitingForAck 是否释放 |
| 期待报文类型 | byExpectedMsgType 对不对 |
| Packet ID | 发出去和收回来的是否匹配 |
| inflight 队列 | 是否堆积、是否反复超时 |
| 诊断信息 | sDiagMsg 最后写了什么 |
这 6 个点比“盲抓包 + 猜原因”效率高很多。
十一、现场排障最容易犯的 3 个错误
错误 1:一看到超时就怀疑网络
网络当然可能有问题。 但 MQTT 里很多超时,本质是协议时序没跑顺。
错误 2:只看收没收到消息,不看 ACK 有没有回
尤其是 QoS1 / QoS2,这个误判非常多。
错误 3:只看最后掉到 iTcpDisconnect,不看它之前卡在哪
这会让你把很多不同根因都误归成“断线问题”。
十二、给一张现场快速定位表
| 现象 | 最短定位路径 |
|---|---|
TcpConnect timeout | 先查 TCP 路由、端口、防火墙 |
| TCP 通但连不上 MQTT | 查 CONNECT / CONNACK |
| QoS0 正常,QoS1 炸 | 查 PUBACK 等待链 |
| QoS1 正常,QoS2 炸 | 查 PUBREC/PUBREL/PUBCOMP |
| 收到消息但仍超时 | 查即时 ACK 是否及时回发 |
| 退订卡死 | 查 UNSUBACK 等待链 |
| 重连后订阅没了 | 查会话语义和本地恢复策略 |
十三、这一篇你最该记住的 6 句话
- 排 MQTT 故障,先分层,不要一上来全怀疑。
TcpConnect timeout大概率还是 TCP 层问题,不是 MQTT 报文问题。Immediate response timeout很多时候是 ACK 回慢了,不是消息没收到。- QoS0 正常而 QoS1 / QoS2 异常,优先查 ACK 链和 inflight。
iTcpDisconnect是统一异常收口,不等于根因就是网络断了。- 重连后订阅丢失,必须同时从协议语义、Broker 行为、客户端恢复策略三层看。
十四、下篇预告
下一篇我们收回到用户最关心的落地视角:
怎么把这套开源 MQTT 客户端真正用起来
会讲:
- 接入思路
- 参数怎么配
- 用 PLC 内置 Broker、EMQX、Mosquitto 分别要注意什么
- 真正上线前至少该过哪些测试项
完整 ST 代码
复制使用说明
- 这部分给出的是与本篇主题直接对应的完整 ST 代码,不是零碎片段。
- 如果你只是想先跑通,优先整段复制,不要只摘几行变量或几条赋值语句。
- 如果是
METHOD,请确认它仍然属于FB_MqttClient;如果是PROGRAM,请确认相关 DUT、GVL、FB 已一并导入。
代码阅读重点
- 先按
报文结构 -> 状态机入口 -> 关键变量 -> 返回结果的顺序看。 - 再把正文里的十六进制拆解和这里的字节写入、字节解析语句一行行对上。
- 最后回到在线调试,重点盯
uiTxLength、uiRxLength、eState、xWaitingForAck这类状态量。
完整代码 1:M_TcpClient
- 对应源码路径:
10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient.M_TcpClient.st - 复制使用说明:这是 TCP 连接层的完整封装,现场一旦报
TcpConnect timeout,首先就该看这里。 - 阅读重点:重点看 NBS TCP 客户端的使能、句柄、连接结果回传,先把“底层通不通”和“MQTT 通不通”分开。
/// =======================================================================
/// 名称 : M_TcpClient
/// 功能 : 控制 TCP 客户端的连接与断开
/// 说明 : 封装 NBS TCP 客户端连接调用,输出连接状态与错误信息。
/// 编程人员 : ControlRookie
/// 时间 : 2026-01-10
/// 版本 : V1.1
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_TcpClient
VAR_INPUT
bEnable : BOOL; // TCP 连接使能标志
sIP : STRING; // 服务器 IP 地址
uiPortNum : UINT; // 服务器端口号
END_VAR
VAR_OUTPUT
bIsConnected : BOOL; // 连接成功标志
bError : BOOL; // 错误标志
eError : NBS.ERROR; // NBS 错误码
END_VAR
VAR
stIP : NBS.IP_ADDR; // 服务器地址结构
END_VAR
// === IMPLEMENTATION ===
stIP.sAddr := sIP;
fbTcpClient(
xEnable := bEnable,
ipAddr := stIP,
uiPort := uiPortNum,
eError => eError,
hConnection => hConnection);
bError := fbTcpClient.xError;
IF fbTcpClient.xActive AND fbTcpClient.hConnection <> 0 THEN
bIsConnected := TRUE;
ELSE
bIsConnected := FALSE;
END_IF完整代码 2:M_TcpRead
- 对应源码路径:
10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient.M_TcpRead.st - 复制使用说明:这是收包入口,很多“明明网络通但总超时”的问题最后都能追到读取节拍上。
- 阅读重点:重点看执行条件、读取完成标志和字节数回传,这里会直接影响接收状态机是否误判超时。
/// =======================================================================
/// 名称 : M_TcpRead
/// 功能 : 从 TCP 连接读取数据到接收缓冲区
/// 说明 : 包装 NBS.TCP_Read,读取成功后更新 uiRxLength。
/// 编程人员 : ControlRookie
/// 时间 : 2026-05-05
/// 版本 : V1.1
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_TcpRead
VAR_INPUT
bEnable : BOOL; // TCP 读取使能标志
pDataReceive : POINTER TO BYTE; // 接收数据区首地址
udiDataSize : UDINT; // 接收数据区大小
END_VAR
VAR_OUTPUT
bDone : BOOL; // 读取完成标志
bError : BOOL; // 错误标志
eError : NBS.ERROR; // NBS 错误码
udiBytesRead : UDINT; // 本次读取到的字节数
END_VAR
// === IMPLEMENTATION ===
fbTcpRead(
xEnable := bEnable,
xError => bError,
hConnection := hConnection,
szSize := udiDataSize,
pData := pDataReceive,
eError => eError);
IF fbTcpRead.xReady AND NOT fbTcpRead.xError THEN
bDone := TRUE;
udiBytesRead := TO_UDINT(fbTcpRead.szCount);
ELSE
bDone := FALSE;
udiBytesRead := 0;
END_IF
IF NOT bEnable THEN
bDone := FALSE;
udiBytesRead := 0;
END_IF完整代码 3:M_TcpWrite
- 对应源码路径:
10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient.M_TcpWrite.st - 复制使用说明:这是发包入口,出现
Immediate response timeout、发送超时、QoS 回包跟不上时要优先看这里。 - 阅读重点:重点看执行沿、完成锁存和发送长度,理解为什么“报文已经组好了”不等于“报文已经真正发出去了”。
/// =======================================================================
/// 名称 : M_TcpWrite
/// 功能 : 向 TCP 连接发送缓冲区数据
/// 说明 : 包装 NBS.TCP_Write,发送成功后置位完成标志。
/// 编程人员 : ControlRookie
/// 时间 : 2026-05-05
/// 版本 : V1.1
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_TcpWrite
VAR_INPUT
bExecute : BOOL; // TCP 写入执行标志
pDataSend : POINTER TO BYTE; // 发送数据区首地址
udiDataSize : UDINT; // 发送数据区大小
END_VAR
VAR_OUTPUT
bDone : BOOL; // 发送完成标志
bError : BOOL; // 错误标志
eError : NBS.ERROR; // NBS 错误码
END_VAR
// === IMPLEMENTATION ===
fbTcpWrite(
xExecute := bExecute,
xError => bError,
hConnection := hConnection,
szSize := udiDataSize,
pData := pDataSend,
eError => eError);
IF NOT bExecute THEN
bWriteDoneLatched := FALSE;
ELSIF fbTcpWrite.xError THEN
bWriteDoneLatched := FALSE;
ELSIF fbTcpWrite.xDone THEN
bWriteDoneLatched := TRUE;
END_IF
bDone := bWriteDoneLatched;完整代码 4:M_SetError
- 对应源码路径:
10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient/辅助功能/M_SetError.st - 复制使用说明:这是统一错误上报入口,现场诊断时最好和状态机、TCP 方法一起看。
- 阅读重点:重点看错误码和诊断字符串是怎么被统一落到输出引脚上的,这直接决定了排障效率。
/// =======================================================================
/// 名称 : M_SetError
/// 功能 : 设置错误状态和诊断信息
/// 说明 : 写入内部错误码与诊断文本,并置位错误标志。
/// 编程人员 : ControlRookie
/// 时间 : 2026-05-05
/// 版本 : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_SetError : BOOL
VAR_INPUT
uiErrorCode : UINT; // 内部错误码
sMessage : STRING(2048); // 诊断信息文本
END_VAR
// === IMPLEMENTATION ===
bError := TRUE;
eErrorID := TO_INT(uiErrorCode);
sDiagMsg := sMessage;
M_SetError := TRUE;完整代码 5:M_ResetError
- 对应源码路径:
10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient/辅助功能/M_ResetError.st - 复制使用说明:这是错误复位入口,和
M_SetError配套使用。 - 阅读重点:重点看哪些状态量会被清掉,理解为什么有些问题不是简单拉低
bConnect就一定能恢复。
/// =======================================================================
/// 名称 : M_ResetError
/// 功能 : 复位错误状态
/// 说明 : 清除错误标志、错误码与诊断信息并返回成功状态。
/// 编程人员 : ControlRookie
/// 时间 : 2026-05-05
/// 版本 : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_ResetError : BOOL
// === IMPLEMENTATION ===
bError := FALSE;
eErrorID := 0;
sDiagMsg := '';
M_ResetError := TRUE;系列导航
- 系列定位:第 7 篇
- 上一篇:第6篇 PLC 里写 MQTT,最难的是状态机
- 下一篇:第8篇 怎么把这套开源 MQTT 客户端真正用起来
评论区预留
这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。