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

读图重点
先找到 FB_HttpMessageBuilder 在本组中的位置,再沿图确认其余对象分别承担数据、状态、执行或诊断职责。图只给阅读顺序,最终判断必须回到下面的完整 ST 代码。
先看文件职责
FB_HttpMessageBuilder.st | |||
F_HttpMethodToString.st | |||
F_HttpStatusReason.st |
阅读顺序不是按文件名机械展开。先看数据和状态,再看公开方法,最后顺着错误出口和调用对象检查边界。源码篇的目标是让读者能对照工程复现,不是用大段代码制造篇幅。
从协议约束到代码职责
协议约束
构造器把结构体变成对端可识别的 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_HttpMessageBuilderVAR_OUTPUTbDone : 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 : BOOLVAR_IN_OUTstRequest : ST_HttpRequest; // 结构化协议或测试数据。END_VARVAR_OUTPUTsMessage : STRING(GVL_Http.cnMaxMessageSize); // 诊断或协议文本字段。END_VARVARsMethod : 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 THENM_SetError(eNewError := E_HttpError.iMissingHost,sMessage := 'request host is empty');RETURN;END_IFsMethod := F_HttpMethodToString(eMethod := stRequest.eMethod);IF LEN(stRequest.sTarget) = 0 THENsTarget := GVL_Http.cnDefaultPath;ELSEsTarget := stRequest.sTarget;END_IFuiBodyLen := 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 THENsMessage := CONCAT(sMessage, 'close$R$N');ELSEsMessage := CONCAT(sMessage, 'keep-alive$R$N');END_IFIF LEN(stRequest.sAdditionalHeader) > 0 THENsMessage := CONCAT(sMessage, stRequest.sAdditionalHeader);sMessage := CONCAT(sMessage, '$R$N');END_IFIF LEN(stRequest.sContentType) > 0 THENsMessage := CONCAT(sMessage, 'Content-Type: ');sMessage := CONCAT(sMessage, stRequest.sContentType);sMessage := CONCAT(sMessage, '$R$N');END_IFIF uiBodyLen > 0 THENsMessage := CONCAT(sMessage, 'Content-Length: ');sMessage := CONCAT(sMessage, sContentLen);sMessage := CONCAT(sMessage, '$R$N');END_IFsMessage := CONCAT(sMessage, '$R$N');IF uiBodyLen > 0 THENsMessage := CONCAT(sMessage, stRequest.sBody);END_IFuiCandidate := TO_UINT(LEN(sMessage));IF uiCandidate >= GVL_Http.cnMaxMessageSize THENM_SetError(eNewError := E_HttpError.iBufferTooSmall,sMessage := 'request message exceeds buffer');RETURN;END_IFudiMessageLength := 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 : BOOLVAR_IN_OUTstResponse : ST_HttpResponse; // 结构化协议或测试数据。END_VARVAR_OUTPUTsMessage : STRING(GVL_Http.cnMaxMessageSize); // 诊断或协议文本字段。END_VARVARsReason : 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 THENM_SetError(eNewError := E_HttpError.iInvalidArgument,sMessage := 'response status code is zero');RETURN;END_IFIF LEN(stResponse.sReason) > 0 THENsReason := stResponse.sReason;ELSEsReason := F_HttpStatusReason(uiStatusCode := stResponse.uiStatusCode);END_IFIF LEN(stResponse.sContentType) > 0 THENsContentType := stResponse.sContentType;ELSEsContentType := GVL_Http.cnDefaultContentType;END_IFuiBodyLen := 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 THENsMessage := CONCAT(sMessage, 'close$R$NContent-Type: ');ELSEsMessage := CONCAT(sMessage, 'keep-alive$R$NContent-Type: ');END_IFsMessage := CONCAT(sMessage, sContentType);sMessage := CONCAT(sMessage, '$R$NContent-Length: ');sMessage := CONCAT(sMessage, sContentLen);sMessage := CONCAT(sMessage, '$R$N');IF LEN(stResponse.sAdditionalHeader) > 0 THENsMessage := CONCAT(sMessage, stResponse.sAdditionalHeader);sMessage := CONCAT(sMessage, '$R$N');END_IFsMessage := CONCAT(sMessage, '$R$N');IF uiBodyLen > 0 THENsMessage := CONCAT(sMessage, stResponse.sBody);END_IFuiCandidate := TO_UINT(LEN(sMessage));IF uiCandidate >= GVL_Http.cnMaxMessageSize THENM_SetError(eNewError := E_HttpError.iBufferTooSmall,sMessage := 'response message exceeds buffer');RETURN;END_IFudiMessageLength := 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_SetErrorVAR_INPUTeNewError : 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_INPUTeMethod : E_HttpMethod := E_HttpMethod.iUnknown; // 枚举状态或错误码。END_VAR// === IMPLEMENTATION ===/// =======================================================================/// 名称 : F_HttpMethodToString/// 功能 : 将 E_HttpMethod 转为 HTTP start-line 方法文本。/// 库依赖 : 暂无/// =======================================================================/// 使用说明 : 1. 未知方法返回 GET,保证出站请求可控。/// : 2. 入站原始方法仍由 ST_HttpRequest.sMethod 保留。/// =======================================================================// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。CASE eMethod OFE_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';ELSEF_HttpMethodToString := 'GET';END_CASE
完整源码 3:F_HttpStatusReason.st
职责:状态码原因短语
/// 功能 : HTTP 状态码默认原因短语。/// 库依赖 : 暂无{attribute 'hide_all_locals'}FUNCTION F_HttpStatusReason : STRING(64)VAR_INPUTuiStatusCode : UINT := 200; // 计数、长度或状态数值。END_VAR// === IMPLEMENTATION ===/// =======================================================================/// 名称 : F_HttpStatusReason/// 功能 : 提供首版常用 HTTP 状态码原因短语。/// 库依赖 : 暂无/// =======================================================================/// 使用说明 : 1. Builder 在 sReason 为空时使用本函数填充。/// : 2. 未覆盖状态码返回 OK 以外不会用于错误判定,仅影响可读文本。/// =======================================================================// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 边界说明:执行前后保持输出和错误码可被在线诊断追踪。CASE uiStatusCode OF200: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';ELSEF_HttpStatusReason := 'OK';END_CASE
本篇阅读抓手
Host 是请求构造硬门槛。 出站统一显式 Content-Length。 消息超限时拒绝发送。
如何验证这组源码
先针对 FB_HttpMessageBuilder.st的公开输入和错误出口建立确定性用例。再把“Host 是请求构造硬门槛”转成至少一个正常场景和一个失败场景。 本篇 3 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。 真机复核时重点观察“消息超限时拒绝发送”,并保留对应状态、计数和原始报文。
这一篇你最该记住
Host 是请求构造硬门槛。 出站统一显式 Content-Length。 消息超限时拒绝发送。
系列导航
系列:CodeSys HTTP 系列教程,第 21/28 篇。 当前源码加更:第 3/8 篇。 本篇完整源码文件数:3。 上一篇:第20篇 下一篇:第22篇