ControlRookie
返回文章

第16篇_Client 05|状态行、Header 和响应 Body 怎样完整接收

Client 收到第一段响应后,应先解析状态行和 Header,再根据边界字段判断 Body。状态码 200 也不能替代完整性检查。

Client 收到第一段响应后,应先解析状态行和 Header,再根据边界字段判断 Body。状态码 200 也不能替代完整性检查。

适合谁收藏

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

客户端篇,第 5/7 篇;主系列第 16/28 篇。

现场问题

在线变量已经出现 HTTP/1.1 200 OK,业务却拿不到完整 Body。原因通常是程序在状态行出现后就提前完成,忽略了后续字节。

响应和请求一样可能跨多个 TCP_Read。Client 必须保留接收缓冲区,在 Parser 明确 Done 之前继续累积。

先给结论

响应完成条件由 Parser 决定,不由状态码文本决定。状态行合法、Header 完整、Body 边界满足之后,Client 才能向应用输出状态码和 Body。

Client 05|状态行、Header 和响应 Body 怎样完整接收
Client 05|状态行、Header 和响应 Body 怎样完整接收

读图重点

这张图只压缩本篇的判断路径。读图时先找“状态行”对应的输入边界,再沿着“在无明确长度的受控场景中提供边界”检查状态怎样推进,最后用“不能与半包混淆”确认输出是否已经形成验收证据。

把对象和边界分开

对象或阶段工程职责现场观察点
状态行版本、状态码、原因短语非法时拒绝响应
HeaderContent-Type、长度、传输编码、连接决定后续读取
Body按长度或 chunked 解码超限时失败
关闭在无明确长度的受控场景中提供边界不能与半包混淆

从协议约束到代码职责

协议约束

响应完成条件由 Parser 决定,不由状态码文本决定。状态行合法、Header 完整、Body 边界满足之后,Client 才能向应用输出状态码和 Body。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。

工程抽象

ST_HttpResponse 同时保存原始 Header 和结构化字段。原始文本方便复盘,结构化字段供业务和状态机判断,两者职责不同。

状态码表示服务端处理结果,不表示传输一定完整。即使看到 200,如果 Body 声明 100 字节而实际只收到 60 字节,Client 仍应等待或超时。

  • 状态行:工程职责是“版本、状态码、原因短语”。它不能只停留在命名层面,运行时必须能通过“非法时拒绝响应”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Header:工程职责是“Content-Type、长度、传输编码、连接”。它不能只停留在命名层面,运行时必须能通过“决定后续读取”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • Body:工程职责是“按长度或 chunked 解码”。它不能只停留在命名层面,运行时必须能通过“超限时失败”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
  • 关闭:工程职责是“在无明确长度的受控场景中提供边界”。它不能只停留在命名层面,运行时必须能通过“不能与半包混淆”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。

程序单元

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

本篇核心源码片段

下面两段代码来自同一个真实文件 FB_HttpMessageParser.st,以 METHOD PUBLIC M_ParseResponse 为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看“版本、状态码、原因短语”怎样进入对象,以及“不能与半包混淆”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“应用只消费 Parser 完成后的响应。”,就不能把局部代码截图当成实现证据。

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

iecst
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,

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

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

iecst
        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,

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

验证路径

场景操作通过口径
分段状态行第一行跨两次读取Parser 等待完整 CRLF
200 + 半 Body长度未满足不提前 Done
204无内容响应状态码和空 Body 合法
错误状态码404 或 500传输完成但业务结果保留

场景 1:分段状态行

把状态行拆成两次 TCP_Read 送入 Parser。第一次只收到 HTTP/1.1 20 时,bDone 必须保持 FALSE,缓冲区保留未完成字节;第二次补齐 0 OK 和 CRLF 后,才允许进入 Header 解析。验收时同时看接收长度、Parser 状态和原始报文,避免把“读到部分状态行”误判为“已经收到响应”。

场景 2:200 + 半 Body

构造 Content-Length: 100、实际只到达 60 字节的响应。此时 Header 可以被接受,但 Body 计数必须停在 60,Parser 继续等待而不是发出完成脉冲。再补入剩余 40 字节,检查完整 Body 与声明长度相等后才转入 Done;这能直接区分“TCP 已有数据”和“HTTP 响应完整”。

场景 3:204

让服务端返回 204 No Content,并确认 Header 结束后没有 Body。这里的通过条件不是“Body 长度为零”这一项,而是状态码语义、Header 边界和 Parser 的结束路径彼此一致。随后立即发起下一笔请求,验证前一笔没有把空响应的残留状态带入新的事务。

场景 4:错误状态码

用带有 JSON 错误体的 404 或 500 响应验证两层结果:传输层应正常收完整条消息,业务层应保留状态码和错误正文供调用方判断。不要因为状态码不是 2xx 就把 Parser 直接置为通信失败,否则会丢掉服务端已经返回的诊断信息。

常见误判

  • 读到 200 OK 就提前完成,声明长度对应的 Body 仍留在 TCP 缓冲区。
  • 只保存结构化状态码,不保留原始 Header,现场无法复盘对端真实响应。
  • 把 404 或 500 当成通信失败;实际上事务可能完整,只是业务结果失败。

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

这一篇你最该记住

  • 看到 200 仍要检查消息完整性。
  • 原始 Header 和结构化字段都要保留。
  • 应用只消费 Parser 完成后的响应。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 16/28 篇。
  • 阶段:客户端篇,职责线位置 5/7。
  • 上一篇:第15篇
  • 下一篇:第17篇
  • 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。
评论和回复区

评论区预留

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

↑ ↓