ControlRookie
返回文章

第24篇_源码加更 06|HTTP Client 状态机和运行指标

Client 源码展示一次主动请求怎样跨越连接、构造、发送、接收、解析和恢复阶段。

Client 源码展示一次主动请求怎样跨越连接、构造、发送、接收、解析和恢复阶段。

适合谁收藏

  • 准备复用本组源码并审查对象边界的工程师。
  • 需要把公开代码装配进目标 CodeSys 工程的人。
  • 准备重新完成编译、离线测试和真机验证的读者。

本篇完整公开 3 个 ST 文件。代码直接读取已验证工程,保留声明、实现、注释和缩进;没有伪代码,没有跨文件拼接,也没有省略号。

先给结论

Client 源码展示一次主动请求怎样跨越连接、构造、发送、接收、解析和恢复阶段。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

源码加更 06|HTTP Client 状态机和运行指标
源码加更 06|HTTP Client 状态机和运行指标

读图重点

先找到 E_HttpClientState 在本组中的位置,再沿图确认其余对象分别承担数据、状态、执行或诊断职责。图只给阅读顺序,最终判断必须回到下面的完整 ST 代码。

先看文件职责

序号文件职责行数
1E_HttpClientState.stClient 事务状态15
2FB_HttpClient.st主动请求状态机834
3ST_HttpClientMetrics.stClient 运行指标11

阅读顺序不是按文件名机械展开。先看数据和状态,再看公开方法,最后顺着错误出口和调用对象检查边界。源码篇的目标是让读者能对照工程复现,不是用大段代码制造篇幅。

从协议约束到代码职责

协议约束

Client 源码展示一次主动请求怎样跨越连接、构造、发送、接收、解析和恢复阶段。 协议层只定义消息与状态成立的条件,工程层还必须把条件分配给确定对象,避免 Parser、Builder、Server、Client 和测试入口互相越权。

对象分工

  • 第 1 个对象 E_HttpClientState.st:承担“Client 事务状态”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 2 个对象 FB_HttpClient.st:承担“主动请求状态机”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 3 个对象 ST_HttpClientMetrics.st:承担“Client 运行指标”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。

装配与验证

这组文件必须作为一个职责单元阅读和编译。先用确定输入验证“Execute 只触发事务,FB 仍需周期调用”,再制造非法或容量边界验证“完成条件是响应解析结束”,最后在真实通信或上层调用中确认“连接错误和协议错误分开输出”。如果单文件测试通过但装配后失败,应优先检查结构体、常量、状态枚举和周期调用关系,而不是立即重写核心算法。源码完整公开只证明读者拿到了同一事实源,目标运行时是否通过仍需重新编译和真机取证。

本篇从 E_HttpClientState.st 开始审查:先确认“Execute 只触发事务,FB 仍需周期调用”对应的类型、常量或公开输入,再沿 CASE 或方法调用追踪成功路径,随后逐个检查容量、超时和协议错误出口,最后在 ST_HttpClientMetrics.st 对应的结果或装配位置确认错误能被观测、清理并再次执行。这个顺序专门用来区分“算法正确但没有周期调用”“状态能完成但错误被覆盖”和“连接错误和协议错误分开输出尚未形成验证证据”三类问题。只有本篇源码、装配和验证使用同一组对象与同一组边界,完整开源才具有可复现意义。

本篇完整开源代码

下面按职责顺序给出本篇全部 ST 文件。每个代码块保留完整声明与实现;阅读时把状态、长度、错误出口和调用对象与上面的职责表逐项对照。

完整源码 1:E_HttpClientState.st

职责:Client 事务状态

iecst
/// 功能    : HTTP Client 状态枚举。
/// 库依赖 : 暂无
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_HttpClientState :
(
    iDisabled   := 0,
    iIdle       := 1,
    iTcpConnect := 2,
    iSend       := 3,
    iReceive    := 4,
    iDone       := 5,
    iFault      := 6
) INT;
END_TYPE

完整源码 2:FB_HttpClient.st

职责:主动请求状态机

