尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

第16篇_Client 05|状态行、Header 和响应 Body 怎样完整接收 适合谁收藏正在让 PLC 主动访问 HTTP 服务的工程师。需要处理请求构造、响应边界、超时与连接复用的人。希望把 Client 故障定位到确定状态和错误出口的读者。本篇位置客户端篇第 5/7 篇主系列第 16/28 篇。现场问题在线变量已经出现HTTP/1.1 200 OK业务却拿不到完整 Body。原因通常是程序在状态行出现后就提前完成忽略了后续字节。响应和请求一样可能跨多个 TCP_Read。Client 必须保留接收缓冲区在 Parser 明确 Done 之前继续累积。先给结论响应完成条件由 Parser 决定不由状态码文本决定。状态行合法、Header 完整、Body 边界满足之后Client 才能向应用输出状态码和 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 完成后的响应。”就不能把局部代码截图当成实现证据。片段一入口、声明与前置条件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 三种常见响应边界。 // 约束TECL 响应按协议错误处理避免 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。片段二状态推进、边界与输出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 等待完整 CRLF200 半 Body长度未满足不提前 Done204无内容响应状态码和空 Body 合法错误状态码404 或 500传输完成但业务结果保留场景 1分段状态行把状态行拆成两次 TCP_Read 送入 Parser。第一次只收到HTTP/1.1 20时bDone必须保持 FALSE缓冲区保留未完成字节第二次补齐0 OK和 CRLF 后才允许进入 Header 解析。验收时同时看接收长度、Parser 状态和原始报文避免把“读到部分状态行”误判为“已经收到响应”。场景 2200 半 Body构造Content-Length: 100、实际只到达 60 字节的响应。此时 Header 可以被接受但 Body 计数必须停在 60Parser 继续等待而不是发出完成脉冲。再补入剩余 40 字节检查完整 Body 与声明长度相等后才转入 Done这能直接区分“TCP 已有数据”和“HTTP 响应完整”。场景 3204让服务端返回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 - 完整源码加更 - 综合收束。
返回列表