ControlRookie
返回文章

第20篇_源码加更 02|Parser、Header 与长度解析

这一组代码负责把原始 HTTP 文本变成结构化请求或响应,并拒绝 Host、Header、Content-Length 与 Transfer-Encoding 的歧义。

这一组代码负责把原始 HTTP 文本变成结构化请求或响应,并拒绝 Host、Header、Content-Length 与 Transfer-Encoding 的歧义。

适合谁收藏

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

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

先给结论

这一组代码负责把原始 HTTP 文本变成结构化请求或响应,并拒绝 Host、Header、Content-Length 与 Transfer-Encoding 的歧义。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

源码加更 02|Parser、Header 与长度解析
源码加更 02|Parser、Header 与长度解析

读图重点

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

先看文件职责

序号文件职责行数
1FB_HttpMessageParser.st请求与响应解析器957
2F_HttpFindHeader.stHeader 查找与语法检查134
3F_HttpAsciiEqualsIgnoreCase.stASCII 不区分大小写比较57
4F_HttpParseContentLength.stContent-Length 十进制解析49

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

从协议约束到代码职责

协议约束

这一组代码负责把原始 HTTP 文本变成结构化请求或响应,并拒绝 Host、Header、Content-Length 与 Transfer-Encoding 的歧义。 协议层只定义消息与状态成立的条件,工程层还必须把条件分配给确定对象,避免 Parser、Builder、Server、Client 和测试入口互相越权。

对象分工

  • 第 1 个对象 FB_HttpMessageParser.st:承担“请求与响应解析器”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 2 个对象 F_HttpFindHeader.st:承担“Header 查找与语法检查”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 3 个对象 F_HttpAsciiEqualsIgnoreCase.st:承担“ASCII 不区分大小写比较”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
  • 第 4 个对象 F_HttpParseContentLength.st:承担“Content-Length 十进制解析”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。

装配与验证

这组文件必须作为一个职责单元阅读和编译。先用确定输入验证“字段名按 ASCII 不区分大小写”,再制造非法或容量边界验证“缺失与非法必须分开”,最后在真实通信或上层调用中确认“NeedMoreData 不能伪装成完成”。如果单文件测试通过但装配后失败,应优先检查结构体、常量、状态枚举和周期调用关系,而不是立即重写核心算法。源码完整公开只证明读者拿到了同一事实源,目标运行时是否通过仍需重新编译和真机取证。

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

本篇完整开源代码

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

完整源码 1:FB_HttpMessageParser.st

职责:请求与响应解析器

iecst
/// 功能    : HTTP/1.1 请求与响应解析器。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_HttpMessageParser
VAR_OUTPUT
    bDone         : BOOL;           // 解析完成标志。
    bError        : BOOL;           // 错误锁存标志。
    diErrorID     : DINT;           // 诊断错误码,0表示无错误。
    eError        : E_HttpError;    // 最近一次解析错误码。
    sDiagMsg      : STRING(255);    // 最近一次解析诊断文本。
    udiBodyLength : UDINT;          // 已解析 body 字节数。
END_VAR
VAR
    fbChunked : FB_HttpChunkedDecoder;    // chunked body 解码器实例。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : FB_HttpMessageParser
/// 功能      : 解析完整 HTTP/1.1 请求或响应文本。
/// 库依赖    : 暂无
/// =======================================================================
/// 使用说明  : 1. 输入应为 TCP 层累计到 `CRLF CRLF` 后的一条完整报文。
///           : 2. 请求解析拒绝 TE+CL,HTTP/1.1 默认 keep-alive,入站支持 chunked 解码。
/// =======================================================================

// === METHOD M_ParseRequest ===
/// =======================================================================
/// 名称      : M_ParseRequest
/// 功能      : 解析 HTTP 请求报文。
/// 说明      : HTTP/1.1 请求必须包含 Host,TE+CL 直接拒绝。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_ParseRequest : BOOL
VAR_INPUT
    sMessage : STRING(GVL_Http.cnMaxMessageSize) := '';    // 诊断或协议文本字段。
END_VAR
VAR_OUTPUT
    stRequest : ST_HttpRequest;    // 结构化协议或测试数据。
END_VAR
VAR
    iFirstLineEnd    : INT;                                     // 计数、长度或状态数值。
    iHeaderEnd       : INT;                                     // 计数、长度或状态数值。
    iSpace1          : INT;                                     // 计数、长度或状态数值。
    iSpace2Relative  : INT;                                     // 计数、长度或状态数值。
    iHeaderStart     : INT;                                     // 计数、长度或状态数值。
    iHeaderLen       : INT;                                     // 计数、长度或状态数值。
    iBodyStart       : INT;                                     // 计数、长度或状态数值。
    iBodyLen         : INT;                                     // 计数、长度或状态数值。
    sStartLine       : STRING(255);                             // 诊断或协议文本字段。
    sAfterMethod     : STRING(255);                             // 诊断或协议文本字段。
    sHeaderText      : STRING(GVL_Http.cnMaxHeaderSize);        // 诊断或协议文本字段。
    sBodyText        : STRING(GVL_Http.cnMaxBodySize);          // 诊断或协议文本字段。
    sHeaderValue     : STRING(GVL_Http.cnMaxHeaderValueLen);    // 诊断或协议文本字段。
    udiContentLength : UDINT;                                   // 计数、长度或状态数值。
    uiHeaderCount    : UINT;                                    // 计数、长度或状态数值。
    bHeaderFound     : BOOL;                                    // 布尔状态或命令标志。
    bHeaderInvalid   : BOOL;                                    // 布尔状态或命令标志。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 约束:HTTP/1.1 请求必须校验 Host,缺失或重复 Host 直接按协议错误处理,避免虚拟主机场景路由歧义。
