第23篇_源码加更 05|HTTP Server、连接实例和诊断数据

适合谁收藏

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

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

先给结论

Server 外层负责监听和多槽位调度,单连接 FB 负责 Read、解析、路由、构造、Write 与清理。两个层级必须一起公开才能看清生命周期。 本篇的通过标准不是"代码已经贴出",而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

读图重点

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

先看文件职责

序号 文件 职责 行数
1 E_HttpServerState.st Server 外层状态 13
2 E_HttpConnectionState.st Server 单连接状态 13
3 FB_HttpServer.st 监听和多槽位调度 364
4 FB_HttpServerConnection.st 单连接请求响应闭环 851
5 ST_HttpServerMetrics.st Server 运行指标 11
6 ST_HttpConnectionSnapshot.st 连接槽位快照 13

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

从协议约束到代码职责

协议约束

Server 外层负责监听和多槽位调度,单连接 FB 负责 Read、解析、路由、构造、Write 与清理。两个层级必须一起公开才能看清生命周期。 协议层只定义消息与状态成立的条件,工程层还必须把条件分配给确定对象,避免 Parser、Builder、Server、Client 和测试入口互相越权。

对象分工

  • 第 1 个对象 E_HttpServerState.st:承担"Server 外层状态"。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 2 个对象 E_HttpConnectionState.st:承担"Server 单连接状态"。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 3 个对象 FB_HttpServer.st:承担"监听和多槽位调度"。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 4 个对象 FB_HttpServerConnection.st:承担"单连接请求响应闭环"。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 5 个对象 ST_HttpServerMetrics.st:承担"Server 运行指标"。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 6 个对象 ST_HttpConnectionSnapshot.st:承担"连接槽位快照"。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。

装配与验证

这组文件必须作为一个职责单元阅读和编译。先用确定输入验证"监听器与连接槽分层",再制造非法或容量边界验证"每个槽位独立缓冲和错误",最后在真实通信或上层调用中确认"Metrics 与 Snapshot 为真机排障保留证据"。如果单文件测试通过但装配后失败,应优先检查结构体、常量、状态枚举和周期调用关系,而不是立即重写核心算法。源码完整公开只证明读者拿到了同一事实源,目标运行时是否通过仍需重新编译和真机取证。

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

本篇完整开源代码

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

完整源码 1:E_HttpServerState.st

职责:Server 外层状态

iecst 复制代码
/// 功能    : HTTP Server 状态枚举。
/// 库依赖 : 暂无
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_HttpServerState :
(
    iDisabled := 0,
    iInit     := 1,
    iListen   := 2,
    iRunning  := 3,
    iFault    := 4
) INT;
END_TYPE

完整源码 2:E_HttpConnectionState.st

职责:Server 单连接状态

iecst 复制代码
/// 功能    : HTTP Server 单连接状态枚举。
/// 库依赖 : 暂无
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_HttpConnectionState :
(
    iEmpty    := 0,
    iReceive  := 1,
    iRespond  := 2,
    iClosing  := 3,
    iFault    := 4
) INT;
END_TYPE

完整源码 3:FB_HttpServer.st

职责:监听和多槽位调度

iecst 复制代码
/// 功能    : 基于 NBS 的 HTTP Server。
/// 库依赖 : CAA Net Base Services
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_HttpServer
VAR_INPUT
    xEnable             : BOOL := FALSE;                                  // CODESYS 风格 Server 使能。
    xReset              : BOOL := FALSE;                                  // 复位命令,TRUE 时清空状态和诊断。
    xCloseConnection    : BOOL := FALSE;                                  // 响应后主动关闭连接,TRUE 为短连接。
    bEnable             : BOOL := FALSE;                                  // 兼容旧接口的 Server 使能。
    sBindIP             : STRING := '0.0.0.0';                            // 监听网口 IP,0.0.0.0 表示任意网卡。
    uiPort              : UINT := GVL_Http.cnDefaultServerPort;           // 监听端口。
    uiResponseStatusCode: UINT := 200;                                    // 自定义响应状态码,0 时使用内置路由默认值。
    sResponseBody       : STRING(GVL_Http.cnMaxBodySize) := '';           // 自定义响应 body,空时使用内置路由默认值。
    sResponseContentType: STRING(96) := GVL_Http.cnDefaultContentType;    // 自定义响应 Content-Type。
    sAdditionalHeader   : STRING(GVL_Http.cnMaxHeaderSize) := '';         // 自定义响应 Header 行,不包含结尾空行。
    udiNowMs            : UDINT := 0;                                     // 外部毫秒时钟。
END_VAR
VAR_OUTPUT
    xListening           : BOOL;                                                                // CODESYS 风格监听状态。
    xRunning             : BOOL;                                                                // CODESYS 风格运行状态。
    xBusy                : BOOL;                                                                // CODESYS 风格忙状态。
    xError               : BOOL;                                                                // CODESYS 风格错误状态。
    bListening           : BOOL;                                                                // 已获得监听句柄。
    bRunning             : BOOL;                                                                // Server 正在运行。
    bBusy                : BOOL;                                                                // 正在初始化或等待监听。
    bError               : BOOL;                                                                // 错误锁存。
    diErrorID            : DINT;                                                                // 错误诊断码。
    sDiagMsg             : STRING(255);                                                         // 诊断文本。
    eLastNbsError        : NBS.ERROR;                                                           // 最近一次 NBS 错误。
    eState               : E_HttpServerState;                                                   // Server 状态机。
    eLastError           : E_HttpError;                                                         // 最近一次 HTTP 错误。
    stMetrics            : ST_HttpServerMetrics;                                                // 运行统计。
    uiActiveConnections  : UINT;                                                                // 活跃连接数。
    uiLastErrorSlot      : UINT;                                                                // 最近错误槽位。
    uiLastRequestSlot    : UINT;                                                                // 最近请求槽位。
    sRequestTarget       : STRING(GVL_Http.cnMaxTargetLen);                                     // 最近请求路径。
    sRequestBody         : STRING(GVL_Http.cnMaxBodySize);                                      // 最近请求 body。
    sRxMessage           : STRING(GVL_Http.cnMaxMessageSize);                                   // 最近一次接收报文文本。
    sTxMessage           : STRING(GVL_Http.cnMaxMessageSize);                                   // 最近一次发送报文文本。
    sLastTarget          : STRING(GVL_Http.cnMaxTargetLen);                                     // 最近请求路径。
    sLastBody            : STRING(GVL_Http.cnMaxBodySize);                                      // 最近请求 body。
    hListenHandle        : NBS.CAA.HANDLE;                                                      // 监听句柄快照。
    aConnectionSnapshots : ARRAY[1..GVL_Http.cnMaxClientSlots] OF ST_HttpConnectionSnapshot;    // 连接快照。