iecst
/// 功能    : 基于 NBS 的 HTTP Client。
/// 库依赖 : CAA Net Base Services
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_HttpClient
VAR_INPUT
    xEnable          : BOOL := FALSE;                                   // CODESYS 风格使能输入。
    xExecute         : BOOL := FALSE;                                   // CODESYS 风格发送命令上升沿。
    xReset           : BOOL := FALSE;                                   // 复位命令,TRUE 时清空状态和诊断。
    xAbort           : BOOL := FALSE;                                   // 中止命令,TRUE 时关闭当前连接并复位事务。
    xCloseConnection : BOOL := FALSE;                                   // TRUE 时每次响应后关闭 TCP,FALSE 时顺序长连接。
    bEnable          : BOOL := FALSE;                                   // 兼容旧接口的使能输入。
    bSend            : BOOL := FALSE;                                   // 兼容旧接口的发送命令上升沿。
    udiTimeOut       : UDINT := GVL_Http.cnTcpConnectTimeoutUs;         // CODESYS 风格超时,单位 [us]。
    sURL             : STRING(1024) := '';                              // URL 输入,支持 http://host[:port]/path?query。
    sServerIP        : STRING := '127.0.0.1';                           // 远端 HTTP Server IPv4 地址字符串。
    uiPort           : UINT := GVL_Http.cnDefaultServerPort;            // 远端 TCP 端口。
    sHost            : STRING(GVL_Http.cnMaxHostLen) := '127.0.0.1';    // HTTP Host 字段。
    sPath            : STRING(GVL_Http.cnMaxTargetLen) := '/';          // HTTP 请求路径。
    eRequestType     : E_HttpMethod := E_HttpMethod.iGet;               // CODESYS 风格请求方法。
    eMethod          : E_HttpMethod := E_HttpMethod.iGet;               // 兼容旧接口的 HTTP 方法。
    sBody            : STRING(GVL_Http.cnMaxBodySize) := '';            // 出站请求 body。
    sContentType     : STRING(96) := 'text/plain';                      // 出站 Content-Type。
    sAdditionalHeader: STRING(GVL_Http.cnMaxHeaderSize) := '';          // 自定义 Header 行,不包含结尾空行。
    udiNowMs         : UDINT := 0;                                      // 外部毫秒时钟,用于超时诊断。
END_VAR
VAR_OUTPUT
    xActive         : BOOL;                                 // TCP 连接保持状态。
    xConnected      : BOOL;                                 // TCP 连接有效快照。
    xBusy           : BOOL;                                 // 正在建链、发送或接收。
    xDone           : BOOL;                                 // 本次事务完成。
    xError          : BOOL;                                 // 错误锁存。
    bTcpConnected   : BOOL;                                 // TCP 连接有效快照。
    bBusy           : BOOL;                                 // 正在建链、发送或接收。
    bDone           : BOOL;                                 // 本次事务完成。
    bError          : BOOL;                                 // 错误锁存。
    diErrorID       : DINT;                                 // 错误诊断码。
    sDiagMsg        : STRING(255);                          // 最近一次诊断文本。
    eLastNbsError   : NBS.ERROR;                            // 最近一次 NBS 错误。
    eState          : E_HttpClientState;                    // Client 状态机。
    eLastError      : E_HttpError;                          // 最近一次 HTTP 错误。
    uiStatusCode    : UINT;                                 // 最近一次 HTTP 响应状态码。
    sTxMessage      : STRING(GVL_Http.cnMaxMessageSize);    // 最近一次发送报文文本。
    sRxMessage      : STRING(GVL_Http.cnMaxMessageSize);    // 最近一次接收报文文本。
    sResponseBody   : STRING(GVL_Http.cnMaxBodySize);       // 最近一次响应 body。
    sResponseHeader : STRING(GVL_Http.cnMaxHeaderSize);     // 最近一次响应 Header。
    stMetrics       : ST_HttpClientMetrics;                 // 运行统计。
    stResponse      : ST_HttpResponse;                      // 最近一次响应。
