ControlRookie
返回文章

第21篇_源码加更 03|Builder、方法转换和状态码文本

构造器把结构体变成对端可识别的 HTTP/1.1 报文。方法文本、原因短语、长度和连接策略在这里统一收口。

构造器把结构体变成对端可识别的 HTTP/1.1 报文。方法文本、原因短语、长度和连接策略在这里统一收口。

适合谁收藏

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

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

先给结论

构造器把结构体变成对端可识别的 HTTP/1.1 报文。方法文本、原因短语、长度和连接策略在这里统一收口。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

源码加更 03|Builder、方法转换和状态码文本
源码加更 03|Builder、方法转换和状态码文本

读图重点

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

先看文件职责

序号文件职责行数
1FB_HttpMessageBuilder.st请求与响应构造器244
2F_HttpMethodToString.st方法枚举转文本35
3F_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

职责:请求与响应构造器

iecst
/// 功能    : 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

职责:方法枚举转文本

iecst
/// 功能    : 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

职责:状态码原因短语

iecst
/// 功能    : 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。
  • 消息超限时拒绝发送。

如何验证这组源码

  1. 先针对 FB_HttpMessageBuilder.st 的公开输入和错误出口建立确定性用例。
  2. 再把“Host 是请求构造硬门槛”转成至少一个正常场景和一个失败场景。
  3. 本篇 3 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
  4. 真机复核时重点观察“消息超限时拒绝发送”,并保留对应状态、计数和原始报文。

这一篇你最该记住

  • Host 是请求构造硬门槛。
  • 出站统一显式 Content-Length。
  • 消息超限时拒绝发送。

系列导航

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

评论区预留

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

↑ ↓