第15篇_Client 04|GET、POST 和 Body 怎样保持一致

适合谁收藏

  • 正在让 PLC 主动访问 HTTP 服务的工程师。
  • 需要处理请求构造、响应边界、超时与连接复用的人。
  • 希望把 Client 故障定位到确定状态和错误出口的读者。

本篇位置 客户端篇,第 4/7 篇;主系列第 15/28 篇。

现场问题

把 GET 改成 POST 后,服务端可能仍返回 400。请求行看起来正确,但 Body 有内容却没有 Content-Length,或者 JSON Body 仍使用 text/plain。

PLC 端常把多个字符串输入直接交给拼接逻辑,最终很难判断是方法错、内容类型错还是长度错。

先给结论

方法只描述意图,Body 边界由长度决定,内容解释由 Content-Type 决定。三者必须在 ST_HttpRequest 中一致,再交给 Builder。

读图重点

这张图只压缩本篇的判断路径。读图时先找"GET"对应的输入边界,再沿着"当前枚举可表达"检查状态怎样推进,最后用"不能默认等于现场安全动作"确认输出是否已经形成验收证据。

把对象和边界分开

对象或阶段 工程职责 现场观察点
GET 通常无 Body,目标包含查询参数 读取资源或状态
POST 常带 Body 和 Content-Type 提交命令或数据
PUT 当前枚举可表达 是否使用取决于服务端契约
DELETE 当前枚举可表达 不能默认等于现场安全动作

用两条报文检查模型是否一致

下面两条报文的差别不止是 GET 和 POST。GET 把查询条件放在请求目标中,POST 则同时引入内容类型和 Body 长度。

http 复制代码
GET /api/status?line=1 HTTP/1.1
Host: 192.168.1.20:8088
Connection: keep-alive
http 复制代码
POST /api/echo HTTP/1.1
Host: 192.168.1.20:8088
Content-Type: application/json
Content-Length: 15
Connection: close

{"value":123}

检查顺序应当固定:方法是否符合服务端契约,请求目标是否完整,Host 是否存在,Content-Type 是否能解释 Body,Content-Length 是否等于实际传输长度。只改第一行,后四项不会自动正确。

从协议约束到代码职责

协议约束

方法只描述意图,Body 边界由长度决定,内容解释由 Content-Type 决定。三者必须在 ST_HttpRequest 中一致,再交给 Builder。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。

工程抽象

请求结构保存方法、目标、Host、Content-Type、Authorization、附加 Header、原始 Header、Body 和边界标志。它让上层业务不必理解报文文本,也让测试可以直接构造边界条件。

当前 Builder 只有 Body 非空时才写 Content-Length。文章必须明确这是当前实现行为,不把它扩大成所有 HTTP 客户端的唯一写法。

  • GET:工程职责是"通常无 Body,目标包含查询参数"。它不能只停留在命名层面,运行时必须能通过"读取资源或状态"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • POST:工程职责是"常带 Body 和 Content-Type"。它不能只停留在命名层面,运行时必须能通过"提交命令或数据"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • PUT:工程职责是"当前枚举可表达"。它不能只停留在命名层面,运行时必须能通过"是否使用取决于服务端契约"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • DELETE:工程职责是"当前枚举可表达"。它不能只停留在命名层面,运行时必须能通过"不能默认等于现场安全动作"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。

程序单元

本篇主证据来自 FB_HttpMessageBuilder.st 中以 METHOD PUBLIC M_BuildRequest 为定位点的连续源码。这里不是为了展示语法,而是把协议约束落到确定程序单元:输入先进入结构体或缓冲区,状态机只在本周期处理可确认的部分,长度和结束条件决定能否前进,错误码与指标负责把失败原因带出对象边界。这样一来,"方法、内容类型和 Body 是一个整体。"可以在代码、在线变量和外部报文之间逐项对照,而不是依赖经验猜测。

本篇核心源码片段

下面两段代码来自同一个真实文件 FB_HttpMessageBuilder.st,以 METHOD PUBLIC M_BuildRequest 为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看"通常无 Body,目标包含查询参数"怎样进入对象,以及"不能默认等于现场安全动作"怎样证明本次处理已经结束。若两段之间的连续关系无法解释"结构化请求比散落字符串更容易测试。",就不能把局部代码截图当成实现证据。