END_VAR
VAR
    fbTcpClient       : NBS.TCP_Client;                                      // NBS TCP_Client 实例,必须周期调用。
    fbTcpRead         : NBS.TCP_Read;                                        // NBS TCP_Read 实例,必须周期调用。
    fbTcpWrite        : NBS.TCP_Write;                                       // NBS TCP_Write 实例,必须周期调用。
    fbBuilder         : FB_HttpMessageBuilder;                               // HTTP 请求构造器。
    fbParser          : FB_HttpMessageParser;                                // HTTP 响应解析器。
    rtrigSend         : R_TRIG;                                              // 发送命令上升沿检测。
    tonWrite          : TON;                                                 // 写超时定时器。
    ipServer          : NBS.IP_ADDR;                                         // NBS 远端 IP 地址。
    hConnection       : NBS.CAA.HANDLE;                                      // 当前 TCP 连接句柄。
    aRxBuf            : ARRAY[0..GVL_Http.cnRxBufferSize - 1] OF BYTE;       // 接收缓冲。
    aTxBuf            : ARRAY[0..GVL_Http.cnTxBufferSize - 1] OF BYTE;       // 发送缓冲。
    stRequest         : ST_HttpRequest;                                      // 当前请求结构。
    sRequestText      : STRING(GVL_Http.cnMaxMessageSize);                   // 当前请求文本。
    sResponseText     : STRING(GVL_Http.cnMaxMessageSize);                   // 当前响应文本。
    uiRxLength        : UINT;                                                // 接收有效字节数。
    uiWriteLen        : UINT;                                                // 待发送字节数。
    uiIndex           : UINT;                                                // 循环索引。
    bWriteExecute     : BOOL;                                                // TCP_Write 执行位。
    bWriteBusy        : BOOL;                                                // TCP_Write 忙状态。
    bWriteDoneLatched : BOOL;                                                // TCP_Write 完成锁存。
    eTcpError         : NBS.ERROR;                                           // 当前 NBS 错误。
    ePrevState        : E_HttpClientState := E_HttpClientState.iDisabled;    // 上一扫描状态。
    udiStateEnterMs   : UDINT;                                               // 状态进入时间。
    bRunEnable        : BOOL;                                                // 兼容旧接口和新接口后的实际使能。
    bExecuteCommand   : BOOL;                                                // 兼容旧接口和新接口后的发送命令。
    bRequestQueued    : BOOL;                                                // 当前事务请求已构造并进入发送缓冲。
    bReuseAttempt     : BOOL;                                                // 当前事务是否复用了上一事务保留的 TCP 句柄。
    bRetryAfterStale  : BOOL;                                                // 当前事务是否已因失效长连接重连重试。
    sEffectiveServerIP: STRING;                                              // URL 解析后的 TCP 建链地址。
    sEffectiveHost    : STRING(GVL_Http.cnMaxHostLen);                       // URL 解析后的 Host 字段。
    sEffectivePath    : STRING(GVL_Http.cnMaxTargetLen);                     // URL 解析后的请求路径。
    uiEffectivePort   : UINT;                                                // URL 解析后的端口。
    eEffectiveMethod  : E_HttpMethod;                                        // 兼容旧接口和新接口后的请求方法。
    udiTimeoutMs      : UDINT;                                               // 当前事务超时时间,单位 [ms]。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : FB_HttpClient
/// 功能      : 基于 CAA Net Base Services 实现 HTTP/1.1 Client 事务和连接策略。
/// 库依赖    : CAA Net Base Services
/// =======================================================================
/// 使用说明  : 1. 每次 xExecute 或 bSend 上升沿发起一次请求。
///           : 2. xCloseConnection 为 FALSE 时保持顺序长连接,不支持 pipeline。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:Client 对外提供 CODESYS 风格命令引脚,同时保留旧接口,迁移时不破坏既有真机脚本。
// 约束:xExecute/bSend 只作为上升沿命令,状态机未回到 Done/Idle 前不得覆盖当前事务缓冲。
// 风险:NBS 连接、写入和读取错误必须分别计数,真机排查时才能区分链路故障和 HTTP 协议故障。
// 诊断:stResponse、eLastError、eLastNbsError 和 stMetrics 是外部测试脚本判定真实通信的唯一出口。
bRunEnable := xEnable OR bEnable;
bExecuteCommand := xExecute OR bSend;
IF udiTimeOut > 0 THEN
    udiTimeoutMs := udiTimeOut / 1000;
    IF udiTimeoutMs = 0 THEN
        udiTimeoutMs := 1;
    END_IF
ELSE
    udiTimeoutMs := GVL_Http.cnClientTimeoutMs;
END_IF

rtrigSend(
    CLK := bExecuteCommand
    );

IF eState <> ePrevState THEN
    ePrevState := eState;
    udiStateEnterMs := udiNowMs;
END_IF

IF uiEffectivePort = 0 THEN
    uiEffectivePort := uiPort;