END_VAR
VAR
    fbServer      : NBS.TCP_Server;                                                    // NBS TCP_Server 实例。
    aTcpAccept    : ARRAY[1..GVL_Http.cnMaxClientSlots] OF NBS.TCP_Connection;         // 接入槽位。
    aConnections  : ARRAY[1..GVL_Http.cnMaxClientSlots] OF FB_HttpServerConnection;    // HTTP 连接槽位。
    ipBindAddr    : NBS.IP_ADDR;                                                       // 监听 IP。
    hServer       : NBS.CAA.HANDLE;                                                    // 监听句柄。
    hCleanup      : NBS.CAA.HANDLE;                                                    // 清理连接句柄。
    uiSlotIndex   : UINT;                                                              // 槽位索引。
    uiActiveCount : UINT;                                                              // 活跃连接计数。
    eTcpError     : NBS.ERROR;                                                         // NBS 错误。
    bAcceptError  : BOOL;                                                              // 接入错误汇总。
    bRunEnable    : BOOL;                                                              // 兼容旧接口和新接口后的实际使能。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : FB_HttpServer
/// 功能      : 基于 CAA Net Base Services 实现 HTTP Server 监听、多槽位调度和连接策略。
/// 库依赖    : CAA Net Base Services
/// =======================================================================
/// 使用说明  : 1. 每扫描周期调用一次,内部周期调用 TCP_Server/TCP_Connection/Read/Write。
///           : 2. xCloseConnection 控制短连接或顺序长连接,内置路由为 `/api/ping`、`/api/echo`、`/api/status`。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:监听句柄只在 Running 状态对外暴露,避免外部脚本把初始化中的端口误判为可服务。
// 风险:NBS Server 和连接槽错误必须汇总到统一 Server 诊断,否则真实 client 只能看到连接关闭。
ipBindAddr.sAddr := sBindIP;
bRunEnable := xEnable OR bEnable;

IF xReset THEN
    M_Reset();
END_IF

IF NOT bRunEnable THEN
    fbServer(
        xEnable := FALSE,
        ipAddr  := ipBindAddr,
        uiPort  := uiPort
        );
    FOR uiSlotIndex := 1 TO GVL_Http.cnMaxClientSlots DO
        aTcpAccept[uiSlotIndex](
            xEnable := FALSE,
            hServer := hServer
        );
        aConnections[uiSlotIndex](
            bEnable             := FALSE,
            xCloseConnection    := xCloseConnection,
            uiSlot              := uiSlotIndex,
            hConnectionIn       := 0,
            bConnectionActiveIn := FALSE,
            uiResponseStatusCode:= uiResponseStatusCode,
            sResponseBody       := sResponseBody,
            sResponseContentType:= sResponseContentType,
            sAdditionalHeader   := sAdditionalHeader,
            udiNowMs            := udiNowMs
            );
        aConnectionSnapshots[uiSlotIndex] := aConnections[uiSlotIndex].stConnection;
    END_FOR
    M_Reset();
    RETURN;
END_IF

CASE eState OF
    E_HttpServerState.iDisabled:
        eState := E_HttpServerState.iInit;

    E_HttpServerState.iInit:
        hServer := 0;
        hListenHandle := 0;
        bError := FALSE;
        eLastError := E_HttpError.iNoError;
        sDiagMsg := '';
        eState := E_HttpServerState.iListen;

    E_HttpServerState.iListen,
    E_HttpServerState.iRunning:
        fbServer(
            xEnable := TRUE,
            ipAddr  := ipBindAddr,
            uiPort  := uiPort,
            eError  => eTcpError
            );
        hServer := fbServer.hServer;
        hListenHandle := hServer;
        IF fbServer.xError THEN
            eLastNbsError := eTcpError;
            M_SetServerError(
                eError   := E_HttpError.iTcpServerFailed,
                sMessage := 'TCP server listen failed'
                );
            eState := E_HttpServerState.iFault;
        ELSIF hServer <> 0 THEN
            eState := E_HttpServerState.iRunning;
            M_AcceptNewConnection();
            M_ServiceConnections();
        END_IF

    E_HttpServerState.iFault:
        fbServer(
            xEnable := FALSE,
            ipAddr  := ipBindAddr,
            uiPort  := uiPort
            );
ELSE
    eState := E_HttpServerState.iFault;
END_CASE

M_UpdateServerOutputs();

// === METHOD M_AcceptNewConnection ===
/// =======================================================================
/// 名称      : M_AcceptNewConnection
/// 功能      : 周期调用所有 NBS.TCP_Connection 接入槽位。
/// 说明      : 所有槽位每周期都调用,符合 NBS 电平触发语义。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_AcceptNewConnection
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
bAcceptError := FALSE;
FOR uiSlotIndex := 1 TO GVL_Http.cnMaxClientSlots DO
    aTcpAccept[uiSlotIndex](
        xEnable := hServer <> 0,
        hServer := hServer
        );
    IF aTcpAccept[uiSlotIndex].xActive THEN
        IF (NOT aConnections[uiSlotIndex].bActive)
            OR (aConnections[uiSlotIndex].stConnection.hConnection <> aTcpAccept[uiSlotIndex].hConnection) THEN
            // 原因:接入阶段只绑定 NBS 句柄,读写统一留给 M_ServiceConnections;
            // 避免新连接在同一扫描周期被 TCP_Read 调用两次,导致大 body 半包场景被误判为接收错误。
            aConnections[uiSlotIndex].M_Attach(
                hConnection := aTcpAccept[uiSlotIndex].hConnection
                );
            stMetrics.udiAcceptedConnections := stMetrics.udiAcceptedConnections + 1;
        END_IF
    END_IF
    IF aTcpAccept[uiSlotIndex].xError THEN
        bAcceptError := TRUE;
        eTcpError := aTcpAccept[uiSlotIndex].eError;
        uiLastErrorSlot := uiSlotIndex;
    END_IF
END_FOR

IF bAcceptError THEN
    eLastNbsError := eTcpError;
    M_SetServerError(
        eError   := E_HttpError.iTcpServerFailed,
        sMessage := 'TCP accept failed'
        );
END_IF

