ControlRookie
返回文章

第12篇_Client 01|先让 PLC 完成第一次 HTTP GET

先用固定地址、固定路径、无 Body GET 和短连接跑通 Client 最小闭环,明确 Execute、Connect、Send、Receive、Done 各自代表什么。

先用固定地址、固定路径、无 Body GET 和短连接跑通 Client 最小闭环,明确 Execute、Connect、Send、Receive、Done 各自代表什么。

适合谁收藏

  • 正在让 PLC 主动访问 HTTP 服务的工程师。
  • 需要处理请求构造、响应边界、超时与连接复用的人。
  • 希望把 Client 故障定位到确定状态和错误出口的读者。

客户端篇,第 1/7 篇;主系列第 12/28 篇。

现场问题

Client 最容易出现的误判,是连接句柄一建立就认为请求成功。实际上此时还没有构造请求、没有发送字节,更没有收到状态码和 Body。

第一笔请求只访问一个已知服务的 /api/ping。它不追求复杂功能,只用最少变量证明 PLC 能主动完成完整 HTTP 事务。

先给结论

最小 Client 的成功条件是状态从 Idle 依次经过 TcpConnect、Send、Receive 到 Done,输出合法状态码和完整 Body。Connect 成功、Write 完成和 HTTP Done 是三个不同证据。

Client 01|先让 PLC 完成第一次 HTTP GET
Client 01|先让 PLC 完成第一次 HTTP GET

读图重点

这张图只压缩本篇的判断路径。读图时先找“Execute 上升沿”对应的输入边界,再沿着“Parser 确认响应完整”检查状态怎样推进,最后用“状态码、Header 和 Body 可消费”确认输出是否已经形成验收证据。

把对象和边界分开

对象或阶段工程职责现场观察点
Execute 上升沿锁存本次请求参数事务只启动一次
TcpConnect建立到目标 IP 与端口的通道句柄有效但请求尚未成功
Send / Receive发送请求并累积响应Tx/Rx 长度持续推进
DoneParser 确认响应完整状态码、Header 和 Body 可消费

从协议约束到代码职责

协议约束

最小 Client 的成功条件是状态从 Idle 依次经过 TcpConnect、Send、Receive 到 Done,输出合法状态码和完整 Body。Connect 成功、Write 完成和 HTTP Done 是三个不同证据。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。

工程抽象

首个 Client 用例固定使用 GET、短连接和短文本响应。固定条件能把地址错误、请求构造错误、发送错误和响应解析错误逐层暴露,而不是让 JSON、认证或业务动作干扰判断。

xExecute 只是启动命令,FB 仍需每扫描周期调用。Done 也应作为一次事务结果被上层消费,下一次请求开始前必须清理接收缓冲、偏移、超时和完成脉冲。

  • Execute 上升沿:工程职责是“锁存本次请求参数”。它不能只停留在命名层面,运行时必须能通过“事务只启动一次”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • TcpConnect:工程职责是“建立到目标 IP 与端口的通道”。它不能只停留在命名层面,运行时必须能通过“句柄有效但请求尚未成功”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Send / Receive:工程职责是“发送请求并累积响应”。它不能只停留在命名层面,运行时必须能通过“Tx/Rx 长度持续推进”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Done:工程职责是“Parser 确认响应完整”。它不能只停留在命名层面,运行时必须能通过“状态码、Header 和 Body 可消费”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。

程序单元