END_IF
IF LEN(sEffectiveServerIP) = 0 THEN
    sEffectiveServerIP := sServerIP;
END_IF
ipServer.sAddr := sEffectiveServerIP;

IF xReset OR xAbort THEN
    fbTcpClient(
        xEnable     := FALSE,
        ipAddr      := ipServer,
        uiPort      := uiPort,
        hConnection => hConnection
        );
    M_Reset();
    RETURN;
END_IF

IF NOT bRunEnable THEN
    // 原因:外部 M_Reset 可能已把 hConnection 清零,但 NBS 读写 FB 仍需要一个禁用扫描来释放上一事务状态。
    fbTcpClient(
        xEnable     := FALSE,
        ipAddr      := ipServer,
        uiPort      := uiPort,
        hConnection => hConnection
        );
    fbTcpRead(
        xEnable     := FALSE,
        hConnection := hConnection,
        szSize      := 0,
        pData       := ADR(aRxBuf)
        );
    fbTcpWrite(
        xExecute    := FALSE,
        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'
                );
            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
                );
            bTcpConnected := fbTcpClient.xActive AND (hConnection <> 0);
        END_IF
        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.iFault:
        bBusy := FALSE;
        fbTcpClient(
            xEnable     := FALSE,
            ipAddr      := ipServer,
            uiPort      := uiEffectivePort,
            hConnection => hConnection
            );
ELSE
    eState := E_HttpClientState.iFault;
END_CASE

M_UpdateClientOutputs();

// === METHOD M_PrepareRequest ===
/// =======================================================================
/// 名称      : M_PrepareRequest
/// 功能      : 从输入引脚生成当前事务请求结构。
/// 说明      : URL 优先,离散 IP/Host/Path 引脚作为兼容输入。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_PrepareRequest
VAR
    sWork        : STRING(1024);    // URL 去掉 scheme 后的临时文本。
    sHostPort    : STRING(255);     // Host[:Port] 临时文本。
    sPortText    : STRING(16);      // 端口文本。
    iSlash       : INT;             // 路径分隔符位置。
    iColon       : INT;             // 端口分隔符位置。
    uiParsedPort : UINT;            // URL 中解析出的端口。
END_VAR
// === IMPLEMENTATION ===
// 原因:CODESYS WebClient 习惯以 sURL 为主入口;旧项目仍常用 IP、Host、Path 分离配置,二者必须可兼容。
// 约束:首版只处理明文 HTTP URL;TLS/证书/认证已经明确排除,https 输入直接进入可诊断错误。
M_ResetTransaction();

eEffectiveMethod := eRequestType;
IF (eRequestType = E_HttpMethod.iGet) AND (eMethod <> E_HttpMethod.iGet) THEN
    eEffectiveMethod := eMethod;
END_IF
sEffectiveServerIP := sServerIP;
sEffectiveHost := sHost;
sEffectivePath := sPath;
uiEffectivePort := uiPort;

IF LEN(sURL) > 0 THEN
    IF FIND(sURL, 'https://') = 1 THEN
        M_SetClientError(
            eError   := E_HttpError.iInvalidArgument,
            sMessage := 'HTTPS URL is not supported'
            );
        RETURN;
    ELSIF FIND(sURL, 'http://') = 1 THEN
        sWork := MID(sURL, LEN(sURL) - 7, 8);
    ELSE
        sWork := sURL;
    END_IF

    iSlash := FIND(sWork, '/');
    IF iSlash > 0 THEN
        sHostPort := LEFT(sWork, iSlash - 1);
        sEffectivePath := MID(sWork, LEN(sWork) - iSlash + 1, iSlash);
    ELSE
        sHostPort := sWork;
        sEffectivePath := GVL_Http.cnDefaultPath;
    END_IF

    IF LEN(sHostPort) > 0 THEN
        sEffectiveServerIP := sHostPort;
        sEffectiveHost := sHostPort;
        iColon := FIND(sHostPort, ':');
        IF iColon > 0 THEN
            sEffectiveServerIP := LEFT(sHostPort, iColon - 1);
            sPortText := MID(sHostPort, LEN(sHostPort) - iColon, iColon + 1);
            uiParsedPort := STRING_TO_UINT(sPortText);
            IF uiParsedPort > 0 THEN
                uiEffectivePort := uiParsedPort;
            END_IF
        END_IF
    END_IF