// === METHOD M_ServiceConnections ===
/// =======================================================================
/// 名称      : M_ServiceConnections
/// 功能      : 周期服务所有 HTTP 连接槽位。
/// 说明      : 请求、响应、错误和清理均由连接 FB 输出聚合。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ServiceConnections
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:每个连接槽都必须每周期服务,NBS 句柄状态变化可能发生在任意扫描周期。
// 诊断:最近请求槽、最近错误槽和连接快照用于外部探针复盘多连接真实通信路径。
uiActiveCount := 0;
FOR uiSlotIndex := 1 TO GVL_Http.cnMaxClientSlots DO
    aConnections[uiSlotIndex](
        bEnable             := aConnections[uiSlotIndex].bActive,
        xCloseConnection    := xCloseConnection,
        uiSlot              := uiSlotIndex,
        hConnectionIn       := aConnections[uiSlotIndex].stConnection.hConnection,
        bConnectionActiveIn := aConnections[uiSlotIndex].bActive,
        uiResponseStatusCode:= uiResponseStatusCode,
        sResponseBody       := sResponseBody,
        sResponseContentType:= sResponseContentType,
        sAdditionalHeader   := sAdditionalHeader,
        udiNowMs            := udiNowMs
        );

    IF aConnections[uiSlotIndex].bActive THEN
        uiActiveCount := uiActiveCount + 1;
    END_IF

    IF aConnections[uiSlotIndex].bRequestReady THEN
        uiLastRequestSlot := uiSlotIndex;
        sLastTarget := aConnections[uiSlotIndex].stRequest.sTarget;
        sLastBody := aConnections[uiSlotIndex].stRequest.sBody;
        sRequestTarget := aConnections[uiSlotIndex].stRequest.sTarget;
        sRequestBody := aConnections[uiSlotIndex].stRequest.sBody;
        sRxMessage := aConnections[uiSlotIndex].sRxMessage;
        stMetrics.udiRequestCount := stMetrics.udiRequestCount + 1;
    END_IF

    IF aConnections[uiSlotIndex].bResponseSent THEN
        sTxMessage := aConnections[uiSlotIndex].sTxMessage;
        stMetrics.udiResponseCount := stMetrics.udiResponseCount + 1;
    END_IF

    IF aConnections[uiSlotIndex].bProtocolError THEN
        stMetrics.udiProtocolErrorCount := stMetrics.udiProtocolErrorCount + 1;
        stMetrics.eLastError := aConnections[uiSlotIndex].eProtocolError;
        uiLastErrorSlot := uiSlotIndex;
        eLastError := aConnections[uiSlotIndex].eProtocolError;
        sDiagMsg := aConnections[uiSlotIndex].sDiagMsg;
    END_IF

    IF aConnections[uiSlotIndex].bError THEN
        stMetrics.udiProtocolErrorCount := stMetrics.udiProtocolErrorCount + 1;
        stMetrics.eLastError := aConnections[uiSlotIndex].eLastError;
        uiLastErrorSlot := uiSlotIndex;
        eLastNbsError := aConnections[uiSlotIndex].eLastNbsError;
        M_SetServerError(
            eError   := aConnections[uiSlotIndex].eLastError,
            sMessage := aConnections[uiSlotIndex].sDiagMsg
            );
    END_IF

    IF aConnections[uiSlotIndex].bNeedCleanup THEN
        hCleanup := aConnections[uiSlotIndex].stConnection.hConnection;
        aConnections[uiSlotIndex](
            bEnable             := FALSE,
            xCloseConnection    := xCloseConnection,
            uiSlot              := uiSlotIndex,
            hConnectionIn       := 0,
            bConnectionActiveIn := FALSE,
            uiResponseStatusCode:= uiResponseStatusCode,
            sResponseBody       := sResponseBody,
            sResponseContentType:= sResponseContentType,
            sAdditionalHeader   := sAdditionalHeader,
            udiNowMs            := udiNowMs
            );
        aTcpAccept[uiSlotIndex](
            xEnable := FALSE,
            hServer := hServer
            );
        IF hCleanup = 0 THEN
            ;
        END_IF
    END_IF

    aConnectionSnapshots[uiSlotIndex] := aConnections[uiSlotIndex].stConnection;
END_FOR
uiActiveConnections := uiActiveCount;

// === METHOD M_Reset ===
/// =======================================================================
/// 名称      : M_Reset
/// 功能      : 完整复位 Server 输出和状态。
/// 说明      : 禁用路径调用,不清除工程文件或库配置。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_Reset
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
hServer := 0;
hListenHandle := 0;
xListening := FALSE;
xRunning := FALSE;
xBusy := FALSE;
xError := FALSE;
bListening := FALSE;
bRunning := FALSE;
bBusy := FALSE;
bError := FALSE;
diErrorID := 0;
sDiagMsg := '';
eState := E_HttpServerState.iDisabled;
eLastError := E_HttpError.iNoError;
uiActiveConnections := 0;
uiLastErrorSlot := 0;
uiLastRequestSlot := 0;
sRequestTarget := '';
sRequestBody := '';
sRxMessage := '';
sTxMessage := '';
sLastTarget := '';
sLastBody := '';

// === METHOD M_SetServerError ===
/// =======================================================================
/// 名称      : M_SetServerError
/// 功能      : 锁存 Server 错误。
/// 说明      : Server 级错误和连接槽位错误统一进入本出口。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_SetServerError : 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_SetServerError := TRUE;

// === METHOD M_UpdateServerOutputs ===
/// =======================================================================
/// 名称      : M_UpdateServerOutputs
/// 功能      : 同步 Server 在线输出。
/// 说明      : hServer 非 0 代表监听句柄有效。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_UpdateServerOutputs
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
bListening := hListenHandle <> 0;
bRunning := eState = E_HttpServerState.iRunning;
bBusy := (eState = E_HttpServerState.iInit) OR (eState = E_HttpServerState.iListen);
xListening := bListening;
xRunning := bRunning;
xBusy := bBusy;
xError := bError;
diErrorID := TO_DINT(eLastError);

完整源码 4:FB_HttpServerConnection.st

职责:单连接请求响应闭环

iecst 复制代码
/// 功能    : 基于 NBS 的 HTTP Server 单连接处理。
/// 库依赖 : CAA Net Base Services
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_HttpServerConnection
VAR_INPUT
    bEnable             : BOOL := FALSE;                                  // 槽位使能。
    xCloseConnection    : BOOL := FALSE;                                  // 响应后主动关闭连接,TRUE 时按短连接运行。
    uiSlot              : UINT := 0;                                      // 槽位编号。
    hConnectionIn       : NBS.CAA.HANDLE := 0;                            // NBS TCP 连接句柄。
    bConnectionActiveIn : BOOL := FALSE;                                  // NBS TCP_Connection 活跃状态。
    uiResponseStatusCode: UINT := 200;                                    // 用户自定义响应状态码,0 时使用内置路由默认值。
    sResponseBody       : STRING(GVL_Http.cnMaxBodySize) := '';           // 用户自定义响应 body,空时使用内置路由默认值。
    sResponseContentType: STRING(96) := GVL_Http.cnDefaultContentType;    // 用户自定义响应 Content-Type。
    sAdditionalHeader   : STRING(GVL_Http.cnMaxHeaderSize) := '';         // 用户自定义响应 Header 行,不包含结尾空行。
    udiNowMs            : UDINT := 0;                                     // 外部毫秒时钟。
