构造器把结构体变成对端可识别的 HTTP/1.1 报文。方法文本、原因短语、长度和连接策略在这里统一收口。
适合谁收藏
- 准备复用本组源码并审查对象边界的工程师。
- 需要把公开代码装配进目标 CodeSys 工程的人。
- 准备重新完成编译、离线测试和真机验证的读者。
本篇完整公开 3 个 ST 文件。代码直接读取已验证工程,保留声明、实现、注释和缩进;没有伪代码,没有跨文件拼接,也没有省略号。
先给结论
构造器把结构体变成对端可识别的 HTTP/1.1 报文。方法文本、原因短语、长度和连接策略在这里统一收口。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

读图重点
先找到 FB_HttpMessageBuilder 在本组中的位置,再沿图确认其余对象分别承担数据、状态、执行或诊断职责。图只给阅读顺序,最终判断必须回到下面的完整 ST 代码。
先看文件职责
| 序号 | 文件 | 职责 | 行数 |
|---|---|---|---|
| 1 | FB_HttpMessageBuilder.st | 请求与响应构造器 | 244 |
| 2 | F_HttpMethodToString.st | 方法枚举转文本 | 35 |
| 3 | F_HttpStatusReason.st | 状态码原因短语 | 43 |
阅读顺序不是按文件名机械展开。先看数据和状态,再看公开方法,最后顺着错误出口和调用对象检查边界。源码篇的目标是让读者能对照工程复现,不是用大段代码制造篇幅。
从协议约束到代码职责
协议约束
构造器把结构体变成对端可识别的 HTTP/1.1 报文。方法文本、原因短语、长度和连接策略在这里统一收口。 协议层只定义消息与状态成立的条件,工程层还必须把条件分配给确定对象,避免 Parser、Builder、Server、Client 和测试入口互相越权。
对象分工
- 第 1 个对象
FB_HttpMessageBuilder.st:承担“请求与响应构造器”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。 - 第 2 个对象
F_HttpMethodToString.st:承担“方法枚举转文本”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。 - 第 3 个对象
F_HttpStatusReason.st:承担“状态码原因短语”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
装配与验证
这组文件必须作为一个职责单元阅读和编译。先用确定输入验证“Host 是请求构造硬门槛”,再制造非法或容量边界验证“出站统一显式 Content-Length”,最后在真实通信或上层调用中确认“消息超限时拒绝发送”。如果单文件测试通过但装配后失败,应优先检查结构体、常量、状态枚举和周期调用关系,而不是立即重写核心算法。源码完整公开只证明读者拿到了同一事实源,目标运行时是否通过仍需重新编译和真机取证。
本篇从 FB_HttpMessageBuilder.st 开始审查:先确认“Host 是请求构造硬门槛”对应的类型、常量或公开输入,再沿 CASE 或方法调用追踪成功路径,随后逐个检查容量、超时和协议错误出口,最后在 F_HttpStatusReason.st 对应的结果或装配位置确认错误能被观测、清理并再次执行。这个顺序专门用来区分“算法正确但没有周期调用”“状态能完成但错误被覆盖”和“消息超限时拒绝发送尚未形成验证证据”三类问题。只有本篇源码、装配和验证使用同一组对象与同一组边界,完整开源才具有可复现意义。
本篇完整开源代码
下面按职责顺序给出本篇全部 ST 文件。每个代码块保留完整声明与实现;阅读时把状态、长度、错误出口和调用对象与上面的职责表逐项对照。
完整源码 1:FB_HttpMessageBuilder.st
职责:请求与响应构造器
/// 功能 : HTTP/1.1 请求与响应构造器。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_HttpMessageBuilder
VAR_OUTPUT
bDone : BOOL; // 报文构造完成标志。
bError : BOOL; // 错误锁存标志。
diErrorID : DINT; // 诊断错误码,0表示无错误。
eError : E_HttpError; // 最近一次构造错误码。
sDiagMsg : STRING(255); // 最近一次构造诊断文本。
udiMessageLength : UDINT; // 构造后的报文字节长度。
END_VAR
// === IMPLEMENTATION ===
/// =======================================================================
/// 名称 : FB_HttpMessageBuilder
/// 功能 : 构造首版 HTTP/1.1 Client 请求与 Server 响应文本。
/// 库依赖 : 暂无
/// =======================================================================
/// 使用说明 : 1. 由调用者通过 bConnectionClose 选择 `keep-alive` 或 `close`。
/// : 2. 不生成 chunked 出站报文,统一使用 Content-Length 便于 PLC 侧诊断。
/// =======================================================================
// === METHOD M_BuildRequest ===
/// =======================================================================
/// 名称 : M_BuildRequest
/// 功能 : 将 ST_HttpRequest 构造为 HTTP 请求文本。
/// 说明 : Host 必填,body 非空时自动填充 Content-Length。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_BuildRequest : BOOL
VAR_IN_OUT
stRequest : ST_HttpRequest; // 结构化协议或测试数据。
END_VAR
VAR_OUTPUT
sMessage : STRING(GVL_Http.cnMaxMessageSize); // 诊断或协议文本字段。
END_VAR
VAR
sMethod : STRING(16); // 诊断或协议文本字段。
sTarget : STRING(GVL_Http.cnMaxTargetLen); // 诊断或协议文本字段。
sContentLen : STRING(16); // 诊断或协议文本字段。
uiBodyLen : UINT; // 计数、长度或状态数值。
uiCandidate : UINT; // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:请求构造统一写入 Host、Connection 和 Content-Length,避免 PLC 端生成歧义 HTTP/1.1 报文。
// 约束:首版不生成 chunked 出站请求,长连接只允许顺序复用,不允许 pipeline 并发请求。
M_Reset();
M_BuildRequest := FALSE;
sMessage := '';
IF LEN(stRequest.sHost) = 0 THEN
M_SetError(
eNewError := E_HttpError.iMissingHost,
sMessage := 'request host is empty'
);
RETURN;
END_IF
sMethod := F_HttpMethodToString(
eMethod := stRequest.eMethod
);
IF LEN(stRequest.sTarget) = 0 THEN
sTarget := GVL_Http.cnDefaultPath;
ELSE
sTarget := stRequest.sTarget;
END_IF
uiBodyLen := TO_UINT(LEN(stRequest.sBody));
sContentLen := UINT_TO_STRING(uiBodyLen);
sMessage := CONCAT(sMethod, ' ');
sMessage := CONCAT(sMessage, sTarget);
sMessage := CONCAT(sMessage, ' HTTP/1.1$R$NHost: ');
sMessage := CONCAT(sMessage, stRequest.sHost);
sMessage := CONCAT(sMessage, '$R$NConnection: ');
IF stRequest.bConnectionClose THEN
sMessage := CONCAT(sMessage, 'close$R$N');
ELSE
sMessage := CONCAT(sMessage, 'keep-alive$R$N');
END_IF
IF LEN(stRequest.sAdditionalHeader) > 0 THEN
sMessage := CONCAT(sMessage, stRequest.sAdditionalHeader);
sMessage := CONCAT(sMessage, '$R$N');
END_IF
IF LEN(stRequest.sContentType) > 0 THEN
sMessage := CONCAT(sMessage, 'Content-Type: ');
sMessage := CONCAT(sMessage, stRequest.sContentType);
sMessage := CONCAT(sMessage, '$R$N');
END_IF
IF uiBodyLen > 0 THEN
sMessage := CONCAT(sMessage, 'Content-Length: ');
sMessage := CONCAT(sMessage, sContentLen);
sMessage := CONCAT(sMessage, '$R$N');
END_IF
sMessage := CONCAT(sMessage, '$R$N');
IF uiBodyLen > 0 THEN
sMessage := CONCAT(sMessage, stRequest.sBody);
END_IF
uiCandidate := TO_UINT(LEN(sMessage));
IF uiCandidate >= GVL_Http.cnMaxMessageSize THEN
M_SetError(
eNewError := E_HttpError.iBufferTooSmall,
sMessage := 'request message exceeds buffer'
);
RETURN;
END_IF
udiMessageLength := TO_UDINT(uiCandidate);
bDone := TRUE;
M_BuildRequest := TRUE;
// === METHOD M_BuildResponse ===
/// =======================================================================
/// 名称 : M_BuildResponse
/// 功能 : 将 ST_HttpResponse 构造为 HTTP 响应文本。
/// 说明 : sReason 为空时自动采用常用原因短语。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_BuildResponse : BOOL
VAR_IN_OUT
stResponse : ST_HttpResponse; // 结构化协议或测试数据。
END_VAR
VAR_OUTPUT
sMessage : STRING(GVL_Http.cnMaxMessageSize); // 诊断或协议文本字段。
END_VAR
VAR
sReason : STRING(64); // 诊断或协议文本字段。
sStatus : STRING(16); // 诊断或协议文本字段。
sContentLen : STRING(16); // 诊断或协议文本字段。
sContentType : STRING(96); // 诊断或协议文本字段。
uiBodyLen : UINT; // 计数、长度或状态数值。
uiCandidate : UINT; // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
// 原因:响应构造始终显式 Content-Length,使外部 client 可以用真实字节数验证 Server 行为。
// 风险:状态码为 0 或 body 超过上限时必须拒绝构造,否则 Server 会发送不可诊断的坏报文。
M_Reset();
M_BuildResponse := FALSE;
sMessage := '';
IF stResponse.uiStatusCode = 0 THEN
M_SetError(
eNewError := E_HttpError.iInvalidArgument,
sMessage := 'response status code is zero'
);
RETURN;
END_IF
IF LEN(stResponse.sReason) > 0 THEN
sReason := stResponse.sReason;
ELSE
sReason := F_HttpStatusReason(
uiStatusCode := stResponse.uiStatusCode
);
END_IF
IF LEN(stResponse.sContentType) > 0 THEN
sContentType := stResponse.sContentType;
ELSE
sContentType := GVL_Http.cnDefaultContentType;
END_IF
uiBodyLen := TO_UINT(LEN(stResponse.sBody));
sStatus := UINT_TO_STRING(stResponse.uiStatusCode);
sContentLen := UINT_TO_STRING(uiBodyLen);
sMessage := CONCAT('HTTP/1.1 ', sStatus);
sMessage := CONCAT(sMessage, ' ');
sMessage := CONCAT(sMessage, sReason);
sMessage := CONCAT(sMessage, '$R$NConnection: ');
IF stResponse.bConnectionClose THEN
sMessage := CONCAT(sMessage, 'close$R$NContent-Type: ');
ELSE
sMessage := CONCAT(sMessage, 'keep-alive$R$NContent-Type: ');
END_IF
sMessage := CONCAT(sMessage, sContentType);
sMessage := CONCAT(sMessage, '$R$NContent-Length: ');
sMessage := CONCAT(sMessage, sContentLen);
sMessage := CONCAT(sMessage, '$R$N');
IF LEN(stResponse.sAdditionalHeader) > 0 THEN
sMessage := CONCAT(sMessage, stResponse.sAdditionalHeader);
sMessage := CONCAT(sMessage, '$R$N');
END_IF
sMessage := CONCAT(sMessage, '$R$N');
IF uiBodyLen > 0 THEN
sMessage := CONCAT(sMessage, stResponse.sBody);
END_IF
uiCandidate := TO_UINT(LEN(sMessage));
IF uiCandidate >= GVL_Http.cnMaxMessageSize THEN
M_SetError(
eNewError := E_HttpError.iBufferTooSmall,
sMessage := 'response message exceeds buffer'
);
RETURN;
END_IF
udiMessageLength := TO_UDINT(uiCandidate);
bDone := TRUE;
M_BuildResponse := TRUE;
// === METHOD M_Reset ===
/// =======================================================================
/// 名称 : M_Reset
/// 功能 : 清空构造器诊断状态。
/// 说明 : 每个公开构造方法开头调用,保证输出语义一致。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PRIVATE M_Reset
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
bDone := FALSE;
bError := FALSE;
eError := E_HttpError.iNoError;
sDiagMsg := '';
udiMessageLength := 0;
// === METHOD M_SetError ===
/// =======================================================================
/// 名称 : M_SetError
/// 功能 : 锁存构造错误诊断。
/// 说明 : 内部错误出口统一调用,避免 bError/eError/sDiagMsg 不一致。
/// =======================================================================
{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;完整源码 2:F_HttpMethodToString.st
职责:方法枚举转文本
/// 功能 : HTTP 方法枚举转字符串。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION F_HttpMethodToString : STRING(16)
VAR_INPUT
eMethod : E_HttpMethod := E_HttpMethod.iUnknown; // 枚举状态或错误码。
END_VAR
// === IMPLEMENTATION ===
/// =======================================================================
/// 名称 : F_HttpMethodToString
/// 功能 : 将 E_HttpMethod 转为 HTTP start-line 方法文本。
/// 库依赖 : 暂无
/// =======================================================================
/// 使用说明 : 1. 未知方法返回 GET,保证出站请求可控。
/// : 2. 入站原始方法仍由 ST_HttpRequest.sMethod 保留。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
CASE eMethod OF
E_HttpMethod.iHead:
F_HttpMethodToString := 'HEAD';
E_HttpMethod.iPost:
F_HttpMethodToString := 'POST';
E_HttpMethod.iPut:
F_HttpMethodToString := 'PUT';
E_HttpMethod.iDelete:
F_HttpMethodToString := 'DELETE';
E_HttpMethod.iPatch:
F_HttpMethodToString := 'PATCH';
E_HttpMethod.iOptions:
F_HttpMethodToString := 'OPTIONS';
ELSE
F_HttpMethodToString := 'GET';
END_CASE完整源码 3:F_HttpStatusReason.st
职责:状态码原因短语
/// 功能 : HTTP 状态码默认原因短语。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION F_HttpStatusReason : STRING(64)
VAR_INPUT
uiStatusCode : UINT := 200; // 计数、长度或状态数值。
END_VAR
// === IMPLEMENTATION ===
/// =======================================================================
/// 名称 : F_HttpStatusReason
/// 功能 : 提供首版常用 HTTP 状态码原因短语。
/// 库依赖 : 暂无
/// =======================================================================
/// 使用说明 : 1. Builder 在 sReason 为空时使用本函数填充。
/// : 2. 未覆盖状态码返回 OK 以外不会用于错误判定,仅影响可读文本。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。
CASE uiStatusCode OF
200:
F_HttpStatusReason := 'OK';
201:
F_HttpStatusReason := 'Created';
204:
F_HttpStatusReason := 'No Content';
400:
F_HttpStatusReason := 'Bad Request';
404:
F_HttpStatusReason := 'Not Found';
405:
F_HttpStatusReason := 'Method Not Allowed';
411:
F_HttpStatusReason := 'Length Required';
413:
F_HttpStatusReason := 'Payload Too Large';
500:
F_HttpStatusReason := 'Internal Server Error';
501:
F_HttpStatusReason := 'Not Implemented';
ELSE
F_HttpStatusReason := 'OK';
END_CASE本篇阅读抓手
- Host 是请求构造硬门槛。
- 出站统一显式 Content-Length。
- 消息超限时拒绝发送。
如何验证这组源码
- 先针对
FB_HttpMessageBuilder.st的公开输入和错误出口建立确定性用例。 - 再把“Host 是请求构造硬门槛”转成至少一个正常场景和一个失败场景。
- 本篇 3 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
- 真机复核时重点观察“消息超限时拒绝发送”,并保留对应状态、计数和原始报文。
这一篇你最该记住
- Host 是请求构造硬门槛。
- 出站统一显式 Content-Length。
- 消息超限时拒绝发送。
系列导航
- 系列:CodeSys HTTP 系列教程,第 21/28 篇。
- 当前源码加更:第 3/8 篇。
- 本篇完整源码文件数:3。
- 上一篇:第20篇
- 下一篇:第22篇
评论区预留
这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。