// 约束:Transfer-Encoding 与 Content-Length 同时存在必须拒绝,避免请求走私类歧义进入 PLC Server。
// 风险:Content-Length、chunked 和 close 语义互斥处理,任何长度错误都必须停在协议层。
// 诊断:解析失败统一输出 eError 和 sDiagMsg,外部 Server 探针可把 4xx 响应映射到具体协议原因。
M_Reset();
M_ParseRequest := FALSE;
M_ResetRequest(
    stRequest := stRequest
    );

iFirstLineEnd := FIND(sMessage, '$R$N');
iHeaderEnd := FIND(sMessage, '$R$N$R$N');
IF (iFirstLineEnd <= 1) OR (iHeaderEnd <= iFirstLineEnd) THEN
    M_SetError(
        eNewError := E_HttpError.iNeedMoreData,
        sMessage  := 'request header is incomplete'
        );
    RETURN;
END_IF

sStartLine := LEFT(sMessage, iFirstLineEnd - 1);
iSpace1 := FIND(sStartLine, ' ');
IF iSpace1 <= 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'request start-line misses method'
        );
    RETURN;
END_IF

sAfterMethod := MID(sStartLine, LEN(sStartLine) - iSpace1, iSpace1 + 1);
iSpace2Relative := FIND(sAfterMethod, ' ');
IF iSpace2Relative <= 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'request start-line misses version'
        );
    RETURN;
END_IF

stRequest.sMethod := LEFT(sStartLine, iSpace1 - 1);
stRequest.sTarget := LEFT(sAfterMethod, iSpace2Relative - 1);
stRequest.sVersion := MID(sAfterMethod, LEN(sAfterMethod) - iSpace2Relative, iSpace2Relative + 1);
stRequest.eMethod := M_ParseMethod(
    sMethod := stRequest.sMethod
    );

IF (LEN(stRequest.sTarget) = 0) OR (FIND(stRequest.sVersion, 'HTTP/') <> 1) THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'request target or version is invalid'
        );
    RETURN;
END_IF

iHeaderStart := iFirstLineEnd + 2;
iHeaderLen := iHeaderEnd - iHeaderStart;
IF iHeaderLen > GVL_Http.cnMaxHeaderSize THEN
    M_SetError(
        eNewError := E_HttpError.iBufferTooSmall,
        sMessage  := 'request header exceeds limit'
        );
    RETURN;
END_IF
IF iHeaderLen > 0 THEN
    sHeaderText := MID(sMessage, iHeaderLen, iHeaderStart);
ELSE
    sHeaderText := '';
END_IF
stRequest.sRawHeaders := sHeaderText;

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Host',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderInvalid THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidHeader,
        sMessage  := 'request header grammar is invalid'
        );
    RETURN;
ELSIF NOT bHeaderFound THEN
    M_SetError(
        eNewError := E_HttpError.iMissingHost,
        sMessage  := 'request host is missing'
        );
    RETURN;
ELSIF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iDuplicateHost,
        sMessage  := 'request host is duplicated'
        );
    RETURN;
ELSE
    stRequest.sHost := sHeaderValue;
END_IF

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Content-Type',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stRequest.sContentType := sHeaderValue;
END_IF

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Authorization',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stRequest.sAuthorization := sHeaderValue;
END_IF

stRequest.bTransferChunked := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Transfer-Encoding',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iUnsupportedTransferEncoding,
        sMessage  := 'duplicate transfer-encoding'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    IF F_HttpAsciiEqualsIgnoreCase(
        sLeft  := sHeaderValue,
        sRight := 'chunked'
        ) THEN
        stRequest.bTransferChunked := TRUE;
    ELSE
        M_SetError(
            eNewError := E_HttpError.iUnsupportedTransferEncoding,
            sMessage  := 'unsupported transfer-encoding'
            );
        RETURN;
    END_IF
END_IF

stRequest.bHasContentLength := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Content-Length',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidContentLength,
        sMessage  := 'duplicate content-length'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    stRequest.bHasContentLength := TRUE;
    IF NOT F_HttpParseContentLength(
        sValue   := sHeaderValue,
        udiValue => udiContentLength
        ) THEN
        M_SetError(
            eNewError := E_HttpError.iInvalidContentLength,
            sMessage  := 'invalid content-length'
            );
        RETURN;
    END_IF
    stRequest.udiContentLength := udiContentLength;
    IF stRequest.udiContentLength > TO_UDINT(GVL_Http.cnMaxBodySize) THEN
        // 原因:声明长度已经超过固定 body 缓冲上限时,应立即报协议容量错误,不能等对端关闭后被 TCP 读错误覆盖。
        M_SetError(
            eNewError := E_HttpError.iBodyTooLarge,
            sMessage  := 'request content-length exceeds body limit'
            );
        RETURN;
    END_IF
END_IF

IF stRequest.bTransferChunked AND stRequest.bHasContentLength THEN
    M_SetError(
        eNewError := E_HttpError.iTransferEncodingContentLength,
        sMessage  := 'transfer-encoding and content-length coexist'
        );
    RETURN;
END_IF