END_IF

IF LEN(sEffectivePath) = 0 THEN
    sEffectivePath := GVL_Http.cnDefaultPath;
END_IF
IF LEN(sEffectiveHost) = 0 THEN
    sEffectiveHost := sEffectiveServerIP;
END_IF

ipServer.sAddr := sEffectiveServerIP;
stRequest.eMethod := eEffectiveMethod;
stRequest.sMethod := F_HttpMethodToString(
    eMethod := eEffectiveMethod
    );
IF LEN(sEffectivePath) = 0 THEN
    stRequest.sTarget := GVL_Http.cnDefaultPath;
ELSE
    stRequest.sTarget := sEffectivePath;
END_IF
stRequest.sVersion := 'HTTP/1.1';
stRequest.sHost := sEffectiveHost;
stRequest.sContentType := sContentType;
stRequest.sAdditionalHeader := sAdditionalHeader;
stRequest.sBody := sBody;
stRequest.bConnectionClose := xCloseConnection;
stRequest.bHasContentLength := LEN(sBody) > 0;
stRequest.udiContentLength := TO_UDINT(LEN(sBody));

// 诊断:Builder 错误直接映射到 Client 错误出口,保证 URL/Header/长度问题能被外部读出。
IF fbBuilder.M_BuildRequest(
    stRequest := stRequest,
    sMessage  => sRequestText
    ) THEN
    IF M_QueueRawString(
        sText := sRequestText
        ) THEN
        bRequestQueued := TRUE;
    END_IF
ELSE
    M_SetClientError(
        eError   := fbBuilder.eError,
        sMessage := fbBuilder.sDiagMsg
        );
END_IF

// === METHOD M_ServiceRead ===
/// =======================================================================
/// 名称      : M_ServiceRead
/// 功能      : 周期调用 NBS.TCP_Read 接收响应字节。
/// 说明      : TCP 是字节流,读取结果追加到接收缓冲,解析层负责半包判断。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ServiceRead
VAR
    udiFreeBytes : UDINT;    // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
udiFreeBytes := TO_UDINT(GVL_Http.cnRxBufferSize - uiRxLength);
IF (hConnection <> 0) AND (udiFreeBytes > 0) THEN
    fbTcpRead(
        xEnable     := TRUE,
        hConnection := hConnection,
        eError      => eTcpError,
        szSize      := udiFreeBytes,
        pData       := ADR(aRxBuf[uiRxLength])
        );
    IF fbTcpRead.xReady AND NOT fbTcpRead.xError THEN
        IF fbTcpRead.szCount > 0 THEN
            uiRxLength := uiRxLength + TO_UINT(fbTcpRead.szCount);
        END_IF
    ELSIF fbTcpRead.xError THEN
        eLastNbsError := eTcpError;
        IF bReuseAttempt
            AND NOT bRetryAfterStale
            AND (uiRxLength = 0)
            AND ((stRequest.eMethod = E_HttpMethod.iGet)
                OR (stRequest.eMethod = E_HttpMethod.iHead)
                OR (stRequest.eMethod = E_HttpMethod.iOptions)) THEN
            // 原因:HTTP/1.1 允许 Server 随时关闭空闲 keep-alive 连接;NBS 可能到下一次读才暴露旧句柄失效。
            // 约束:只对无响应字节的 GET/HEAD/OPTIONS 自动重连一次,避免 POST/PUT/DELETE 被重复提交。
            fbTcpRead(
                xEnable     := FALSE,
                hConnection := hConnection,
                szSize      := 0,
                pData       := ADR(aRxBuf)
                );
            fbTcpWrite(
                xExecute    := FALSE,
                hConnection := hConnection,
                szSize      := 0,
                pData       := ADR(aTxBuf)
                );
            fbTcpClient(
                xEnable     := FALSE,
                ipAddr      := ipServer,
                uiPort      := uiEffectivePort,
                hConnection => hConnection
                );
            hConnection := 0;
            bTcpConnected := FALSE;
            uiRxLength := 0;
            sRxMessage := '';
            bReuseAttempt := FALSE;
            bRetryAfterStale := TRUE;
            IF M_QueueRawString(
                sText := sRequestText
                ) THEN
                eState := E_HttpClientState.iTcpConnect;
            ELSE
                eState := E_HttpClientState.iFault;
            END_IF
            RETURN;
        ELSE
            M_SetClientError(
                eError   := E_HttpError.iTcpReadFailed,
                sMessage := 'TCP read failed'
                );
            eState := E_HttpClientState.iFault;
        END_IF
    END_IF
