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

资讯详情

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

第14篇_Client 03|URI、Host 和请求头怎样构造

第14篇_Client 03|URI、Host 和请求头怎样构造 适合谁收藏正在让 PLC 主动访问 HTTP 服务的工程师。需要处理请求构造、响应边界、超时与连接复用的人。希望把 Client 故障定位到确定状态和错误出口的读者。本篇位置客户端篇第 3/7 篇主系列第 14/28 篇。现场问题用 IP 直接访问测试服务时请求可能正常换成域名或反向代理后服务端返回 400 或 404。此时 TCP 地址没有错问题在 Host 或请求目标。连接层需要 IP 和端口HTTP/1.1 请求行需要 path/queryHeader 需要 Host。把三者拼成一个字符串会让修改和诊断都变得困难。先给结论Client 先把输入归一到结构化请求再由 Builder 生成请求行和 Header。Host 为空时直接拒绝构造不发送一条已知不合法的 HTTP/1.1 请求。读图重点这张图只压缩本篇的判断路径。读图时先找“sServerIP”对应的输入边界再沿着“Host Header”检查状态怎样推进最后用“决定虚拟主机语义”确认输出是否已经形成验收证据。把对象和边界分开对象或阶段工程职责现场观察点sServerIPTCP 远端地址决定连接到哪台主机uiPortTCP 远端端口默认不等于协议语义sTarget/sPath请求目标与查询串进入请求行sHostHost Header决定虚拟主机语义从协议约束到代码职责协议约束Client 先把输入归一到结构化请求再由 Builder 生成请求行和 Header。Host 为空时直接拒绝构造不发送一条已知不合法的 HTTP/1.1 请求。 这条结论先限定消息什么时候成立再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务半包、超时、重复执行和连接残留就会进入应用层。工程抽象Builder 将方法、目标和HTTP/1.1组成 start-line并始终写入 Host 和 Connection。附加 Header 由调用者提供但不能包含最终空行避免破坏 Header/Body 分界。URL 输入可以为工程调用提供便利但内部仍应拆回连接参数和请求参数。对 PLC 来说显式字段比运行时做复杂 URI 推断更容易验证。sServerIP工程职责是“TCP 远端地址”。它不能只停留在命名层面运行时必须能通过“决定连接到哪台主机”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。uiPort工程职责是“TCP 远端端口”。它不能只停留在命名层面运行时必须能通过“默认不等于协议语义”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。sTarget/sPath工程职责是“请求目标与查询串”。它不能只停留在命名层面运行时必须能通过“进入请求行”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。sHost工程职责是“Host Header”。它不能只停留在命名层面运行时必须能通过“决定虚拟主机语义”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。程序单元本篇主证据来自FB_HttpMessageBuilder.st中以METHOD PUBLIC M_BuildRequest为定位点的连续源码。这里不是为了展示语法而是把协议约束落到确定程序单元输入先进入结构体或缓冲区状态机只在本周期处理可确认的部分长度和结束条件决定能否前进错误码与指标负责把失败原因带出对象边界。这样一来“IP 解决连接Host 解决 HTTP 主机语义。”可以在代码、在线变量和外部报文之间逐项对照而不是依赖经验猜测。本篇核心源码片段下面两段代码来自同一个真实文件FB_HttpMessageBuilder.st以METHOD PUBLIC M_BuildRequest为中心连续截取没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件第二段用于确认状态、边界和输出。核对时重点看“TCP 远端地址”怎样进入对象以及“决定虚拟主机语义”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“先构造结构体再生成报文。”就不能把局部代码截图当成实现证据。片段一入口、声明与前置条件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这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件不能只看某个布尔量是否变成 TRUE。片段二状态推进、边界与输出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 : ;第二段继续展示同一连续源码范围。把它与第一段合起来才能判断输入怎样被锁存、状态何时推进、边界何时满足以及错误出口是否保留了足够诊断信息。验证路径场景操作通过口径缺少 HostHost 置空构造阶段直接报错错误路径访问不存在的 target对端返回 404 而非连接失败查询参数path 带 query请求行保持完整附加 Header加入自定义字段格式和空行仍合法场景 1缺少 Host将 Host 留空后启动构造器检查它是否在发送前拒绝请求。正确结果是给出明确参数错误、发送长度为零并且 TCP 连接没有被无意义地建立。这个场景要证明 Host 是请求构造的必填边界而不是把缺失字段交给对端后再靠 400 猜测原因。场景 2错误路径把 URI 写成格式正确但服务端不存在的 target随后抓取实际请求行和 Host。预期连接和报文格式均成功对端才返回 404PLC 应完整保存该业务状态而不是把它归为网络异常。借此可验证 URI 的语义错误与 Host、端口或连接故障之间的诊断分层。场景 3查询参数使用带查询参数的路径例如/api/status?detail1并从通信猫或服务端日志逐字核对请求行。问号后的查询不能丢失、重复编码或被拼进 Host服务端拿到的路径应与 PLC 输入一致。这个用例专门覆盖字符串拼接最容易发生的边界错误而不是重复测试普通 GET。场景 4附加 Header加入一条自定义 Header再检查它是否恰好位于标准 Header 与空行之间。若字段缺少 CRLF、重复换行或把 Body 首字节吞进 Header 区服务端行为可能看似偶发。通过口径是抓包中的 Header 顺序、空行边界和 Body 起始位置都准确同时默认 Header 没有被覆盖或重复。常见误判把连接 IP、请求目标和 Host 当成同一个字符串换到反向代理后无法定位 404。Host 为空仍然发送请求把可在构造阶段发现的问题推给远端。附加 Header 自带最终空行导致 Builder 生成两个 Header/Body 分界。这些误判的共同点是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标并用相同输入完成回归。这一篇你最该记住IP 解决连接Host 解决 HTTP 主机语义。请求目标不是完整 URL 的机械复制。先构造结构体再生成报文。系列导航系列CodeSys HTTP 系列教程第 14/28 篇。阶段客户端篇职责线位置 3/7。上一篇第13篇下一篇第15篇发布顺序基础认知 - Server - Client - 完整源码加更 - 综合收束。
返回列表