stRequest.bConnectionClose := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Connection',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stRequest.bConnectionClose := F_HttpAsciiEqualsIgnoreCase(
        sLeft  := sHeaderValue,
        sRight := 'close'
        );
END_IF

iBodyStart := iHeaderEnd + 4;
iBodyLen := LEN(sMessage) - iBodyStart + 1;
IF iBodyLen < 0 THEN
    iBodyLen := 0;
END_IF
IF iBodyLen > GVL_Http.cnMaxBodySize THEN
    M_SetError(
        eNewError := E_HttpError.iBodyTooLarge,
        sMessage  := 'request body exceeds limit'
        );
    RETURN;
END_IF
IF iBodyLen > 0 THEN
    sBodyText := MID(sMessage, iBodyLen, iBodyStart);
ELSE
    sBodyText := '';
END_IF

IF stRequest.bTransferChunked THEN
    fbChunked.M_Decode(
        sChunked := sBodyText,
        sBody    => stRequest.sBody
        );
    IF fbChunked.bError THEN
        M_SetError(
            eNewError := fbChunked.eError,
            sMessage  := fbChunked.sDiagMsg
            );
        RETURN;
    END_IF
    udiBodyLength := fbChunked.udiDecodedLength;
    stRequest.udiContentLength := udiBodyLength;
ELSIF stRequest.bHasContentLength THEN
    IF TO_UDINT(iBodyLen) < stRequest.udiContentLength THEN
        M_SetError(
            eNewError := E_HttpError.iNeedMoreData,
            sMessage  := 'request body is incomplete'
            );
        RETURN;
    END_IF
    IF stRequest.udiContentLength > 0 THEN
        stRequest.sBody := LEFT(sBodyText, TO_INT(stRequest.udiContentLength));
    END_IF
    udiBodyLength := stRequest.udiContentLength;
ELSE
    stRequest.sBody := '';
    udiBodyLength := 0;
END_IF

bDone := TRUE;
M_ParseRequest := TRUE;

// === METHOD M_ParseResponse ===
/// =======================================================================
/// 名称      : M_ParseResponse
/// 功能      : 解析 HTTP 响应报文。
/// 说明      : Client 使用该方法读取状态码、Header 和 body。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_ParseResponse : BOOL
VAR_INPUT
    sMessage : STRING(GVL_Http.cnMaxMessageSize) := '';    // 诊断或协议文本字段。
END_VAR
VAR_OUTPUT
    stResponse : ST_HttpResponse;    // 结构化协议或测试数据。
END_VAR
VAR
    iFirstLineEnd    : INT;                                     // 计数、长度或状态数值。
    iHeaderEnd       : INT;                                     // 计数、长度或状态数值。
    iSpace1          : INT;                                     // 计数、长度或状态数值。
    iSpace2Relative  : INT;                                     // 计数、长度或状态数值。
    iHeaderStart     : INT;                                     // 计数、长度或状态数值。
    iHeaderLen       : INT;                                     // 计数、长度或状态数值。
    iBodyStart       : INT;                                     // 计数、长度或状态数值。
    iBodyLen         : INT;                                     // 计数、长度或状态数值。
    sStartLine       : STRING(255);                             // 诊断或协议文本字段。
    sAfterVersion    : STRING(255);                             // 诊断或协议文本字段。
    sHeaderText      : STRING(GVL_Http.cnMaxHeaderSize);        // 诊断或协议文本字段。
    sBodyText        : STRING(GVL_Http.cnMaxBodySize);          // 诊断或协议文本字段。
    sHeaderValue     : STRING(GVL_Http.cnMaxHeaderValueLen);    // 诊断或协议文本字段。
    udiContentLength : UDINT;                                   // 计数、长度或状态数值。
    uiHeaderCount    : UINT;                                    // 计数、长度或状态数值。
    bHeaderFound     : BOOL;                                    // 布尔状态或命令标志。
    bHeaderInvalid   : BOOL;                                    // 布尔状态或命令标志。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:Client 响应解析必须同时覆盖 Content-Length、chunked 和 close-delimited 三种常见响应边界。
// 约束:TE+CL 响应按协议错误处理,避免 PLC Client 接受代理或服务器生成的歧义报文。
// 风险:超限 body 在拷贝到业务字符串前即失败,保护固定长度 STRING 不被截断后误判为成功。
// 诊断:状态码、Reason、body 和协议错误码全部写入 stResponse/eError,供真机矩阵逐项断言。
M_Reset();
M_ParseResponse := FALSE;
M_ResetResponse(
    stResponse := stResponse
    );

iFirstLineEnd := FIND(sMessage, '$R$N');
iHeaderEnd := FIND(sMessage, '$R$N$R$N');
IF (iFirstLineEnd <= 1) OR (iHeaderEnd <= iFirstLineEnd) THEN
    M_SetError(
        eNewError := E_HttpError.iNeedMoreData,
        sMessage  := 'response header is incomplete'
        );
    RETURN;
END_IF

sStartLine := LEFT(sMessage, iFirstLineEnd - 1);
iSpace1 := FIND(sStartLine, ' ');
IF (iSpace1 <= 1) OR (FIND(sStartLine, 'HTTP/') <> 1) THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'response start-line is invalid'
        );
    RETURN;
END_IF

sAfterVersion := MID(sStartLine, LEN(sStartLine) - iSpace1, iSpace1 + 1);
iSpace2Relative := FIND(sAfterVersion, ' ');
IF iSpace2Relative <= 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'response start-line misses reason'
        );
    RETURN;
END_IF