END_VAR
VAR_OUTPUT
    bActive        : BOOL;                                 // 槽位正在使用。
    bBusy          : BOOL;                                 // 正在收请求或发响应。
    bRequestReady  : BOOL;                                 // 单周期请求解析完成事件。
    bResponseSent  : BOOL;                                 // 响应已发送。
    bProtocolError : BOOL;                                 // 单周期协议错误响应事件。
    bNeedCleanup   : BOOL;                                 // Server 应清理槽位。
    bError         : BOOL;                                 // 错误锁存。
    diErrorID      : DINT;                                 // 错误诊断码。
    sDiagMsg       : STRING(255);                          // 诊断文本。
    eLastNbsError  : NBS.ERROR;                            // 最近一次 NBS 错误。
    eLastError     : E_HttpError;                          // 最近一次 HTTP 错误。
    eProtocolError : E_HttpError;                          // 最近一次协议错误响应码。
    sRxMessage     : STRING(GVL_Http.cnMaxMessageSize);    // 最近一次接收报文文本。
    sTxMessage     : STRING(GVL_Http.cnMaxMessageSize);    // 最近一次发送报文文本。
    stConnection   : ST_HttpConnectionSnapshot;            // 槽位快照。
    stRequest      : ST_HttpRequest;                       // 最近一次请求。
    stResponse     : ST_HttpResponse;                      // 最近一次响应。
END_VAR
VAR
    fbTcpRead         : NBS.TCP_Read;                                     // NBS TCP_Read 实例,必须周期调用。
    fbTcpWrite        : NBS.TCP_Write;                                    // NBS TCP_Write 实例,必须周期调用。
    fbParser          : FB_HttpMessageParser;                             // HTTP 请求解析器。
    fbBuilder         : FB_HttpMessageBuilder;                            // HTTP 响应构造器。
    fbChunked         : FB_HttpChunkedDecoder;                            // HTTP chunked body 解码器。
    tonWrite          : TON;                                              // 写超时定时器。
    aRxBuf            : ARRAY[0..GVL_Http.cnRxBufferSize - 1] OF BYTE;    // 接收缓冲。
    aTxBuf            : ARRAY[0..GVL_Http.cnTxBufferSize - 1] OF BYTE;    // 发送缓冲。
    sRequestText      : STRING(GVL_Http.cnMaxMessageSize);                // 请求文本。
    sResponseText     : STRING(GVL_Http.cnMaxMessageSize);                // 响应文本。
    uiWriteLen        : UINT;                                             // 待发送字节数。
    uiWriteOffset     : UINT;                                             // 当前发送偏移。
    uiWriteChunkLen   : UINT;                                             // 当前发送分块长度。
    uiIndex           : UINT;                                             // 循环索引。
    bWriteExecute     : BOOL;                                             // TCP_Write 执行位。
    bWriteBusy        : BOOL;                                             // TCP_Write 忙状态。
    bWriteDoneLatched : BOOL;                                             // TCP_Write 完成锁存。
    bWriteGapCycle    : BOOL;                                             // TCP_Write 分块间隔扫描。
    eTcpError         : NBS.ERROR;                                        // 当前 NBS 错误。
    uiTcpReadErrors   : UINT;                                             // TCP_Read 连续错误计数。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : FB_HttpServerConnection
/// 功能      : 服务端单连接收 HTTP 请求、生成响应并按连接策略关闭或复用。
/// 库依赖    : CAA Net Base Services
/// =======================================================================
/// 使用说明  : 1. 每个 TCP_Connection 槽位必须独立持有一个本 FB 实例。
///           : 2. xCloseConnection 为 FALSE 时允许同一连接顺序处理下一条请求,不支持 pipeline。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:单连接 FB 独占接收和发送缓冲,避免多个 TCP 连接共享 STRING 缓冲导致报文串扰。
// 风险:连接失活必须转为清理请求,否则 NBS 句柄残留会让后续真机连接占满槽位。
bRequestReady := FALSE;
bResponseSent := FALSE;
bProtocolError := FALSE;
eProtocolError := E_HttpError.iNoError;

IF NOT bEnable THEN
    IF stConnection.hConnection <> 0 THEN
        fbTcpRead(
            xEnable     := FALSE,
            hConnection := stConnection.hConnection,
            szSize      := 0,
            pData       := ADR(aRxBuf)
            );
        fbTcpWrite(
            xExecute    := FALSE,
            hConnection := stConnection.hConnection,
            szSize      := 0,
            pData       := ADR(aTxBuf)
            );
    END_IF
    tonWrite(
        IN := FALSE,
        PT := GVL_Http.cnWriteTimeout
        );
    M_Reset();
    RETURN;
END_IF

IF (NOT stConnection.bUsed) OR (stConnection.hConnection <> hConnectionIn) THEN
    M_Attach(
        hConnection := hConnectionIn
        );
END_IF

IF NOT bConnectionActiveIn THEN
    bNeedCleanup := TRUE;
END_IF

CASE stConnection.eState OF
    E_HttpConnectionState.iReceive:
        M_ServiceRead();
        M_ProcessRequest();

    E_HttpConnectionState.iRespond:
        M_ServiceWrite();

    E_HttpConnectionState.iClosing:
        bNeedCleanup := (NOT bWriteBusy) AND (NOT bWriteExecute);

    E_HttpConnectionState.iFault:
        bNeedCleanup := TRUE;
ELSE
    bNeedCleanup := TRUE;
END_CASE

M_UpdateConnectionOutputs();

// === METHOD M_Attach ===
/// =======================================================================
/// 名称      : M_Attach
/// 功能      : 绑定新 TCP 连接句柄。
/// 说明      : 槽位复用前必须清空缓冲和诊断,避免上一连接数据串扰。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_Attach
VAR_INPUT
    hConnection : NBS.CAA.HANDLE := 0;    // 工程变量或诊断字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
M_Reset();
stConnection.bUsed := TRUE;
stConnection.hConnection := hConnection;
stConnection.eState := E_HttpConnectionState.iReceive;
bActive := TRUE;
bNeedCleanup := FALSE;

// === METHOD M_ServiceRead ===
/// =======================================================================
/// 名称      : M_ServiceRead
/// 功能      : 从 TCP 连接读取请求字节。
/// 说明      : 只追加到剩余缓冲空间,解析器负责判断完整报文。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ServiceRead
VAR
    udiFreeBytes : UDINT;    // 计数、长度或状态数值。
    udiReadBytes : UDINT;    // 本次 TCP_Read 允许读取的字节数。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:长连接回到接收态后必须给 TCP_Write 一个禁用扫描,释放上一响应的写状态;