ELSE
    fbTcpRead(
        xEnable     := FALSE,
        hConnection := hConnection,
        szSize      := 0,
        pData       := ADR(aRxBuf)
        );
END_IF

// === METHOD M_ServiceWrite ===
/// =======================================================================
/// 名称      : M_ServiceWrite
/// 功能      : 周期调用 NBS.TCP_Write 发送请求。
/// 说明      : xDone 脉冲通过锁存收口,避免扫描周期错过发送完成。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ServiceWrite
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
fbTcpWrite(
    xExecute    := bWriteExecute,
    hConnection := hConnection,
    eError      => eTcpError,
    szSize      := TO_UDINT(uiWriteLen),
    pData       := ADR(aTxBuf)
    );

IF NOT bWriteExecute THEN
    bWriteDoneLatched := FALSE;
ELSIF fbTcpWrite.xError THEN
    eLastNbsError := eTcpError;
    bWriteExecute := FALSE;
    bWriteBusy := FALSE;
    M_SetClientError(
        eError   := E_HttpError.iTcpWriteFailed,
        sMessage := 'TCP write failed'
        );
    eState := E_HttpClientState.iFault;
ELSIF fbTcpWrite.xDone THEN
    bWriteDoneLatched := TRUE;
END_IF

tonWrite(
    IN := bWriteBusy,
    PT := GVL_Http.cnWriteTimeout
    );

IF bWriteBusy AND bWriteDoneLatched THEN
    bWriteExecute := FALSE;
    bWriteBusy := FALSE;
    bWriteDoneLatched := FALSE;
    uiWriteLen := 0;
ELSIF bWriteBusy AND tonWrite.Q THEN
    bWriteExecute := FALSE;
    bWriteBusy := FALSE;
    M_SetClientError(
        eError   := E_HttpError.iTcpWriteFailed,
        sMessage := 'TCP write timeout'
        );
    eState := E_HttpClientState.iFault;
END_IF

// === METHOD M_ProcessResponse ===
/// =======================================================================
/// 名称      : M_ProcessResponse
/// 功能      : 将接收缓冲转换为响应文本并解析。
/// 说明      : Header 未收完整或 body 未收完整时继续等待。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ProcessResponse
VAR
    pRxBuf : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
IF M_FindHttpEnd() = 0 THEN
    RETURN;
END_IF

pRxBuf := ADR(aRxBuf);
M_CopyBytesToString(
    pSource     := pRxBuf,
    uiByteCount := uiRxLength,
    sTarget     := sResponseText
    );
sRxMessage := sResponseText;
fbParser.M_ParseResponse(
    sMessage   := sResponseText,
    stResponse => stResponse
    );
IF fbParser.bDone AND NOT fbParser.bError THEN
    stMetrics.udiResponseCount := stMetrics.udiResponseCount + 1;
    eLastError := E_HttpError.iNoError;
    sDiagMsg := '';
    uiStatusCode := stResponse.uiStatusCode;
    sResponseBody := stResponse.sBody;
    sResponseHeader := stResponse.sRawHeaders;
    eState := E_HttpClientState.iDone;
ELSIF fbParser.bError AND (fbParser.eError <> E_HttpError.iNeedMoreData) THEN
    stMetrics.udiProtocolErrorCount := stMetrics.udiProtocolErrorCount + 1;
    M_SetClientError(
        eError   := fbParser.eError,
        sMessage := fbParser.sDiagMsg
        );
    eState := E_HttpClientState.iFault;
END_IF

// === METHOD M_QueueRawString ===
/// =======================================================================
/// 名称      : M_QueueRawString
/// 功能      : 将请求文本复制到发送缓冲并启动 TCP_Write。
/// 说明      : 不发送 STRING 结尾 0,只发送真实 HTTP 字节。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_QueueRawString : BOOL
VAR_INPUT
    sText : STRING(GVL_Http.cnMaxMessageSize) := '';    // 诊断或协议文本字段。
END_VAR
VAR
    uiLen : UINT;               // 计数、长度或状态数值。
    pRead : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