stResponse.sVersion := LEFT(sStartLine, iSpace1 - 1);
stResponse.uiStatusCode := STRING_TO_UINT(LEFT(sAfterVersion, iSpace2Relative - 1));
stResponse.sReason := MID(sAfterVersion, LEN(sAfterVersion) - iSpace2Relative, iSpace2Relative + 1);
IF stResponse.uiStatusCode = 0 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'response status code is invalid'
        );
    RETURN;
END_IF

iHeaderStart := iFirstLineEnd + 2;
iHeaderLen := iHeaderEnd - iHeaderStart;
IF iHeaderLen > GVL_Http.cnMaxHeaderSize THEN
    M_SetError(
        eNewError := E_HttpError.iBufferTooSmall,
        sMessage  := 'response header exceeds limit'
        );
    RETURN;
END_IF
IF iHeaderLen > 0 THEN
    sHeaderText := MID(sMessage, iHeaderLen, iHeaderStart);
ELSE
    sHeaderText := '';
END_IF
stResponse.sRawHeaders := sHeaderText;

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Content-Type',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderInvalid THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidHeader,
        sMessage  := 'response header grammar is invalid'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    stResponse.sContentType := sHeaderValue;
END_IF

stResponse.bTransferChunked := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Transfer-Encoding',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iUnsupportedTransferEncoding,
        sMessage  := 'duplicate response transfer-encoding'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    IF F_HttpAsciiEqualsIgnoreCase(
        sLeft  := sHeaderValue,
        sRight := 'chunked'
        ) THEN
        stResponse.bTransferChunked := TRUE;
    ELSE
        M_SetError(
            eNewError := E_HttpError.iUnsupportedTransferEncoding,
            sMessage  := 'unsupported response transfer-encoding'
            );
        RETURN;
    END_IF
END_IF

stResponse.bHasContentLength := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Content-Length',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidContentLength,
        sMessage  := 'duplicate response content-length'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    stResponse.bHasContentLength := TRUE;
    IF NOT F_HttpParseContentLength(
        sValue   := sHeaderValue,
        udiValue => udiContentLength
        ) THEN
        M_SetError(
            eNewError := E_HttpError.iInvalidContentLength,
            sMessage  := 'invalid response content-length'
            );
        RETURN;
    END_IF
    stResponse.udiContentLength := udiContentLength;
    IF stResponse.udiContentLength > TO_UDINT(GVL_Http.cnMaxBodySize) THEN
        // 原因:真实服务器可能在超限 body 发送后立即关闭连接,Header 阶段先失败可保留 HTTP 协议级诊断。
        M_SetError(
            eNewError := E_HttpError.iBodyTooLarge,
            sMessage  := 'response content-length exceeds body limit'
            );
        RETURN;
    END_IF
END_IF

IF stResponse.bTransferChunked AND stResponse.bHasContentLength THEN
    M_SetError(
        eNewError := E_HttpError.iTransferEncodingContentLength,
        sMessage  := 'response transfer-encoding and content-length coexist'
        );
    RETURN;
END_IF

stResponse.bConnectionClose := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Connection',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stResponse.bConnectionClose := F_HttpAsciiEqualsIgnoreCase(
        sLeft  := sHeaderValue,
        sRight := 'close'
        );
END_IF

iBodyStart := iHeaderEnd + 4;
iBodyLen := LEN(sMessage) - iBodyStart + 1;
IF iBodyLen < 0 THEN
    iBodyLen := 0;
END_IF
IF iBodyLen > GVL_Http.cnMaxBodySize THEN
    M_SetError(
        eNewError := E_HttpError.iBodyTooLarge,
        sMessage  := 'response body exceeds limit'
        );
    RETURN;
END_IF
IF iBodyLen > 0 THEN
    sBodyText := MID(sMessage, iBodyLen, iBodyStart);
ELSE
    sBodyText := '';
END_IF

IF stResponse.bTransferChunked THEN
    fbChunked.M_Decode(
        sChunked := sBodyText,
        sBody    => stResponse.sBody
        );
    IF fbChunked.bError THEN
        M_SetError(
            eNewError := fbChunked.eError,
            sMessage  := fbChunked.sDiagMsg
            );
        RETURN;
    END_IF
    udiBodyLength := fbChunked.udiDecodedLength;
    stResponse.udiContentLength := udiBodyLength;
ELSIF stResponse.bHasContentLength THEN
    IF TO_UDINT(iBodyLen) < stResponse.udiContentLength THEN
        M_SetError(
            eNewError := E_HttpError.iNeedMoreData,
            sMessage  := 'response body is incomplete'
            );
        RETURN;
    END_IF
    IF stResponse.udiContentLength > 0 THEN
        stResponse.sBody := LEFT(sBodyText, TO_INT(stResponse.udiContentLength));
    END_IF
    udiBodyLength := stResponse.udiContentLength;
ELSE
    stResponse.sBody := sBodyText;
    udiBodyLength := TO_UDINT(LEN(sBodyText));
END_IF

bDone := TRUE;
M_ParseResponse := TRUE;

// === METHOD M_ParseMethod ===
/// =======================================================================
/// 名称      : M_ParseMethod
/// 功能      : 将请求方法文本转换为枚举。
/// 说明      : 未覆盖方法保留原始 sMethod,并以 iUnknown 输出。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ParseMethod : E_HttpMethod
VAR_INPUT
    sMethod : STRING(16) := '';    // 诊断或协议文本字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
