这一组代码负责把原始 HTTP 文本变成结构化请求或响应,并拒绝 Host、Header、Content-Length 与 Transfer-Encoding 的歧义。
适合谁收藏
- 准备复用本组源码并审查对象边界的工程师。
- 需要把公开代码装配进目标 CodeSys 工程的人。
- 准备重新完成编译、离线测试和真机验证的读者。
本篇完整公开 4 个 ST 文件。代码直接读取已验证工程,保留声明、实现、注释和缩进;没有伪代码,没有跨文件拼接,也没有省略号。
先给结论
这一组代码负责把原始 HTTP 文本变成结构化请求或响应,并拒绝 Host、Header、Content-Length 与 Transfer-Encoding 的歧义。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

读图重点
先找到 FB_HttpMessageParser 在本组中的位置,再沿图确认其余对象分别承担数据、状态、执行或诊断职责。图只给阅读顺序,最终判断必须回到下面的完整 ST 代码。
先看文件职责
| 序号 | 文件 | 职责 | 行数 |
|---|---|---|---|
| 1 | FB_HttpMessageParser.st | 请求与响应解析器 | 957 |
| 2 | F_HttpFindHeader.st | Header 查找与语法检查 | 134 |
| 3 | F_HttpAsciiEqualsIgnoreCase.st | ASCII 不区分大小写比较 | 57 |
| 4 | F_HttpParseContentLength.st | Content-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
职责:请求与响应解析器
/// 功能 : 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 查找与语法检查
/// 功能 : 从 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 不区分大小写比较
/// 功能 : 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 十进制解析
/// 功能 : 严格解析 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 不能伪装成完成。
如何验证这组源码
- 先针对
FB_HttpMessageParser.st的公开输入和错误出口建立确定性用例。 - 再把“字段名按 ASCII 不区分大小写”转成至少一个正常场景和一个失败场景。
- 本篇 4 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
- 真机复核时重点观察“NeedMoreData 不能伪装成完成”,并保留对应状态、计数和原始报文。
这一篇你最该记住
- 字段名按 ASCII 不区分大小写。
- 缺失与非法必须分开。
- NeedMoreData 不能伪装成完成。
系列导航
- 系列:CodeSys HTTP 系列教程,第 20/28 篇。
- 当前源码加更:第 2/8 篇。
- 本篇完整源码文件数:4。
- 上一篇:第19篇
- 下一篇:第21篇
评论区预留
这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。