M_QueueRawString := FALSE;
IF bWriteBusy OR bWriteExecute THEN
    M_SetClientError(
        eError   := E_HttpError.iQueueFull,
        sMessage := 'Client transmit queue is busy'
        );
    RETURN;
END_IF

uiLen := TO_UINT(LEN(sText));
IF uiLen > GVL_Http.cnTxBufferSize THEN
    M_SetClientError(
        eError   := E_HttpError.iBufferTooSmall,
        sMessage := 'Client transmit buffer too small'
        );
    RETURN;
END_IF

pRead := ADR(sText);
IF uiLen > 0 THEN
    FOR uiIndex := 0 TO uiLen - 1 DO
        aTxBuf[uiIndex] := pRead^;
        pRead := pRead + 1;
    END_FOR
END_IF
uiWriteLen := uiLen;
bWriteExecute := TRUE;
bWriteBusy := TRUE;
sTxMessage := sText;
M_QueueRawString := TRUE;

// === METHOD M_FindHttpEnd ===
/// =======================================================================
/// 名称      : M_FindHttpEnd
/// 功能      : 查找 CRLFCRLF。
/// 说明      : 返回 Header 总字节数,0 表示未收完整。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_FindHttpEnd : UINT
VAR
    uiScan : UINT;    // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
M_FindHttpEnd := 0;
IF uiRxLength < 4 THEN
    RETURN;