IF sMethod = 'GET' THEN
    M_ParseMethod := E_HttpMethod.iGet;
ELSIF sMethod = 'HEAD' THEN
    M_ParseMethod := E_HttpMethod.iHead;
ELSIF sMethod = 'POST' THEN
    M_ParseMethod := E_HttpMethod.iPost;
ELSIF sMethod = 'PUT' THEN
    M_ParseMethod := E_HttpMethod.iPut;
ELSIF sMethod = 'DELETE' THEN
    M_ParseMethod := E_HttpMethod.iDelete;
ELSIF sMethod = 'PATCH' THEN
    M_ParseMethod := E_HttpMethod.iPatch;
ELSIF sMethod = 'OPTIONS' THEN
    M_ParseMethod := E_HttpMethod.iOptions;
ELSE
    M_ParseMethod := E_HttpMethod.iUnknown;
END_IF

// === METHOD M_Reset ===
/// =======================================================================
/// 名称      : M_Reset
/// 功能      : 清空解析器诊断状态。
/// 说明      : 每次解析前执行,保证 bDone/bError/eError 不串扰。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_Reset
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
bDone := FALSE;
bError := FALSE;
eError := E_HttpError.iNoError;
sDiagMsg := '';
udiBodyLength := 0;

// === METHOD M_ResetRequest ===
/// =======================================================================
/// 名称      : M_ResetRequest
/// 功能      : 清空请求结构。
/// 说明      : 防止调用者复用结构体时残留旧 Header 或 body。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ResetRequest
VAR_IN_OUT
    stRequest : ST_HttpRequest;    // 结构化协议或测试数据。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
stRequest.eMethod := E_HttpMethod.iUnknown;
stRequest.sMethod := '';
stRequest.sTarget := '';
stRequest.sVersion := 'HTTP/1.1';
stRequest.sHost := '';
stRequest.sContentType := '';
stRequest.sAuthorization := '';
stRequest.sAdditionalHeader := '';
stRequest.sRawHeaders := '';
stRequest.sBody := '';
stRequest.bHasContentLength := FALSE;
stRequest.bTransferChunked := FALSE;
stRequest.bConnectionClose := FALSE;
stRequest.udiContentLength := 0;

// === METHOD M_ResetResponse ===
/// =======================================================================
/// 名称      : M_ResetResponse
/// 功能      : 清空响应结构。
/// 说明      : 防止 Client 连续请求时旧响应残留影响新判断。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_ResetResponse
VAR_IN_OUT
    stResponse : ST_HttpResponse;    // 结构化协议或测试数据。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
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;

// === METHOD M_SetError ===
/// =======================================================================
/// 名称      : M_SetError
/// 功能      : 锁存解析错误诊断。
/// 说明      : 协议错误统一出口,便于在线诊断和离线断言读取。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_SetError
VAR_INPUT
    eNewError : E_HttpError := E_HttpError.iNoError;    // 枚举状态或错误码。
    sMessage  : STRING(255) := '';                      // 诊断或协议文本字段。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
bDone := FALSE;
bError := TRUE;
eError := eNewError;
sDiagMsg := sMessage;

// === METHOD M_ParseRequestHead ===
/// =======================================================================
/// 名称      : M_ParseRequestHead
/// 功能      : 只解析 HTTP 请求行和 Header。
/// 说明      : 大 body 由连接层按 BYTE 缓冲处理,避免 STRING 运行时长度边界影响完整性判断。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_ParseRequestHead : BOOL
VAR_INPUT
    sMessage : STRING(GVL_Http.cnMaxHeaderSize) := '';    // 只包含 Header 区域的请求文本。
END_VAR
VAR_OUTPUT
    stRequest : ST_HttpRequest;    // 结构化协议或测试数据。
END_VAR
VAR
    iFirstLineEnd    : INT;                                     // 计数、长度或状态数值。
    iHeaderEnd       : INT;                                     // 计数、长度或状态数值。
    iSpace1          : INT;                                     // 计数、长度或状态数值。
    iSpace2Relative  : INT;                                     // 计数、长度或状态数值。
    iHeaderStart     : INT;                                     // 计数、长度或状态数值。
    iHeaderLen       : INT;                                     // 计数、长度或状态数值。
    sStartLine       : STRING(255);                             // 诊断或协议文本字段。
    sAfterMethod     : STRING(255);                             // 诊断或协议文本字段。
    sHeaderText      : STRING(GVL_Http.cnMaxHeaderSize);        // 诊断或协议文本字段。
    sHeaderValue     : STRING(GVL_Http.cnMaxHeaderValueLen);    // 诊断或协议文本字段。
    udiContentLength : UDINT;                                   // 计数、长度或状态数值。
    uiHeaderCount    : UINT;                                    // 计数、长度或状态数值。
    bHeaderFound     : BOOL;                                    // 布尔状态或命令标志。
    bHeaderInvalid   : BOOL;                                    // 布尔状态或命令标志。
END_VAR
// === IMPLEMENTATION ===
// 原因:真实 PLC 运行时的 STRING 标准函数在长文本上存在 255 字节边界风险;
// Header-only 解析让 body 留在 BYTE 缓冲中,由连接层用真实接收字节数判断完整性。
// 诊断:Header 阶段仍保留 Host、TE/CL、Content-Length 和 Connection 的协议错误码,便于外部 4xx 用例定位。
M_Reset();
M_ParseRequestHead := FALSE;
M_ResetRequest(
    stRequest := stRequest
    );