片段一:入口、声明与前置条件

iecst 复制代码
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

这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件,不能只看某个布尔量是否变成 TRUE。

片段二:状态推进、边界与输出

iecst 复制代码
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

第二段继续展示同一连续源码范围。把它与第一段合起来,才能判断输入怎样被锁存、状态何时推进、边界何时满足,以及错误出口是否保留了足够诊断信息。

验证路径

场景 操作 通过口径
GET 无 Body 访问查询路径 请求不携带错误长度
POST 文本 text/plain + Body 长度与字符串一致
POST JSON application/json 服务端按契约解析
空 Body POST 方法为 POST 但 Body 为空 行为由服务端契约验证

场景 1:GET 无 Body

对一个只读查询路径发起 GET,抓包检查请求行之后是否错误地附带了上一次 POST 的 Body 或 Content-Length。服务端应只按 URI 处理查询,PLC 侧发送长度也应反映空 Body。这个场景用于确认方法切换时缓冲区已经清空,避免旧数据在下一笔事务里悄悄泄漏。

场景 2:POST 文本

用 POST 发送一段 text/plain 文本,比较 PLC 字符串长度、Header 中的 Content-Length 与服务端实际收到的字节数。三者必须一致,且响应应能回显或确认正文内容。若把字符数和字节数混用,中文或特殊字符会让长度判断失真,因此此处要以真实报文而不是界面显示为准。

场景 3:POST JSON

改用 application/json 并发送结构合法的 JSON,确认 Content-Type、Body 内容和长度一起进入请求构造器。服务端应按 JSON 契约解析,而不是把它当普通文本;PLC 端则要保留响应状态与错误文本。此测试覆盖的是媒体类型与 Body 配套关系,不能用上一场的纯文本回显替代。

场景 4:空 Body POST

保持 POST 方法但传入空 Body,明确观察构造器输出的长度字段与服务端响应。不同接口可能接受空请求、拒绝空请求或给出默认行为,Client 不应擅自把它判成传输错误。验收重点是请求格式仍合法、业务状态原样保留,并且下一次带 Body 的 POST 不受这次空请求影响。

常见误判

  • 只修改方法枚举,不同步检查 Body、Content-Type 和 Content-Length。
  • 把 JSON 文本是否有效与 HTTP 传输是否完整混成同一个判断。
  • POST 响应超时后无条件重发,忽略服务端可能已经执行过动作。

这些误判的共同点,是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标,并用相同输入完成回归。

这一篇你最该记住

  • 方法、内容类型和 Body 是一个整体。
  • 长度描述传输边界,不描述业务语义。
  • 结构化请求比散落字符串更容易测试。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 15/28 篇。
  • 阶段:客户端篇,职责线位置 4/7。
  • 上一篇:第14篇
  • 下一篇:第16篇
  • 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。
相关推荐
张小姐的猫7 小时前
【Linux】网络编程 —— 传输层协议 TCP(下)
linux·运维·服务器·网络·tcp/ip·http·php
在世修行1 天前
3D轮廓仪智能定位激光打标上位机:系统整体架构设计深度解析
上位机·架构设计·plc通信·3d轮廓仪·激光打标
运维全栈笔记1 天前
致远 OA HTTPS 配置避坑指南
网络协议·http·https
A_humble_scholar1 天前
Linux 网络基础(三)下的 HTTP 核心详解:从请求响应到状态管
linux·网络·http
pcplayer2 天前
Delphi WebService 使用 HTTP Header 管理 Session
http·webservice·soap·web server·http header
FfHUCisI2 天前
Golang HTTP 路由设计与请求处理
开发语言·http·golang
小新讲网安2 天前
HTTP请求走私攻击实战:CL.TE与TE.CL绕过前端服务器全解析
服务器·前端·网络·web安全·http·架构·漏洞
wear工程师3 天前
接口超时别只调大 timeout:Java HTTP 客户端其实有 3 段等待
java·http
蜡台3 天前
本机 局域网 搭建本地 HTTPS 域名完整方案
网络协议·http·https·openssl