END_IF
FOR uiScan := 0 TO uiRxLength - 4 DO
    IF (aRxBuf[uiScan] = 16#0D)
        AND (aRxBuf[uiScan + 1] = 16#0A)
        AND (aRxBuf[uiScan + 2] = 16#0D)
        AND (aRxBuf[uiScan + 3] = 16#0A) THEN
        M_FindHttpEnd := uiScan + 4;
        RETURN;
    END_IF
END_FOR

// === METHOD M_CopyBytesToString ===
/// =======================================================================
/// 名称      : M_CopyBytesToString
/// 功能      : 将字节缓冲复制为 0 结尾 STRING。
/// 说明      : 复制前清空目标,超出目标容量时截断并保持字符串合法。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_CopyBytesToString : BOOL
VAR_INPUT
    uiByteCount : UINT := 0;    // 计数、长度或状态数值。
END_VAR
VAR_IN_OUT
    pSource : POINTER TO BYTE;                      // 当前扫描周期使用的字节指针。
    sTarget : STRING(GVL_Http.cnMaxMessageSize);    // 诊断或协议文本字段。
END_VAR
VAR
    pRead     : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
    pWrite    : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
    uiCopyLen : UINT;               // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
pWrite := ADR(sTarget);
pWrite^ := 0;
M_CopyBytesToString := FALSE;
IF pSource = 0 THEN
    RETURN;
END_IF
IF uiByteCount > TO_UINT(SIZEOF(sTarget) - 1) THEN
    uiCopyLen := TO_UINT(SIZEOF(sTarget) - 1);
ELSE
    uiCopyLen := uiByteCount;
END_IF
pRead := pSource;
IF uiCopyLen > 0 THEN
    FOR uiIndex := 0 TO uiCopyLen - 1 DO
        pWrite^ := pRead^;
        pWrite := pWrite + 1;
        pRead := pRead + 1;
    END_FOR
END_IF
pWrite^ := 0;
M_CopyBytesToString := TRUE;

// === METHOD M_ResetTransaction ===
/// =======================================================================
/// 名称      : M_ResetTransaction
/// 功能      : 清空单次事务缓冲。
/// 说明      : 统计量和 TCP 句柄不在这里清零,避免长连接和运行统计被事务复位影响。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ResetTransaction
// === IMPLEMENTATION ===
// 工程说明:只复位一次 HTTP 事务相关缓冲,保留连接和累计诊断。
bRequestQueued := FALSE;
bReuseAttempt := FALSE;
bRetryAfterStale := FALSE;
uiRxLength := 0;
uiWriteLen := 0;
bWriteExecute := FALSE;
bWriteBusy := FALSE;
bWriteDoneLatched := FALSE;
sRequestText := '';
sResponseText := '';
sTxMessage := '';
sRxMessage := '';

// === METHOD M_Reset ===
/// =======================================================================
/// 名称      : M_Reset
/// 功能      : 完整复位 Client。
/// 说明      : 禁用路径调用,清空状态、诊断、缓冲和响应。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_Reset
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
M_ResetTransaction();
hConnection := 0;
bTcpConnected := FALSE;
xActive := FALSE;
xConnected := FALSE;
xBusy := FALSE;
xDone := FALSE;
xError := FALSE;
bBusy := FALSE;
bDone := FALSE;
bError := FALSE;
diErrorID := 0;
sDiagMsg := '';
eLastError := E_HttpError.iNoError;
eState := E_HttpClientState.iDisabled;
stResponse.sVersion := 'HTTP/1.1';
stResponse.uiStatusCode := 0;
stResponse.sReason := '';
stResponse.sContentType := GVL_Http.cnDefaultContentType;
stResponse.sAdditionalHeader := '';
stResponse.sRawHeaders := '';
stResponse.sBody := '';
stResponse.bHasContentLength := FALSE;
stResponse.bTransferChunked := FALSE;
stResponse.bConnectionClose := FALSE;
stResponse.udiContentLength := 0;
uiStatusCode := 0;
sTxMessage := '';
sRxMessage := '';
sResponseBody := '';
sResponseHeader := '';

// === METHOD M_SetClientError ===
/// =======================================================================
/// 名称      : M_SetClientError
/// 功能      : 统一锁存 Client 错误。
/// 说明      : 所有故障路径必须写入诊断文本和统计错误码。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_SetClientError : BOOL
VAR_INPUT
    eError   : E_HttpError := E_HttpError.iNoError;    // 枚举状态或错误码。
    sMessage : STRING(255) := '';                      // 诊断或协议文本字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
eLastError := eError;
stMetrics.eLastError := eError;
bError := TRUE;
diErrorID := TO_DINT(eError);
sDiagMsg := sMessage;
M_SetClientError := TRUE;

// === METHOD M_UpdateClientOutputs ===
/// =======================================================================
/// 名称      : M_UpdateClientOutputs
/// 功能      : 同步 Client 在线输出。
/// 说明      : 输出统一在主状态机末尾刷新,减少分支遗漏。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_UpdateClientOutputs
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
bTcpConnected := fbTcpClient.xActive
    AND (hConnection <> 0)
    AND (eState <> E_HttpClientState.iDisabled)
    AND (eState <> E_HttpClientState.iFault);
bBusy := (eState = E_HttpClientState.iTcpConnect)
    OR (eState = E_HttpClientState.iSend)
    OR (eState = E_HttpClientState.iReceive);
xActive := bTcpConnected;
xConnected := bTcpConnected;
xBusy := bBusy;
xDone := bDone;
xError := bError;
uiStatusCode := stResponse.uiStatusCode;
sResponseBody := stResponse.sBody;
sResponseHeader := stResponse.sRawHeaders;
diErrorID := TO_DINT(eLastError);

完整源码 3:ST_HttpClientMetrics.st

职责:Client 运行指标

iecst
/// 功能    : HTTP Client 运行统计。
/// 库依赖 : 暂无
TYPE ST_HttpClientMetrics :
STRUCT
    udiRequestCount       : UDINT := 0;
    udiResponseCount      : UDINT := 0;
    udiConnectErrorCount  : UDINT := 0;
    udiProtocolErrorCount : UDINT := 0;
    eLastError            : E_HttpError := E_HttpError.iNoError;
END_STRUCT
END_TYPE

本篇阅读抓手

  • Execute 只触发事务,FB 仍需周期调用。
  • 完成条件是响应解析结束。
  • 连接错误和协议错误分开输出。

如何验证这组源码

  1. 先针对 E_HttpClientState.st 的公开输入和错误出口建立确定性用例。
  2. 再把“Execute 只触发事务,FB 仍需周期调用”转成至少一个正常场景和一个失败场景。
  3. 本篇 3 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
  4. 真机复核时重点观察“连接错误和协议错误分开输出”,并保留对应状态、计数和原始报文。

这一篇你最该记住

  • Execute 只触发事务,FB 仍需周期调用。
  • 完成条件是响应解析结束。
  • 连接错误和协议错误分开输出。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 24/28 篇。
  • 当前源码加更:第 6/8 篇。
  • 本篇完整源码文件数:3。
  • 上一篇:第23篇
  • 下一篇:第25篇
评论和回复区

评论区预留

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

↑ ↓