iFirstLineEnd := FIND(sMessage, '$R$N');
iHeaderEnd := FIND(sMessage, '$R$N$R$N');
IF (iFirstLineEnd <= 1) OR (iHeaderEnd <= iFirstLineEnd) THEN
    M_SetError(
        eNewError := E_HttpError.iNeedMoreData,
        sMessage  := 'request header is incomplete'
        );
    RETURN;
END_IF

sStartLine := LEFT(sMessage, iFirstLineEnd - 1);
iSpace1 := FIND(sStartLine, ' ');
IF iSpace1 <= 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'request start-line misses method'
        );
    RETURN;
END_IF

sAfterMethod := MID(sStartLine, LEN(sStartLine) - iSpace1, iSpace1 + 1);
iSpace2Relative := FIND(sAfterMethod, ' ');
IF iSpace2Relative <= 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'request start-line misses version'
        );
    RETURN;
END_IF

stRequest.sMethod := LEFT(sStartLine, iSpace1 - 1);
stRequest.sTarget := LEFT(sAfterMethod, iSpace2Relative - 1);
stRequest.sVersion := MID(sAfterMethod, LEN(sAfterMethod) - iSpace2Relative, iSpace2Relative + 1);
stRequest.eMethod := M_ParseMethod(
    sMethod := stRequest.sMethod
    );

IF (LEN(stRequest.sTarget) = 0) OR (FIND(stRequest.sVersion, 'HTTP/') <> 1) THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidStartLine,
        sMessage  := 'request target or version is invalid'
        );
    RETURN;
END_IF

iHeaderStart := iFirstLineEnd + 2;
iHeaderLen := iHeaderEnd - iHeaderStart;
IF iHeaderLen > GVL_Http.cnMaxHeaderSize THEN
    M_SetError(
        eNewError := E_HttpError.iBufferTooSmall,
        sMessage  := 'request header exceeds limit'
        );
    RETURN;
END_IF
IF iHeaderLen > 0 THEN
    sHeaderText := MID(sMessage, iHeaderLen, iHeaderStart);
ELSE
    sHeaderText := '';
END_IF
stRequest.sRawHeaders := sHeaderText;

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Host',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderInvalid THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidHeader,
        sMessage  := 'request header grammar is invalid'
        );
    RETURN;
ELSIF NOT bHeaderFound THEN
    M_SetError(
        eNewError := E_HttpError.iMissingHost,
        sMessage  := 'request host is missing'
        );
    RETURN;
ELSIF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iDuplicateHost,
        sMessage  := 'request host is duplicated'
        );
    RETURN;
ELSE
    stRequest.sHost := sHeaderValue;
END_IF

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Content-Type',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stRequest.sContentType := sHeaderValue;
END_IF

bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Authorization',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stRequest.sAuthorization := sHeaderValue;
END_IF

stRequest.bTransferChunked := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Transfer-Encoding',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iUnsupportedTransferEncoding,
        sMessage  := 'duplicate transfer-encoding'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    IF F_HttpAsciiEqualsIgnoreCase(
        sLeft  := sHeaderValue,
        sRight := 'chunked'
        ) THEN
        stRequest.bTransferChunked := TRUE;
    ELSE
        M_SetError(
            eNewError := E_HttpError.iUnsupportedTransferEncoding,
            sMessage  := 'unsupported transfer-encoding'
            );
        RETURN;
    END_IF
END_IF

stRequest.bHasContentLength := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Content-Length',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF uiHeaderCount > 1 THEN
    M_SetError(
        eNewError := E_HttpError.iInvalidContentLength,
        sMessage  := 'duplicate content-length'
        );
    RETURN;
END_IF
IF bHeaderFound THEN
    stRequest.bHasContentLength := TRUE;
    IF NOT F_HttpParseContentLength(
        sValue   := sHeaderValue,
        udiValue => udiContentLength
        ) THEN
        M_SetError(
            eNewError := E_HttpError.iInvalidContentLength,
            sMessage  := 'invalid content-length'
            );
        RETURN;
    END_IF
    stRequest.udiContentLength := udiContentLength;
    IF stRequest.udiContentLength > TO_UDINT(GVL_Http.cnMaxBodySize) THEN
        M_SetError(
            eNewError := E_HttpError.iBodyTooLarge,
            sMessage  := 'request content-length exceeds body limit'
            );
        RETURN;
    END_IF
END_IF

IF stRequest.bTransferChunked AND stRequest.bHasContentLength THEN
    M_SetError(
        eNewError := E_HttpError.iTransferEncodingContentLength,
        sMessage  := 'transfer-encoding and content-length coexist'
        );
    RETURN;
END_IF

stRequest.bConnectionClose := FALSE;
bHeaderFound := F_HttpFindHeader(
    sHeaders        := sHeaderText,
    sHeaderName     := 'Connection',
    sValue          => sHeaderValue,
    uiCount         => uiHeaderCount,
    bInvalidGrammar => bHeaderInvalid
    );
IF bHeaderFound THEN
    stRequest.bConnectionClose := F_HttpAsciiEqualsIgnoreCase(
        sLeft  := sHeaderValue,
        sRight := 'close'
        );
END_IF

bDone := TRUE;
M_ParseRequestHead := TRUE;

完整源码 2:F_HttpFindHeader.st

职责:Header 查找与语法检查

iecst
/// 功能    : 从 HTTP Header 文本中查找字段值。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION F_HttpFindHeader : BOOL
VAR_INPUT
    sHeaders    : STRING(GVL_Http.cnMaxHeaderSize) := '';    // 诊断或协议文本字段。
    sHeaderName : STRING(64) := '';                          // 诊断或协议文本字段。