// 否则第二个顺序请求到达时,NBS 读写 FB 可能仍围绕同一句柄保留冲突状态。
fbTcpWrite(
    xExecute    := FALSE,
    hConnection := stConnection.hConnection,
    szSize      := 0,
    pData       := ADR(aTxBuf)
    );

udiFreeBytes := TO_UDINT(GVL_Http.cnRxBufferSize - stConnection.uiRxLength);
IF (stConnection.hConnection <> 0) AND (udiFreeBytes > 0) THEN
    IF udiFreeBytes > TO_UDINT(GVL_Http.cnTcpReadChunkSize) THEN
        udiReadBytes := TO_UDINT(GVL_Http.cnTcpReadChunkSize);
    ELSE
        udiReadBytes := udiFreeBytes;
    END_IF
    fbTcpRead(
        xEnable     := TRUE,
        hConnection := stConnection.hConnection,
        eError      => eTcpError,
        szSize      := udiReadBytes,
        pData       := ADR(aRxBuf[stConnection.uiRxLength])
        );
    IF fbTcpRead.xReady AND NOT fbTcpRead.xError THEN
        IF fbTcpRead.szCount > 0 THEN
            uiTcpReadErrors := 0;
            stConnection.uiRxLength := stConnection.uiRxLength + TO_UINT(fbTcpRead.szCount);
        END_IF
    ELSIF fbTcpRead.xError THEN
        // 原因:NBS 在大 body 半包接收中可能短暂返回 TCP_RECEIVE_ERROR;
        // 先做连续错误滤波,避免合法请求在后续字节到达前被误关闭。
        IF uiTcpReadErrors < GVL_Http.cnTcpReadMaxErrorCount THEN
            uiTcpReadErrors := uiTcpReadErrors + 1;
        END_IF
        IF uiTcpReadErrors >= GVL_Http.cnTcpReadMaxErrorCount THEN
            eLastNbsError := eTcpError;
            M_SetConnectionError(
                eError   := E_HttpError.iTcpReadFailed,
                sMessage := 'TCP read failed'
                );
            stConnection.eState := E_HttpConnectionState.iFault;
        END_IF
    END_IF
ELSE
    fbTcpRead(
        xEnable     := FALSE,
        hConnection := stConnection.hConnection,
        szSize      := 0,
        pData       := ADR(aRxBuf)
        );
END_IF

// === METHOD M_ProcessRequest ===
/// =======================================================================
/// 名称      : M_ProcessRequest
/// 功能      : 解析请求并生成固定路由响应。
/// 说明      : `/api/ping` 返回 pong,`/api/echo` 返回请求 body,其余返回 404。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ProcessRequest
VAR
    pRxBuf            : POINTER TO BYTE;                      // 当前扫描周期使用的字节指针。
    pBodyBuf          : POINTER TO BYTE;                      // 当前请求 body 首字节指针。
    uiHeaderBytes     : UINT;                                 // HTTP Header 总字节数。
    uiBodyBytes       : UINT;                                 // 已接收 body 字节数。
    uiExpectedBodyLen : UINT;                                 // Content-Length 声明的 body 字节数。
    sChunkedBody      : STRING(GVL_Http.cnMaxMessageSize);    // chunked body 临时文本。
    bResponseQueued   : BOOL;                                 // 是否已经直接写入发送队列。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:PLC STRING 标准函数在部分运行时存在约 255 字节处理边界,Server 入站 body 必须按 BYTE 缓冲判断完整性。
// 约束:Header 仍走 Parser 做协议校验,body 只在长度满足后复制或直接回显,避免半包被误判为完整请求。
uiHeaderBytes := M_FindHttpEnd();
IF uiHeaderBytes = 0 THEN
    RETURN;
END_IF

pRxBuf := ADR(aRxBuf);
M_CopyBytesToString(
    pSource     := pRxBuf,
    uiByteCount := uiHeaderBytes,
    sTarget     := sRequestText
    );
sRxMessage := sRequestText;
fbParser.M_ParseRequestHead(
    sMessage  := sRequestText,
    stRequest => stRequest
    );

IF fbParser.bError AND (fbParser.eError = E_HttpError.iNeedMoreData) THEN
    RETURN;
ELSIF fbParser.bError THEN
    M_PrepareErrorResponse(
        uiStatusCode := 400,
        sBody        := 'bad request'
        );
ELSIF fbParser.bDone THEN
    IF stConnection.uiRxLength >= uiHeaderBytes THEN
        uiBodyBytes := stConnection.uiRxLength - uiHeaderBytes;
    ELSE
        uiBodyBytes := 0;
    END_IF

    IF stRequest.bTransferChunked THEN
        IF uiBodyBytes > GVL_Http.cnMaxBodySize THEN
            M_PrepareErrorResponse(
                uiStatusCode := 413,
                sBody        := 'payload too large'
                );
        ELSE
            IF uiBodyBytes > 0 THEN
                pBodyBuf := ADR(aRxBuf[uiHeaderBytes]);
                M_CopyBytesToString(
                    pSource     := pBodyBuf,
                    uiByteCount := uiBodyBytes,
                    sTarget     := sChunkedBody
                    );
            ELSE
                sChunkedBody := '';
            END_IF
            fbChunked.M_Decode(
                sChunked := sChunkedBody,
                sBody    => stRequest.sBody
                );
            IF fbChunked.bError AND (fbChunked.eError = E_HttpError.iNeedMoreData) THEN
                RETURN;
            ELSIF fbChunked.bError THEN
                M_PrepareErrorResponse(
                    uiStatusCode := 400,
                    sBody        := 'bad chunked body'
                    );
            ELSE
                stRequest.udiContentLength := fbChunked.udiDecodedLength;
            END_IF
        END_IF
    ELSIF stRequest.bHasContentLength THEN
        uiExpectedBodyLen := TO_UINT(stRequest.udiContentLength);
        IF uiBodyBytes < uiExpectedBodyLen THEN
            RETURN;
        END_IF
        stRequest.sBody := '';
    ELSE
        stRequest.sBody := '';
        stRequest.udiContentLength := 0;
    END_IF

    bRequestReady := TRUE;
    stConnection.sLastTarget := stRequest.sTarget;
    stResponse.bConnectionClose := xCloseConnection OR stRequest.bConnectionClose;
    stResponse.sAdditionalHeader := sAdditionalHeader;
    IF stRequest.sTarget = '/api/ping' THEN
        M_PrepareOkResponse(
            sBody := 'pong'
            );
    ELSIF stRequest.sTarget = '/api/echo' THEN
        stResponse.sContentType := sResponseContentType;
        stResponse.bConnectionClose := xCloseConnection OR stRequest.bConnectionClose;
        stResponse.sAdditionalHeader := sAdditionalHeader;
        IF stRequest.bHasContentLength THEN
            pBodyBuf := ADR(aRxBuf[uiHeaderBytes]);
            bResponseQueued := M_QueueBytesResponse(
                pBody     := pBodyBuf,
                uiBodyLen := uiExpectedBodyLen
                );
        ELSE
            pBodyBuf := ADR(stRequest.sBody);
            bResponseQueued := M_QueueBytesResponse(
                pBody     := pBodyBuf,
                uiBodyLen := TO_UINT(stRequest.udiContentLength)
                );
        END_IF
    ELSIF stRequest.sTarget = '/api/status' THEN
        M_PrepareOkResponse(
            sBody := 'ok'
            );
    ELSIF LEN(sResponseBody) > 0 THEN
        M_PrepareOkResponse(
            sBody := sResponseBody
            );
    ELSE
        M_PrepareErrorResponse(
            uiStatusCode := 404,
            sBody        := 'not found'
            );
    END_IF
