欢迎光临
我们一直在努力

第13篇_Client 02|TCP 连接成功,为什么 HTTP 请求还没有成功

适合谁收藏

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

本篇位置 客户端篇,第 2/7 篇;主系列第 13/28 篇。

现场问题

PLC 主动访问上位机 API 时,在线变量最早出现的成功信号通常是 TCP 连接成立。很多程序就在这里置位 Done,随后业务拿到的状态码仍然是 0。

HTTP Client 事务至少经过准备、连接、构造、发送、接收、解析和完成。任一步失败都需要不同诊断。

先给结论

Client 只有在响应 Parser 完成并输出有效状态码后才能置位请求完成。连接句柄和发送完成只是中间证据。

Client 02|TCP 连接成功,为什么 HTTP 请求还没有成功

读图重点

这张图只压缩本篇的判断路径。读图时先找“Idle/Prepare”对应的输入边界,再沿着“收齐并解析响应”检查状态怎样推进,最后用“决定 Done 或 Error”确认输出是否已经形成验收证据。

把对象和边界分开

对象或阶段工程职责现场观察点
Idle/Prepare 锁存参数并清理历史状态 避免上次事务残留
Connect 获得有效 TCP 句柄 只证明通道成立
Send 分片写完完整请求 不代表对端已处理
Receive/Parse 收齐并解析响应 决定 Done 或 Error

从协议约束到代码职责

协议约束

Client 只有在响应 Parser 完成并输出有效状态码后才能置位请求完成。连接句柄和发送完成只是中间证据。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。

工程抽象

Client 同时保留 CodeSys 风格的 xEnable/xExecute 和旧接口兼容输入,但内部会归一为一次真实事务。上升沿触发和周期调用必须同时满足,不能在一个扫描周期里反复重建请求。

连接状态、HTTP 错误和 NBS 错误分开输出。这样可以区分“端口拒绝”“响应超时”“状态行非法”和“Body 超限”。

  • Idle/Prepare:工程职责是“锁存参数并清理历史状态”。它不能只停留在命名层面,运行时必须能通过“避免上次事务残留”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Connect:工程职责是“获得有效 TCP 句柄”。它不能只停留在命名层面,运行时必须能通过“只证明通道成立”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Send:工程职责是“分片写完完整请求”。它不能只停留在命名层面,运行时必须能通过“不代表对端已处理”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Receive/Parse:工程职责是“收齐并解析响应”。它不能只停留在命名层面,运行时必须能通过“决定 Done 或 Error”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。

程序单元

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

本篇核心源码片段

下面两段代码来自同一个真实文件 FB_HttpClient.st,以 CASE eState OF 为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看“锁存参数并清理历史状态”怎样进入对象,以及“决定 Done 或 Error”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“TCP 错误与 HTTP 错误不能混成一个码。”,就不能把局部代码截图当成实现证据。

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

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,

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

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

sMessage := 'TCP connect timeout'
);
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

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

验证路径

场景操作通过口径
连接拒绝 目标端口无服务 TcpClientFailed 而非协议错误
连接后不响应 服务端接受但不返回 Timeout 且可复位
返回坏报文 状态行或 Header 非法 Parser 错误
正常事务 200 响应完整 Done、状态码和计数一致

场景 1:连接拒绝

把 Client 指向未监听的端口,并记录连接尝试开始到失败的时间。此时不应产生任何 HTTP 请求字节,也不应进入 Header 或 Body 解析;错误必须被标记为 TCP 建连失败。随后改回正常端口重试,若旧错误没有被显式清除或状态机不能从连接失败回到 Idle,说明复位边界仍不可靠。

场景 2:连接后不响应

让测试服务接受连接却故意不发送响应,验证 Client 从发送完成转入接收等待后能在设定超时到达时退出。在线量应显示连接曾建立、请求已发出、超时发生在响应阶段;缓冲区不能把空响应当成成功。复位之后再次发起正常 GET 必须获得完整响应,证明超时没有留下被占用的连接或半包状态。

场景 3:返回坏报文

由通信猫返回缺少 CRLF、非法状态行或截断 Header 的报文,观察解析器在哪个字段停止,而不是只看一个笼统的失败标志。验收要求是错误码能说明格式问题,已接收字节数可供复盘,并且业务回调不会得到部分 Body。若坏 Header 仍被当作 200 成功,后续的所有业务判断都会建立在错误报文上。

场景 4:正常事务

最后返回一个带明确 Content-Length 的 200 报文,逐项比对状态行、Header、Body、完成脉冲和统计计数。只有解析完成后才允许 Done,并且该次成功应与前面三种失败记录清晰分离。这个对照场景的价值是确认状态机不会因为先前的拒绝、超时或格式错而吞掉下一笔正常事务。

常见误判

  • TCP connect 一成功就置位 Done,应用随后读到状态码 0。
  • 发送完成后立即复位事务,响应晚一个周期到达时被当成无关数据。
  • 把端口拒绝、响应超时和状态行非法全部折叠成同一个 Error。

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

这一篇你最该记住

  • connect 不是 Client 的完成态。
  • 发送和响应必须分别验收。
  • TCP 错误与 HTTP 错误不能混成一个码。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 13/28 篇。
  • 阶段:客户端篇,职责线位置 2/7。
  • 上一篇:第12篇
  • 下一篇:第14篇
  • 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。
赞(0)
未经允许不得转载:171主机测评 » 第13篇_Client 02|TCP 连接成功,为什么 HTTP 请求还没有成功
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址