END_VAR
VAR_OUTPUT
    sValue          : STRING(GVL_Http.cnMaxHeaderValueLen);     // 诊断或协议文本字段。
    uiCount         : UINT;                                     // 计数、长度或状态数值。
    bInvalidGrammar : BOOL;                                     // 布尔状态或命令标志。
END_VAR
VAR
    uiHeadersLen : UINT;               // 计数、长度或状态数值。
    uiNameLen    : UINT;               // 计数、长度或状态数值。
    uiIndex      : UINT;               // 计数、长度或状态数值。
    uiLineIndex  : UINT;               // 计数、长度或状态数值。
    uiNameIndex  : UINT;               // 计数、长度或状态数值。
    uiValueIndex : UINT;               // 计数、长度或状态数值。
    uiOutIndex   : UINT;               // 计数、长度或状态数值。
    bLineStart   : BOOL;               // 布尔状态或命令标志。
    bColonFound  : BOOL;               // 布尔状态或命令标志。
    bMatched     : BOOL;               // 布尔状态或命令标志。
    pWrite       : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : F_HttpFindHeader
/// 功能      : 按 Header 名大小写不敏感规则查找首个字段值。
/// 库依赖    : 暂无
/// =======================================================================
/// 使用说明  : 1. 输入应为不含 start-line 的 Header 文本。
///           : 2. 返回值会去掉冒号后的前导空格和 Tab,不做 token 拆分。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:Header 字段名按大小写不敏感匹配,但冒号前空白必须拒绝,避免兼容旧代理产生协议歧义。
// 诊断:uiCount 与 bInvalidGrammar 同时输出,调用方可区分缺失、重复和语法错误三类风险。
sValue := '';
uiCount := 0;
bInvalidGrammar := FALSE;
F_HttpFindHeader := FALSE;
uiHeadersLen := TO_UINT(LEN(sHeaders));
uiNameLen := TO_UINT(LEN(sHeaderName));
pWrite := ADR(sValue);

IF (uiHeadersLen = 0) OR (uiNameLen = 0) THEN
    RETURN;
END_IF