ELSE
    RETURN;
END_IF

IF bResponseQueued THEN
    stConnection.eState := E_HttpConnectionState.iRespond;
    RETURN;
ELSIF bError THEN
    stConnection.eState := E_HttpConnectionState.iFault;
    RETURN;
END_IF

IF fbBuilder.M_BuildResponse(
    stResponse := stResponse,
    sMessage   => sResponseText
    ) THEN
    IF M_QueueRawString(
        sText := sResponseText
        ) THEN
        stConnection.eState := E_HttpConnectionState.iRespond;
    END_IF
ELSE
    M_SetConnectionError(
        eError   := fbBuilder.eError,
        sMessage := fbBuilder.sDiagMsg
        );
    stConnection.eState := E_HttpConnectionState.iFault;
END_IF

// === METHOD M_PrepareOkResponse ===
/// =======================================================================
/// 名称      : M_PrepareOkResponse
/// 功能      : 准备 200 响应结构。
/// 说明      : body 文本由路由层给出,统一使用 text/plain 和短连接。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_PrepareOkResponse
VAR_INPUT
    sBody : STRING(GVL_Http.cnMaxBodySize) := '';    // 诊断或协议文本字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
stResponse.sVersion := 'HTTP/1.1';
stResponse.uiStatusCode := 200;
stResponse.sReason := 'OK';
IF LEN(sResponseContentType) > 0 THEN
    stResponse.sContentType := sResponseContentType;
ELSE
    stResponse.sContentType := GVL_Http.cnDefaultContentType;
END_IF
stResponse.sBody := sBody;
stResponse.sAdditionalHeader := sAdditionalHeader;
stResponse.bConnectionClose := xCloseConnection OR stRequest.bConnectionClose;
stResponse.bHasContentLength := TRUE;
stResponse.udiContentLength := TO_UDINT(LEN(sBody));

// === METHOD M_PrepareErrorResponse ===
/// =======================================================================
/// 名称      : M_PrepareErrorResponse
/// 功能      : 准备错误响应结构。
/// 说明      : 协议错误同样返回短连接,响应后由 Server 清理槽位。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_PrepareErrorResponse
VAR_INPUT
    uiStatusCode : UINT := 400;                             // 计数、长度或状态数值。
    sBody        : STRING(GVL_Http.cnMaxBodySize) := '';    // 诊断或协议文本字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
stResponse.sVersion := 'HTTP/1.1';
stResponse.uiStatusCode := uiStatusCode;
stResponse.sReason := F_HttpStatusReason(
    uiStatusCode := uiStatusCode
    );
stResponse.sContentType := GVL_Http.cnDefaultContentType;
stResponse.sBody := sBody;
stResponse.sAdditionalHeader := sAdditionalHeader;
stResponse.bConnectionClose := TRUE;
stResponse.bHasContentLength := TRUE;
stResponse.udiContentLength := TO_UDINT(LEN(sBody));
IF uiStatusCode >= 400 THEN
    bProtocolError := TRUE;
    eProtocolError := fbParser.eError;
    eLastError := fbParser.eError;
    stConnection.eLastError := fbParser.eError;
    sDiagMsg := 'HTTP request parse failed';
END_IF

// === METHOD M_ServiceWrite ===
/// =======================================================================
/// 名称      : M_ServiceWrite
/// 功能      : 发送响应。
/// 说明      : 发送完成后按连接策略进入 Closing 或回到 Receive。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ServiceWrite
VAR
    uiRemaining : UINT;               // 当前发送剩余字节数。
    pTxData     : POINTER TO BYTE;    // 当前 TCP_Write 分块首地址。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:TCP_Write 的 xDone 是短脉冲,必须锁存后再切换 Closing,避免扫描周期错过发送完成。
// 诊断:写超时和写失败都映射到同一连接错误出口,外部 Server 矩阵可以读取槽位错误码定位。
// 原因:长连接复用时读写 FB 不能同时保持激活;响应发送阶段先给 TCP_Read 一个禁用扫描,
// 避免上一轮接收状态残留到下一轮请求,导致第二个顺序请求被 NBS 误报为 TCP read failed。
fbTcpRead(
    xEnable     := FALSE,
    hConnection := stConnection.hConnection,
    szSize      := 0,
    pData       := ADR(aRxBuf)
    );

IF bWriteBusy AND (NOT bWriteExecute) AND (NOT bWriteGapCycle) THEN
    IF uiWriteOffset < uiWriteLen THEN
        uiRemaining := uiWriteLen - uiWriteOffset;
        IF uiRemaining > GVL_Http.cnTcpWriteChunkSize THEN
            uiWriteChunkLen := GVL_Http.cnTcpWriteChunkSize;
        ELSE
            uiWriteChunkLen := uiRemaining;
        END_IF
        bWriteExecute := uiWriteChunkLen > 0;
    END_IF
END_IF

pTxData := ADR(aTxBuf);
IF bWriteExecute THEN
    pTxData := ADR(aTxBuf[uiWriteOffset]);
END_IF

fbTcpWrite(
    xExecute    := bWriteExecute,
    hConnection := stConnection.hConnection,
    eError      => eTcpError,
    szSize      := TO_UDINT(uiWriteChunkLen),
    pData       := pTxData
    );

IF bWriteGapCycle AND (NOT bWriteExecute) THEN
    bWriteGapCycle := FALSE;
ELSIF NOT bWriteExecute THEN
    bWriteDoneLatched := FALSE;
ELSIF fbTcpWrite.xError THEN
    eLastNbsError := eTcpError;
    bWriteExecute := FALSE;
    bWriteBusy := FALSE;
    bWriteGapCycle := FALSE;
    M_SetConnectionError(
        eError   := E_HttpError.iTcpWriteFailed,
        sMessage := 'TCP write failed'
        );
    stConnection.eState := E_HttpConnectionState.iFault;
