第18篇_Client 07|超时、断线、重试和真机验证怎样收口

发布时间:2026/8/23 8:13:44
第18篇_Client 07|超时、断线、重试和真机验证怎样收口 适合谁收藏正在让 PLC 主动访问 HTTP 服务的工程师。需要处理请求构造、响应边界、超时与连接复用的人。希望把 Client 故障定位到确定状态和错误出口的读者。本篇位置客户端篇第 7/7 篇主系列第 18/28 篇。现场问题最危险的处理方式是 Client 一报错就立即重发。对于读取类请求可能只是重复流量对于带动作的 POST则可能让服务端执行两次。PLC 需要先判断事务在哪一步失败、请求是否可能已经送达、方法是否允许重试再决定关闭、复位或等待人工处理。先给结论恢复流程固定为锁存错误证据、停止当前读写、关闭失效句柄、清理事务缓冲区、回到可控状态。是否重试由上层策略决定协议栈不无条件重复业务请求。读图重点这张图只压缩本篇的判断路径。读图时先找“连接失败”对应的输入边界再沿着“对端返回不可解析消息”检查状态怎样推进最后用“记录原始响应并停止盲重试”确认输出是否已经形成验收证据。把对象和边界分开对象或阶段工程职责现场观察点连接失败请求尚未发送可按次数和退避策略重试发送中断服务端是否收到不确定保留请求标识并谨慎重试响应超时请求可能已执行先查询结果或按业务幂等判断协议错误对端返回不可解析消息记录原始响应并停止盲重试从协议约束到代码职责协议约束恢复流程固定为锁存错误证据、停止当前读写、关闭失效句柄、清理事务缓冲区、回到可控状态。是否重试由上层策略决定协议栈不无条件重复业务请求。 这条结论先限定消息什么时候成立再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务半包、超时、重复执行和连接残留就会进入应用层。工程抽象当前工程暴露超时、NBS 错误、HTTP 错误、最近响应和 ClientMetrics。真机测试应覆盖端口拒绝、服务端延迟、半响应、主动关闭和恢复后的下一次正常请求。文章只把“自动重试”作为有前提的策略不把它写成默认最佳实践。工业现场更看重动作唯一性和可追溯性。连接失败TCP 尚未建立请求字节没有离开 PLC。记录目标地址、端口、连接状态和底层错误后可以按限定次数与退避时间重连。发送中断部分字节可能已经到达服务端不能仅凭 Write 失败判断业务没有执行。必须保留请求标识、已发送长度和失败时刻再由上层决定是否补偿。响应超时请求可能已经完整送达并被处理只是响应没有在期限内返回。GET 可在策略允许时重试带动作的 POST 应先查询结果或依赖幂等键禁止直接重复执行。协议错误连接存在但报文不能被 Parser 接受。此时保存原始响应、解析阶段和 HTTP 错误码关闭当前连接并停止自动重试避免同一坏报文反复消耗扫描周期。程序单元本篇主证据来自FB_HttpClient.st中以E_HttpClientState.iFault为定位点的连续源码。这里不是为了展示语法而是把协议约束落到确定程序单元输入先进入结构体或缓冲区状态机只在本周期处理可确认的部分长度和结束条件决定能否前进错误码与指标负责把失败原因带出对象边界。这样一来“先判断失败阶段再谈重试。”可以在代码、在线变量和外部报文之间逐项对照而不是依赖经验猜测。本篇核心源码片段下面两段代码来自同一个真实文件FB_HttpClient.st以E_HttpClientState.iFault为中心连续截取没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件第二段用于确认状态、边界和输出。核对时重点看“请求尚未发送”怎样进入对象以及“记录原始响应并停止盲重试”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“恢复前必须保留错误和原始报文证据。”就不能把局部代码截图当成实现证据。片段一入口、声明与前置条件hConnection : hConnection, szSize : 0, pData : ADR(aTxBuf) ); tonWrite( IN : FALSE, PT : GVL_Http.cnWriteTimeout ); M_Reset(); RETURN; END_IF bDone : FALSE; CASE eState OF E_HttpClientState.iDisabled: eState : E_HttpClientState.iIdle; E_HttpClientState.iIdle: bBusy : FALSE; IF rtrigSend.Q THEN M_PrepareRequest(); stMetrics.udiRequestCount : stMetrics.udiRequestCount 1; IF NOT bRequestQueued THEN eState : E_HttpClientState.iFault; ELSIF (hConnection 0) AND bTcpConnected AND NOT stRequest.bConnectionClose THEN bReuseAttempt : TRUE; eState : E_HttpClientState.iSend; ELSE bReuseAttempt : FALSE; eState : E_HttpClientState.iTcpConnect; END_IF END_IF E_HttpClientState.iTcpConnect: bBusy : TRUE; fbTcpClient( xEnable : TRUE, udiTimeOut : udiTimeOut, ipAddr : ipServer, uiPort : uiEffectivePort, eError eTcpError, hConnection hConnection ); bTcpConnected : fbTcpClient.xActive AND (hConnection 0); IF fbTcpClient.xError THEN eLastNbsError : eTcpError; stMetrics.udiConnectErrorCount : stMetrics.udiConnectErrorCount 1; M_SetClientError( eError : E_HttpError.iTcpClientFailed, sMessage : TCP connect failed ); eState : E_HttpClientState.iFault; ELSIF bTcpConnected THEN eState : E_HttpClientState.iSend; ELSIF (udiNowMs 0) AND ((udiNowMs - udiStateEnterMs) udiTimeoutMs) THEN M_SetClientError( eError : E_HttpError.iTimeout, sMessage : TCP connect timeout这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件不能只看某个布尔量是否变成 TRUE。片段二状态推进、边界与输出); eState : E_HttpClientState.iFault; END_IF E_HttpClientState.iSend: bBusy : TRUE; fbTcpClient( xEnable : TRUE, udiTimeOut : udiTimeOut, ipAddr : ipServer, uiPort : uiEffectivePort, eError eTcpError, hConnection hConnection ); M_ServiceWrite(); IF (NOT bWriteBusy) AND (NOT bWriteExecute) THEN eState : E_HttpClientState.iReceive; END_IF E_HttpClientState.iReceive: bBusy : TRUE; fbTcpClient( xEnable : TRUE, udiTimeOut : udiTimeOut, ipAddr : ipServer, uiPort : uiEffectivePort, eError eTcpError, hConnection hConnection ); M_ServiceRead(); M_ProcessResponse(); IF (udiNowMs 0) AND ((udiNowMs - udiStateEnterMs) udiTimeoutMs) THEN M_SetClientError( eError : E_HttpError.iTimeout, sMessage : HTTP response timeout ); eState : E_HttpClientState.iFault; END_IF E_HttpClientState.iDone: bBusy : FALSE; bDone : TRUE; IF stRequest.bConnectionClose OR xCloseConnection THEN fbTcpClient( xEnable : FALSE, ipAddr : ipServer, uiPort : uiEffectivePort, hConnection hConnection ); bTcpConnected : FALSE; ELSE fbTcpClient( xEnable : TRUE, udiTimeOut : udiTimeOut, ipAddr : ipServer, uiPort : uiEffectivePort, eError eTcpError, hConnection hConnection );第二段继续展示同一连续源码范围。把它与第一段合起来才能判断输入怎样被锁存、状态何时推进、边界何时满足以及错误出口是否保留了足够诊断信息。验证路径场景操作通过口径端口拒绝无监听服务连接错误并可恢复响应延迟超过设定超时Timeout 证据完整中途断开Body 未收齐时关闭不输出成功故障后恢复服务恢复再触发请求旧状态不污染新事务场景 1端口拒绝把目标端口改为确定没有服务监听的端口触发一次 GET。Client 应停在连接阶段bDone不得出现连接错误与失败计数只锁存一次复位后句柄回到无效值状态重新进入 Idle。这个场景证明请求尚未发送时可以安全重连。场景 2响应延迟让测试服务接收请求后故意延迟响应延迟时间必须大于 Client 的接收超时。检查状态先进入 Receive再以iTimeout收口同时保留已发送请求和等待时长不能因为没有响应就把本次事务记为未发送也不能在协议栈内部自动重放 POST。场景 3中途断开让服务端声明一个确定的Content-Length只发送部分 Body 后主动断开。Parser 必须保持消息未完成Client 输出传输或协议错误已有半包不能进入业务响应关闭并清理后接收缓冲区长度应回到零。场景 4故障后恢复在完成一次端口拒绝或中途断开后恢复服务再由上层明确触发一笔新 GET。新事务应建立新连接、重新累计 Tx/Rx并得到合法状态码与完整 Body旧错误可供历史诊断但不得继续拉高bError或污染新响应。常见误判任何错误都立即重试没有判断请求是否可能已经送达服务端。延长超时掩盖状态机没有推进最终只是让故障更晚暴露。故障后直接发下一条请求没有先关闭句柄和清理接收缓冲区。这些误判的共同点是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标并用相同输入完成回归。这一篇你最该记住先判断失败阶段再谈重试。POST 超时不能默认安全重发。恢复前必须保留错误和原始报文证据。系列导航系列CodeSys HTTP 系列教程第 18/28 篇。阶段客户端篇职责线位置 7/7。上一篇第17篇下一篇第19篇发布顺序基础认知 - Server - Client - 完整源码加更 - 综合收束。