uiIndex := 0;
WHILE uiIndex < uiHeadersLen DO
    bLineStart := (uiIndex = 0)
        OR ((uiIndex >= 2) AND (sHeaders[uiIndex - 2] = 16#0D) AND (sHeaders[uiIndex - 1] = 16#0A));
    bMatched := FALSE;

    IF bLineStart THEN
        IF (sHeaders[uiIndex] = 16#0D) OR (sHeaders[uiIndex] = 16#0A) THEN
            bInvalidGrammar := TRUE;
            RETURN;
        END_IF

        uiLineIndex := uiIndex;
        bColonFound := FALSE;
        WHILE uiLineIndex < uiHeadersLen DO
            IF (sHeaders[uiLineIndex] = 16#0D) OR (sHeaders[uiLineIndex] = 16#0A) THEN
                EXIT;
            END_IF

            IF sHeaders[uiLineIndex] = 16#3A THEN
                bColonFound := uiLineIndex > uiIndex;
                EXIT;
            END_IF

            IF (sHeaders[uiLineIndex] = 16#20)
                OR (sHeaders[uiLineIndex] = 16#09)
                OR (sHeaders[uiLineIndex] < 16#21)
                OR (sHeaders[uiLineIndex] = 16#7F) THEN
                bInvalidGrammar := TRUE;
                RETURN;
            END_IF

            uiLineIndex := uiLineIndex + 1;
        END_WHILE

        IF NOT bColonFound THEN
            bInvalidGrammar := TRUE;
            RETURN;
        END_IF

        IF (uiIndex + uiNameLen) = uiLineIndex THEN
            bMatched := TRUE;
            uiNameIndex := 0;
            WHILE uiNameIndex < uiNameLen DO
                IF NOT F_HttpAsciiEqualsIgnoreCase(
                    sLeft  := MID(sHeaders, 1, TO_INT(uiIndex + uiNameIndex + 1)),
                    sRight := MID(sHeaderName, 1, TO_INT(uiNameIndex + 1))
                    ) THEN
                    bMatched := FALSE;
                    EXIT;
                END_IF
                uiNameIndex := uiNameIndex + 1;
            END_WHILE
        END_IF

        IF bMatched THEN
            uiValueIndex := uiIndex + uiNameLen + 1;
            WHILE (uiValueIndex < uiHeadersLen)
                AND ((sHeaders[uiValueIndex] = 16#20) OR (sHeaders[uiValueIndex] = 16#09)) DO
                uiValueIndex := uiValueIndex + 1;
            END_WHILE

            IF uiCount = 0 THEN
                uiOutIndex := 0;
                WHILE (uiValueIndex < uiHeadersLen)
                    AND NOT ((sHeaders[uiValueIndex] = 16#0D) OR (sHeaders[uiValueIndex] = 16#0A))
                    AND (uiOutIndex < GVL_Http.cnMaxHeaderValueLen) DO
                    pWrite^ := TO_BYTE(sHeaders[uiValueIndex]);
                    pWrite := pWrite + 1;
                    uiValueIndex := uiValueIndex + 1;
                    uiOutIndex := uiOutIndex + 1;
                END_WHILE
                pWrite^ := 0;
            END_IF
            uiCount := uiCount + 1;
        END_IF
    END_IF

    uiIndex := uiIndex + 1;
END_WHILE

F_HttpFindHeader := uiCount > 0;

完整源码 3:F_HttpAsciiEqualsIgnoreCase.st

职责:ASCII 不区分大小写比较

iecst
/// 功能    : ASCII 字符串忽略大小写比较。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION F_HttpAsciiEqualsIgnoreCase : BOOL
VAR_INPUT
    sLeft  : STRING(255) := '';    // 诊断或协议文本字段。
    sRight : STRING(255) := '';    // 诊断或协议文本字段。
END_VAR
VAR
    uiIndex : UINT;               // 计数、长度或状态数值。
    uiLen   : UINT;               // 计数、长度或状态数值。
    byLeft  : BYTE;               // 计数、长度或状态数值。
    byRight : BYTE;               // 计数、长度或状态数值。
    pLeft   : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
    pRight  : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : F_HttpAsciiEqualsIgnoreCase
/// 功能      : 按 HTTP Header 的 ASCII 语义进行大小写不敏感比较。
/// 库依赖    : 暂无
/// =======================================================================
/// 使用说明  : 1. 本函数只处理 ASCII 范围,避免引入 Unicode 或区域设置行为。
///           : 2. 长度不同直接返回 FALSE,适合 Header 名和固定 token 比较。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
F_HttpAsciiEqualsIgnoreCase := FALSE;

IF LEN(sLeft) <> LEN(sRight) THEN
    RETURN;
END_IF

uiLen := TO_UINT(LEN(sLeft));
IF uiLen = 0 THEN
    F_HttpAsciiEqualsIgnoreCase := TRUE;
    RETURN;
END_IF

pLeft := ADR(sLeft);
pRight := ADR(sRight);
FOR uiIndex := 0 TO uiLen - 1 DO
    byLeft := pLeft[uiIndex];
    byRight := pRight[uiIndex];
    IF (byLeft >= 16#61) AND (byLeft <= 16#7A) THEN
        byLeft := UINT_TO_BYTE(BYTE_TO_UINT(byLeft) - 32);
    END_IF
    IF (byRight >= 16#61) AND (byRight <= 16#7A) THEN
        byRight := UINT_TO_BYTE(BYTE_TO_UINT(byRight) - 32);
    END_IF
    IF byLeft <> byRight THEN
        RETURN;
    END_IF
END_FOR

F_HttpAsciiEqualsIgnoreCase := TRUE;

完整源码 4:F_HttpParseContentLength.st

职责:Content-Length 十进制解析

iecst
/// 功能    : 严格解析 HTTP Content-Length。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION F_HttpParseContentLength : BOOL
VAR_INPUT
    sValue : STRING(64) := '';    // 诊断或协议文本字段。
END_VAR
VAR_OUTPUT
    udiValue : UDINT;    // 计数、长度或状态数值。
END_VAR
VAR
    uiIndex : UINT;               // 计数、长度或状态数值。
    uiLen   : UINT;               // 计数、长度或状态数值。
    byChar  : BYTE;               // 计数、长度或状态数值。
    pText   : POINTER TO BYTE;    // 当前扫描周期使用的字节指针。
END_VAR

// === IMPLEMENTATION ===
/// =======================================================================
/// 名称      : F_HttpParseContentLength
/// 功能      : 只接受十进制非负整数并输出字节长度。
/// 库依赖    : 暂无
/// =======================================================================
/// 使用说明  : 1. 空值、非数字和值超过 UDINT 范围均返回 FALSE。
///           : 2. body 上限由 Parser 统一判断为 iBodyTooLarge,避免把合法长度字段误报为非法语法。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
F_HttpParseContentLength := FALSE;
udiValue := 0;
uiLen := TO_UINT(LEN(sValue));
IF uiLen = 0 THEN
    RETURN;
END_IF

pText := ADR(sValue);
FOR uiIndex := 0 TO uiLen - 1 DO
    byChar := pText[uiIndex];
    IF (byChar < 16#30) OR (byChar > 16#39) THEN
        RETURN;
    END_IF
    // 原因:Content-Length 字段本身只校验十进制语法和 UDINT 上限,超过库 body 上限由 Parser 分类诊断。
    IF (udiValue > 429496729) OR ((udiValue = 429496729) AND (BYTE_TO_UDINT(byChar - 16#30) > 5)) THEN
        RETURN;
    END_IF
    udiValue := (udiValue * 10) + BYTE_TO_UDINT(byChar - 16#30);
END_FOR

F_HttpParseContentLength := TRUE;

本篇阅读抓手

  • 字段名按 ASCII 不区分大小写。
  • 缺失与非法必须分开。
  • NeedMoreData 不能伪装成完成。

如何验证这组源码

  1. 先针对 FB_HttpMessageParser.st 的公开输入和错误出口建立确定性用例。
  2. 再把“字段名按 ASCII 不区分大小写”转成至少一个正常场景和一个失败场景。
  3. 本篇 4 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
  4. 真机复核时重点观察“NeedMoreData 不能伪装成完成”,并保留对应状态、计数和原始报文。

这一篇你最该记住

  • 字段名按 ASCII 不区分大小写。
  • 缺失与非法必须分开。
  • NeedMoreData 不能伪装成完成。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 20/28 篇。
  • 当前源码加更:第 2/8 篇。
  • 本篇完整源码文件数:4。
  • 上一篇:第19篇
  • 下一篇:第21篇
评论和回复区

评论区预留

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

↑ ↓