ELSIF fbTcpWrite.xDone THEN
    bWriteExecute := FALSE;
    bWriteDoneLatched := FALSE;
    uiWriteOffset := uiWriteOffset + uiWriteChunkLen;
    uiWriteChunkLen := 0;
    IF uiWriteOffset >= uiWriteLen THEN
        bWriteDoneLatched := TRUE;
    ELSE
        bWriteGapCycle := TRUE;
    END_IF
END_IF

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

IF bWriteBusy AND bWriteDoneLatched THEN
    bWriteExecute := FALSE;
    bWriteBusy := FALSE;
    bWriteDoneLatched := FALSE;
    bWriteGapCycle := FALSE;
    uiWriteOffset := 0;
    uiWriteChunkLen := 0;
    stConnection.uiTxLength := 0;
    bResponseSent := TRUE;
    IF stResponse.bConnectionClose THEN
        stConnection.eState := E_HttpConnectionState.iClosing;
    ELSE
        stConnection.uiRxLength := 0;
        stConnection.uiTxLength := 0;
        uiWriteLen := 0;
        uiWriteOffset := 0;
        uiWriteChunkLen := 0;
        uiTcpReadErrors := 0;
        bWriteExecute := FALSE;
        bWriteBusy := FALSE;
        bWriteDoneLatched := FALSE;
        bWriteGapCycle := FALSE;
        sRequestText := '';
        sResponseText := '';
        stConnection.eState := E_HttpConnectionState.iReceive;
    END_IF
ELSIF bWriteBusy AND tonWrite.Q THEN
    bWriteExecute := FALSE;
    bWriteBusy := FALSE;
    bWriteGapCycle := FALSE;
    M_SetConnectionError(
        eError   := E_HttpError.iTcpWriteFailed,
        sMessage := 'TCP write timeout'
        );
    stConnection.eState := E_HttpConnectionState.iFault;
END_IF

