news 2026/8/23 8:13:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第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 -> 完整源码加更 -> 综合收束。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 8:13:03

嵌入式低代码开发实战:AWFlow图形化框架解析与应用

1. 项目概述&#xff1a;当嵌入式开发遇上低代码如果你是一名嵌入式软件工程师&#xff0c;或者正在学习嵌入式开发&#xff0c;那么下面这个场景你一定不陌生&#xff1a;为了在某个微控制器&#xff08;MCU&#xff09;上实现一个数据采集、处理并上报的简单功能&#xff0c;…

作者头像 李华
网站建设 2026/8/23 8:12:11

064、SQL跟踪与性能分析(ST05)

上个月&#xff0c;客户一个电话把我从午睡里拽了出来。说某个Z报表跑了一个小时还没出数&#xff0c;IT部门的人已经急得嘴上起泡。我打开那台测试机&#xff0c;用SE80把主流程翻了一遍&#xff0c;代码逻辑不算绕&#xff0c;循环里的SELECT也带了索引&#xff0c;怎么看都觉…

作者头像 李华
网站建设 2026/8/23 8:11:07

均匀随机相位下正弦信号幅度分布:从概率密度到工程应用

1. 从一个看似简单的问题开始最近在整理一些数学基础时&#xff0c;重新审视了一个非常经典的问题&#xff1a;正弦函数y sin(x)的概率分布。乍一看&#xff0c;这似乎是个纯理论推导&#xff0c;甚至有些“书呆子气”。但如果你从事信号处理、通信系统仿真、蒙特卡洛方法应用…

作者头像 李华
网站建设 2026/8/23 8:05:48

AI专利申请怎么写技术交底书

一、AI辅助发明的技术交底书&#xff0c;比传统发明多了一件事传统发明的技术交底书要写清楚3件事&#xff1a;技术问题、技术方案、技术效果。AI辅助发明的技术交底书要多写一件事&#xff1a;你对技术方案作出了什么实质性贡献——AI做了什么&#xff0c;你做了什么。为什么多…

作者头像 李华
网站建设 2026/8/23 8:05:33

Vue+Flask求职推荐系统:Apriori算法优化人岗匹配

1. 项目概述与核心价值 这个基于VueFlask技术栈的求职推荐系统&#xff0c;本质上是通过数据挖掘技术解决人岗匹配的效率问题。我在人力资源科技领域做了8年SaaS产品开发&#xff0c;发现传统招聘平台最大的痛点就是"简历海投"和"岗位轰炸"——求职者盲目投…

作者头像 李华
网站建设 2026/8/23 8:01:55

C++模板分离编译问题解析:从链接错误到模板特化实战

1. 从一次编译报错说起&#xff1a;为什么我的模板函数链接失败了&#xff1f; 最近在重构一个C项目时&#xff0c;我又一次掉进了那个熟悉的“坑”里。场景是这样的&#xff1a;我有一个通用的日志工具类&#xff0c;里面用到了函数模板来处理不同类型的日志格式化。为了代码结…

作者头像 李华