本篇主证据来自 PLC_PRG.st 中以 fbHttpClient( 为定位点的连续源码。这里不是为了展示语法,而是把协议约束落到确定程序单元:输入先进入结构体或缓冲区,状态机只在本周期处理可确认的部分,长度和结束条件决定能否前进,错误码与指标负责把失败原因带出对象边界。这样一来,“Connect 成功不等于 HTTP 成功。”可以在代码、在线变量和外部报文之间逐项对照,而不是依赖经验猜测。

本篇核心源码片段

下面两段代码来自同一个真实文件 PLC_PRG.st,以 fbHttpClient( 为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看“锁存本次请求参数”怎样进入对象,以及“状态码、Header 和 Body 可消费”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“只有完整响应进入 Done 才能交给应用。”,就不能把局部代码截图当成实现证据。

片段一:入口、声明与前置条件

iecst
    sRxMessage           => GVL_HttpRealTest.sServerRxMessage,
    sTxMessage           => GVL_HttpRealTest.sServerTxMessage,
    sLastTarget          => GVL_HttpRealTest.sServerLastTarget,
    sLastBody            => GVL_HttpRealTest.sServerLastBody,
    hListenHandle        => GVL_HttpRealTest.hServerListenHandle,
    aConnectionSnapshots => aServerSnapshots
    );

fbHttpClient(
    xEnable           := GVL_HttpRealTest.bClientEnable,
    xExecute          := GVL_HttpRealTest.bClientSend,
    xReset            := GVL_HttpRealTest.bResetResults,
    xAbort            := GVL_HttpRealTest.bClientAbort,
    xCloseConnection  := GVL_HttpRealTest.bClientCloseConnection,
    bEnable           := GVL_HttpRealTest.bClientEnable,
    bSend             := GVL_HttpRealTest.bClientSend,
    udiTimeOut        := GVL_HttpRealTest.udiClientTimeoutUs,
    sURL              := GVL_HttpRealTest.sClientURL,
    sServerIP         := GVL_HttpRealTest.sClientServerIP,
    uiPort            := GVL_HttpRealTest.uiClientPort,
    sHost             := GVL_HttpRealTest.sClientHost,
    sPath             := GVL_HttpRealTest.sClientPath,
    eRequestType      := GVL_HttpRealTest.eClientMethod,
    eMethod           := GVL_HttpRealTest.eClientMethod,
    sBody             := GVL_HttpRealTest.sClientBody,
    sContentType      := GVL_HttpRealTest.sClientContentType,
    sAdditionalHeader := GVL_HttpRealTest.sClientAdditionalHeader,
    udiNowMs          := GVL_HttpRealTest.udiNowMs,
    xActive           => GVL_HttpRealTest.bClientTcpConnected,
    xConnected        => GVL_HttpRealTest.bClientTcpConnected,
    xBusy             => GVL_HttpRealTest.bClientBusy,
    xDone             => GVL_HttpRealTest.bClientDone,
    xError            => GVL_HttpRealTest.bClientError,
    bTcpConnected     => GVL_HttpRealTest.bClientTcpConnected,
    bBusy             => GVL_HttpRealTest.bClientBusy,
    bDone             => GVL_HttpRealTest.bClientDone,
    bError            => GVL_HttpRealTest.bClientError,

这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件,不能只看某个布尔量是否变成 TRUE。

片段二:状态推进、边界与输出

iecst
    diErrorID         => GVL_HttpRealTest.diClientErrorID,
    sDiagMsg          => GVL_HttpRealTest.sClientDiagMsg,
    eLastNbsError     => GVL_HttpRealTest.eClientLastNbsError,
    eState            => GVL_HttpRealTest.eClientState,
    eLastError        => GVL_HttpRealTest.eClientLastError,
    uiStatusCode      => GVL_HttpRealTest.uiClientStatusCode,
    sTxMessage        => GVL_HttpRealTest.sClientTxMessage,
    sRxMessage        => GVL_HttpRealTest.sClientRxMessage,
    sResponseBody     => GVL_HttpRealTest.sClientResponseBody,
    sResponseHeader   => GVL_HttpRealTest.sClientResponseHeader,
    stMetrics         => stClientMetrics,
    stResponse        => stClientResponse
    );

IF GVL_HttpRealTest.bClientSend THEN
    GVL_HttpRealTest.bClientSend := FALSE;
END_IF

IF GVL_HttpRealTest.bClientAbort THEN
    GVL_HttpRealTest.bClientAbort := FALSE;
END_IF

IF GVL_HttpRealTest.bResetResults THEN
    GVL_HttpRealTest.bResetResults := FALSE;
END_IF

GVL_HttpRealTest.udiServerAcceptedCount := stServerMetrics.udiAcceptedConnections;
GVL_HttpRealTest.udiServerRequestCount := stServerMetrics.udiRequestCount;
GVL_HttpRealTest.udiServerResponseCount := stServerMetrics.udiResponseCount;
GVL_HttpRealTest.udiServerProtocolErrorCount := stServerMetrics.udiProtocolErrorCount;
GVL_HttpRealTest.udiClientRequestCount := stClientMetrics.udiRequestCount;
GVL_HttpRealTest.udiClientResponseCount := stClientMetrics.udiResponseCount;
GVL_HttpRealTest.udiClientConnectErrorCount := stClientMetrics.udiConnectErrorCount;
GVL_HttpRealTest.udiClientProtocolErrorCount := stClientMetrics.udiProtocolErrorCount;
GVL_HttpRealTest.sClientReason := stClientResponse.sReason;

IF (GVL_HttpRealTest.udiServerRequestCount > 0)

第二段继续展示同一连续源码范围。把它与第一段合起来,才能判断输入怎样被锁存、状态何时推进、边界何时满足,以及错误出口是否保留了足够诊断信息。

验证路径

场景操作通过口径
正常 GET访问已知 /api/ping状态码 200,Body 完整
端口拒绝改为无监听端口停在连接层并保留 NBS 错误
合法 404请求不存在路径事务完成,业务状态码保留
再次执行复位后重复 GET旧响应和旧错误不残留

场景 1:正常 GET

把目标固定为已知可用的 /api/ping,从启动脉冲开始观察 Client 的连接、发送、接收和完成状态。外部服务返回 200 只是第一层;还要确认 PLC 保存的状态码、Body 长度和 Done 在同一事务里出现,且下一周期不会重复触发。这样能把“第一次 GET 成功”与“碰巧收到了一个字符串”区分开。

场景 2:端口拒绝

把端口改为一个没有监听服务的值,确认 Client 在 TCP 建连阶段超时或拒绝,而不是误报 HTTP 解析失败。错误码应来自连接层,响应缓冲区必须保持为空,复位后能立即重新发起一次正常请求。这个场景证明网络故障和 HTTP 业务失败被分开处理,排障时不会沿错方向查报文。

场景 3:合法 404

请求一个服务端明确不存在的 URI,预期 TCP 和 HTTP 事务正常结束,但业务结果为 404。PLC 侧应保留完整状态码和响应 Body,同时把传输完成与业务失败分别表达;若 404 被直接映射成 Client Error,调用方就无法判断是地址写错还是链路断开。验收重点正是这两个层次不能混在一个布尔量里。

场景 4:再次执行

先完成一次成功 GET,再复位并切换到另一条路径重新执行。第二次结果必须覆盖第一次的状态、Body 和诊断,不能因旧 Done 脉冲或旧错误码而提前结束。建议把两次请求的序号和返回内容同时放到在线监视表中;只有它们一一对应,Client 才具备在循环程序里反复使用的条件。

常见误判

  • 连接句柄一建立就置位 Done,应用拿到的状态码仍是 0。
  • Execute 保持 TRUE 导致同一请求被反复触发。
  • 第一次请求完成后不清理 Rx/Tx 偏移和超时,第二次请求被旧状态污染。

这些误判的共同点,是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标,并用相同输入完成回归。

这一篇你最该记住

  • Connect 成功不等于 HTTP 成功。
  • Execute 触发事务,周期调用推进事务。
  • 只有完整响应进入 Done 才能交给应用。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 12/28 篇。
  • 阶段:客户端篇,职责线位置 1/7。
  • 上一篇:第11篇
  • 下一篇:第13篇
  • 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。
评论和回复区

评论区预留

这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。

↑ ↓