// === METHOD M_QueueRawString ===
/// =======================================================================
/// 名称      : M_QueueRawString
/// 功能      : 将响应文本放入发送缓冲。
/// 说明      : 单连接单响应,不允许覆盖正在发送的响应。
/// =======================================================================
{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_SetConnectionError(
        eError   := E_HttpError.iQueueFull,
        sMessage := 'Server transmit queue is busy'
        );
    RETURN;
END_IF
uiLen := TO_UINT(LEN(sText));
IF uiLen > GVL_Http.cnTxBufferSize THEN
    M_SetConnectionError(
        eError   := E_HttpError.iBufferTooSmall,
        sMessage := 'Server 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;
uiWriteOffset := 0;
uiWriteChunkLen := 0;
stConnection.uiTxLength := uiLen;
sTxMessage := sText;
bWriteExecute := FALSE;
bWriteBusy := TRUE;
bWriteGapCycle := FALSE;
M_QueueRawString := TRUE;

// === METHOD M_FindHttpEnd ===
/// =======================================================================
/// 名称      : M_FindHttpEnd
/// 功能      : 查找 HTTP Header 结束序列。
/// 说明      : 返回 Header 总字节数,0 表示未收完整。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_FindHttpEnd : UINT
VAR
    uiScan : UINT;    // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
M_FindHttpEnd := 0;
IF stConnection.uiRxLength < 4 THEN
    RETURN;
END_IF
FOR uiScan := 0 TO stConnection.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
/// 功能      : 将接收字节复制为 STRING。
/// 说明      : 用于 HTTP Parser,复制过程保留 ASCII 字节原样。
/// =======================================================================
{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_Reset ===
/// =======================================================================
/// 名称      : M_Reset
/// 功能      : 清空连接槽位。
/// 说明      : 由 Server 清理槽位或禁用路径调用。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_Reset
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
stConnection.bUsed := FALSE;
stConnection.hConnection := 0;
stConnection.eState := E_HttpConnectionState.iEmpty;
stConnection.eLastError := E_HttpError.iNoError;
stConnection.sLastTarget := '';
stConnection.uiRxLength := 0;
stConnection.uiTxLength := 0;
bActive := FALSE;
bBusy := FALSE;
bRequestReady := FALSE;
bResponseSent := FALSE;
bProtocolError := FALSE;
bNeedCleanup := FALSE;
bError := FALSE;
diErrorID := 0;
sDiagMsg := '';
eLastError := E_HttpError.iNoError;
eProtocolError := E_HttpError.iNoError;
sRxMessage := '';
sTxMessage := '';
uiWriteLen := 0;
uiWriteOffset := 0;
uiWriteChunkLen := 0;
uiTcpReadErrors := 0;
bWriteExecute := FALSE;
bWriteBusy := FALSE;
bWriteDoneLatched := FALSE;
bWriteGapCycle := FALSE;
sRequestText := '';
sResponseText := '';

// === METHOD M_SetConnectionError ===
/// =======================================================================
/// 名称      : M_SetConnectionError
/// 功能      : 锁存连接槽位错误。
/// 说明      : 错误同时写入快照和标准输出,便于 Server 聚合。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_SetConnectionError : BOOL
VAR_INPUT
    eError   : E_HttpError := E_HttpError.iNoError;    // 枚举状态或错误码。
    sMessage : STRING(255) := '';                      // 诊断或协议文本字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
stConnection.eLastError := eError;
eLastError := eError;
bError := TRUE;
diErrorID := TO_DINT(eError);
sDiagMsg := sMessage;
M_SetConnectionError := TRUE;

// === METHOD M_UpdateConnectionOutputs ===
/// =======================================================================
/// 名称      : M_UpdateConnectionOutputs
/// 功能      : 同步连接在线输出。
/// 说明      : 保证每扫描周期外部可见状态和快照一致。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_UpdateConnectionOutputs
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
bActive := stConnection.bUsed;
bBusy := (stConnection.eState = E_HttpConnectionState.iReceive)
    OR (stConnection.eState = E_HttpConnectionState.iRespond);
diErrorID := TO_DINT(eLastError);

// === METHOD M_QueueBytesResponse ===
/// =======================================================================
/// 名称      : M_QueueBytesResponse
/// 功能      : 将响应 Header 和原始 body 字节放入发送缓冲。
/// 说明      : 用于大 body 回显,绕开 STRING CONCAT/LEN 在长文本上的运行时边界。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_QueueBytesResponse : BOOL
VAR_INPUT
    uiBodyLen : UINT := 0;    // 响应 body 字节数。
END_VAR
VAR_IN_OUT
    pBody : POINTER TO BYTE;    // 响应 body 首字节指针。
END_VAR
VAR
    sHeader      : STRING(255);        // 响应 Header 文本,长度保持在短 STRING 范围内。
    sContentLen  : STRING(16);         // Content-Length 文本。
    uiHeaderLen  : UINT;               // 响应 Header 字节数。
    uiBodyIndex  : UINT;               // body 复制索引。
    pRead        : POINTER TO BYTE;    // 当前复制源指针。
END_VAR
// === IMPLEMENTATION ===
// 原因:HTTP echo 需要按真实字节回显 1023 字节 body,不能依赖长 STRING 拼接后再发送。
// 诊断:Header 长度和 body 长度分别计算,发送缓冲不足时保留 BufferTooSmall 错误出口。
M_QueueBytesResponse := FALSE;
IF bWriteBusy OR bWriteExecute THEN
    M_SetConnectionError(
        eError   := E_HttpError.iQueueFull,
        sMessage := 'Server transmit queue is busy'
        );
    RETURN;
END_IF

sContentLen := UINT_TO_STRING(uiBodyLen);
sHeader := 'HTTP/1.1 200 OK$R$NConnection: ';
IF stResponse.bConnectionClose THEN
    sHeader := CONCAT(sHeader, 'close$R$NContent-Type: ');
ELSE
    sHeader := CONCAT(sHeader, 'keep-alive$R$NContent-Type: ');
END_IF
sHeader := CONCAT(sHeader, GVL_Http.cnDefaultContentType);
sHeader := CONCAT(sHeader, '$R$NContent-Length: ');
sHeader := CONCAT(sHeader, sContentLen);
IF LEN(stResponse.sAdditionalHeader) > 0 THEN
    sHeader := CONCAT(sHeader, '$R$N');
    sHeader := CONCAT(sHeader, stResponse.sAdditionalHeader);
END_IF
sHeader := CONCAT(sHeader, '$R$N$R$N');
uiHeaderLen := TO_UINT(LEN(sHeader));
sTxMessage := sHeader;

IF (TO_UDINT(uiHeaderLen) + TO_UDINT(uiBodyLen)) > TO_UDINT(GVL_Http.cnTxBufferSize) THEN
    M_SetConnectionError(
        eError   := E_HttpError.iBufferTooSmall,
        sMessage := 'Server transmit buffer too small'
        );
    RETURN;
END_IF

pRead := ADR(sHeader);
IF uiHeaderLen > 0 THEN
    FOR uiIndex := 0 TO uiHeaderLen - 1 DO
        aTxBuf[uiIndex] := pRead^;
        pRead := pRead + 1;
    END_FOR
END_IF

IF uiBodyLen > 0 THEN
    IF pBody = 0 THEN
        M_SetConnectionError(
            eError   := E_HttpError.iInvalidArgument,
            sMessage := 'Server response body pointer is null'
            );
        RETURN;
    END_IF
    pRead := pBody;
    FOR uiBodyIndex := 0 TO uiBodyLen - 1 DO
        aTxBuf[uiHeaderLen + uiBodyIndex] := pRead^;
        pRead := pRead + 1;
    END_FOR
END_IF

uiWriteLen := uiHeaderLen + uiBodyLen;
uiWriteOffset := 0;
uiWriteChunkLen := 0;
stConnection.uiTxLength := uiWriteLen;
bWriteExecute := FALSE;
bWriteBusy := TRUE;
bWriteGapCycle := FALSE;
M_QueueBytesResponse := TRUE;

完整源码 5:ST_HttpServerMetrics.st

职责:Server 运行指标

iecst 复制代码
/// 功能    : HTTP Server 运行统计。
/// 库依赖 : 暂无
TYPE ST_HttpServerMetrics :
STRUCT
    udiAcceptedConnections : UDINT := 0;
    udiRequestCount        : UDINT := 0;
    udiResponseCount       : UDINT := 0;
    udiProtocolErrorCount  : UDINT := 0;
    eLastError             : E_HttpError := E_HttpError.iNoError;
END_STRUCT
END_TYPE

完整源码 6:ST_HttpConnectionSnapshot.st

职责:连接槽位快照

iecst 复制代码
/// 功能    : HTTP Server 单连接诊断快照。
/// 库依赖 : 暂无
TYPE ST_HttpConnectionSnapshot :
STRUCT
    bUsed        : BOOL := FALSE;
    hConnection  : NBS.CAA.HANDLE := 0;
    eState       : E_HttpConnectionState := E_HttpConnectionState.iEmpty;
    eLastError   : E_HttpError := E_HttpError.iNoError;
    sLastTarget  : STRING(GVL_Http.cnMaxTargetLen) := '';
    uiRxLength   : UINT := 0;
    uiTxLength   : UINT := 0;
END_STRUCT
END_TYPE

本篇阅读抓手

  • 监听器与连接槽分层。
  • 每个槽位独立缓冲和错误。
  • Metrics 与 Snapshot 为真机排障保留证据。

如何验证这组源码

  1. 先针对 E_HttpServerState.st 的公开输入和错误出口建立确定性用例。
  2. 再把"监听器与连接槽分层"转成至少一个正常场景和一个失败场景。
  3. 本篇 6 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
  4. 真机复核时重点观察"Metrics 与 Snapshot 为真机排障保留证据",并保留对应状态、计数和原始报文。

这一篇你最该记住

  • 监听器与连接槽分层。
  • 每个槽位独立缓冲和错误。
  • Metrics 与 Snapshot 为真机排障保留证据。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 23/28 篇。
  • 当前源码加更:第 5/8 篇。
  • 本篇完整源码文件数:6。
  • 上一篇:第22篇
  • 下一篇:第24篇
相关推荐
企业数字化笔记7 小时前
AI写的网站一直自动跳转怎么办?301、302和HTTPS重定向排查
网络协议·http·https
那年窗外下的雪.8 小时前
AIDC 学习日志|第 24 天|设备输出反推与 MAC Flapping 定位
网络协议·学习·tcp/ip·http·macos·tcpdump
sugar__salt9 小时前
大模型流式输出完全指南(上):从 HTTP 长连接到手写 SSE
网络·网络协议·http
2601_962885721 天前
不用 SDK 也能取 A 股数据:AlphaFeed REST API用 cURL/HTTP 直连教程
网络·网络协议·http
为思念酝酿的痛1 天前
应用层协议HTTPS
网络·网络协议·http·https
隐擎fox1 天前
深入下一代 Web 协议风控:HTTP/2 帧结构解析与 Akamai/Cloudflare H2 指纹检测实战
python·网络协议·http·ip/tcp
Chengbei111 天前
红队新工具 ToShell 解析:自带 Web 控制台,AES+SM4 国密加密,载荷随机免杀,自主 Agent 自动完成渗透任务
人工智能·tcp/ip·安全·web安全·http·网络安全·系统安全
川石课堂软件测试1 天前
涨薪技术|Prometheus之HTTP API中使用PromQL
网络协议·测试工具·jmeter·http·单元测试·postman·prometheus
流烟默2 天前
HTTP 连接管理技术梳理:JDK KeepAliveCache 机制与池化选型
java·网络协议·http