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

适合谁收藏

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

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

现场问题

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

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

先给结论

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

读图重点

这张图只压缩本篇的判断路径。读图时先找"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 错误不能混成一个码。",就不能把局部代码截图当成实现证据。

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

iecst 复制代码
        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。

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

iecst 复制代码
                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 -> 完整源码加更 -> 综合收束。
相关推荐
IPdodo跨境网络1 天前
HTTP 与 SOCKS5 代理到底差在哪?用 Node.js 跑一次连接链路对比
http
小小龙学IT1 天前
gRPC 开源高性能 RPC 框架深度解析:从 HTTP/2 到跨语言微服务实战
http·rpc·开源
Misnearch2 天前
MCP以及底层协议、传输模式
http·mcp·json-rpc
Aision_3 天前
实习学习笔记:HTTP请求走私漏洞全方位解析
笔记·学习·安全·web安全·http·网络安全·网络攻击模型
zhao3266857513 天前
除了网页浏览,HTTP和HTTPS代理还能干啥?适用场景有哪些
网络协议·http·https
沐苏瑶3 天前
计算机网络核心笔记:打通 TCP/UDP 与 HTTP/HTTPS 底层逻辑(重点下)
笔记·计算机网络·http
NeilYuen4 天前
实现TCP发送HTTP请求
网络协议·tcp/ip·http
wuhuhuan4 天前
Day17:HTTP 接口测试与链式调用 — sendRequest 从入门到实战
网络·网络协议·http
执笔画流年呀4 天前
网络原理(http)(https)
网络·http·https