第02篇_输入一个 URL 以后,一次 HTTP 事务经历了什么
!abstract 用
http://192.168.1.20:8088/api/status跟踪同一笔事务:URL 怎样拆成连接参数与消息参数,请求怎样到达 PLC,响应又怎样回到 Client。
适合谁收藏
- 第一次系统理解 HTTP 的 PLC 工程师。
- 已经会调用通信功能块,但分不清 TCP 连通与 HTTP 完整事务的人。
- 需要建立后续 Server、Client 学习坐标的读者。
!note 本篇位置 基础认知,第 2/4 篇;主系列第 02/28 篇。
现场问题
在浏览器或 curl 中输入一个 URL,看起来只有一次点击。程序内部却必须连续完成地址解析、TCP 连接、请求构造、字节发送、Server 接入、消息解析、资源处理、响应构造、响应接收和事务收尾。
任何一步没有完成,外部都可能只表现为"没有响应"。如果不知道事务停在哪一步,就只能在所有功能块之间来回猜。
先给结论
一次 HTTP 事务不是一次函数调用,而是一条有顺序、有输入输出、有结束条件的链。URL 同时提供 TCP 连接信息和 HTTP 消息信息;TCP 通道、HTTP 消息和 PLC 业务沿同一笔事务推进,但必须分层观察。

读图重点
这张图只压缩本篇的判断路径。读图时先找"解析 URL"对应的输入边界,再沿着"路由、状态码、Body"检查状态怎样推进,最后用"业务结果回到 HTTP 响应"确认输出是否已经形成验收证据。
把对象和边界分开
| 对象或阶段 | 工程职责 | 现场观察点 |
|---|---|---|
| 解析 URL | scheme、host、port、path | 连接参数和请求参数被分开 |
| 建立通道 | Client Connect、Server Listen/Accept | 双方获得 TCP 连接句柄 |
| 交换消息 | Builder、Write、Read、Parser | 请求和响应跨周期完成 |
| 处理资源 | 路由、状态码、Body | 业务结果回到 HTTP 响应 |
一笔事务的十个连续步骤
- Client 把 URL 拆成 scheme、host、port 和 path。
- Client 使用 host 与 port 建立 TCP 连接。
- Builder 用 method、path、Host 和连接策略构造请求。
- TCP_Write 把请求字节发送出去。
- Server 从监听器接入连接,并绑定独立槽位。
- 槽位跨扫描周期读取,Parser 等待完整请求。
- 应用根据目标路径读取或修改资源。
- Builder 构造带状态码、长度和 Body 的响应。
- Client 跨周期接收并解析完整响应。
- 双方关闭连接,或清理事务后顺序复用连接。
这十步不是理论上的"可能过程",而是排障坐标。无法连接时查第 2 步;已经连接但没有请求时查第 3 至 6 步;返回 400 时查第 6 步;返回 404 时查第 7 步;Client 一直 Receive 时查第 8 至 9 步;第二次请求才出错时查第 10 步。
三条证据线
| 证据线 | 开始 | 结束 | 典型观察量 |
|---|---|---|---|
| 控制流 | Enable、Execute、新连接 | Done、Closing、Fault | 状态枚举、超时、复位 |
| 数据流 | URL、请求参数、连接句柄 | 状态码、Header、Body | Tx/Rx 长度、结构体字段 |
| 诊断流 | 底层或协议异常 | 错误锁存与恢复 | NBS error、HTTP error、Metrics |
三条线都闭合,才能宣布事务完成。只看到 Body 正确,仍可能遗漏连接泄漏;只看到 Done,也可能遗漏内容错误。
从协议约束到代码职责
协议约束
一次 HTTP 事务不是一次函数调用,而是一条有顺序、有输入输出、有结束条件的链。URL 同时提供 TCP 连接信息和 HTTP 消息信息;TCP 通道、HTTP 消息和 PLC 业务沿同一笔事务推进,但必须分层观察。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。
工程抽象
以该 URL 为例,192.168.1.20 和 8088 用于建立 TCP 连接,/api/status 进入 HTTP 请求行,主机与端口还要进入 Host Header。连接成功时,Server 只知道有一条字节通道,并不知道对方要读取哪个资源。
Builder 把结构化请求变成报文,Parser 把报文还原成结构化对象。应用层只消费完整请求,并把处理结果写入响应对象。这样每个故障都能定位到连接、构造、发送、解析、路由或响应,不必用一个 Busy 信号代表整条链。
- 解析 URL:工程职责是"scheme、host、port、path"。它不能只停留在命名层面,运行时必须能通过"连接参数和请求参数被分开"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- 建立通道:工程职责是"Client Connect、Server Listen/Accept"。它不能只停留在命名层面,运行时必须能通过"双方获得 TCP 连接句柄"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- 交换消息:工程职责是"Builder、Write、Read、Parser"。它不能只停留在命名层面,运行时必须能通过"请求和响应跨周期完成"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- 处理资源:工程职责是"路由、状态码、Body"。它不能只停留在命名层面,运行时必须能通过"业务结果回到 HTTP 响应"观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
程序单元
本篇主证据来自 PLC_PRG.st 中以 PROGRAM PLC_PRG 为定位点的连续源码。这里不是为了展示语法,而是把协议约束落到确定程序单元:输入先进入结构体或缓冲区,状态机只在本周期处理可确认的部分,长度和结束条件决定能否前进,错误码与指标负责把失败原因带出对象边界。这样一来,"一个 URL 会同时驱动 TCP 连接和 HTTP 请求。"可以在代码、在线变量和外部报文之间逐项对照,而不是依赖经验猜测。
本篇核心源码片段
下面两段代码来自同一个真实文件 PLC_PRG.st,以 PROGRAM PLC_PRG 为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看"scheme、host、port、path"怎样进入对象,以及"业务结果回到 HTTP 响应"怎样证明本次处理已经结束。若两段之间的连续关系无法解释"排障先定位步骤,再检查对应对象。",就不能把局部代码截图当成实现证据。
片段一:入口、声明与前置条件
iecst
PROGRAM PLC_PRG
VAR
fbHttpClient : FB_HttpClient; // HTTP Client 主实例。
fbHttpServer : FB_HttpServer; // HTTP Server 主实例。
stClientMetrics : ST_HttpClientMetrics; // Client 指标输出。
stServerMetrics : ST_HttpServerMetrics; // Server 指标输出。
stClientResponse : ST_HttpResponse; // Client 响应输出。
aServerSnapshots : ARRAY[1..GVL_Http.cnMaxClientSlots] OF ST_HttpConnectionSnapshot; // Server 连接快照。
END_VAR
// === IMPLEMENTATION ===
/// =======================================================================
/// 名称 : PLC_PRG
/// 功能 : HTTP 示例工程主任务,直接完整调用 Client 和 Server 功能块。
/// 库依赖 : CAA Net Base Services
/// =======================================================================
/// 使用说明 : 1. Client/Server 所有真实网络命令默认关闭,由 GVL_HttpRealTest 在线变量触发。
/// : 2. 本主程序完整列出功能块引脚,便于 CODESYS/SmartControl 用户按引脚理解用法。
/// =======================================================================
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
// 约束:PLC_PRG 作为示例主入口直接调用 HTTP 功能块;用户联调不再依赖辅助 PRG。
GVL_HttpRealTest.udiNowMs := GVL_HttpRealTest.udiNowMs + 10;
IF GVL_HttpRealTest.bResetResults THEN
fbHttpServer.M_Reset();
fbHttpClient.M_Reset();
GVL_HttpRealTest.bClientSend := FALSE;
GVL_HttpRealTest.bClientAbort := FALSE;
GVL_HttpRealTest.bServerPassLatched := FALSE;
GVL_HttpRealTest.bClientPassLatched := FALSE;
GVL_HttpRealTest.bOverallPassLatched := FALSE;
END_IF
fbHttpServer(
xEnable := GVL_HttpRealTest.bServerEnable,
xReset := GVL_HttpRealTest.bResetResults,
xCloseConnection := GVL_HttpRealTest.bServerCloseConnection,
bEnable := GVL_HttpRealTest.bServerEnable,
sBindIP := GVL_HttpRealTest.sServerBindIP,
uiPort := GVL_HttpRealTest.uiServerPort,
uiResponseStatusCode := GVL_HttpRealTest.uiServerResponseStatusCode,
sResponseBody := GVL_HttpRealTest.sServerResponseBody,
sResponseContentType := GVL_HttpRealTest.sServerResponseContentType,
sAdditionalHeader := GVL_HttpRealTest.sServerAdditionalHeader,
udiNowMs := GVL_HttpRealTest.udiNowMs,
xListening => GVL_HttpRealTest.bServerListening,
xRunning => GVL_HttpRealTest.bServerRunning,
xBusy => GVL_HttpRealTest.bServerBusy,
xError => GVL_HttpRealTest.bServerError,
bListening => GVL_HttpRealTest.bServerListening,
bRunning => GVL_HttpRealTest.bServerRunning,
bBusy => GVL_HttpRealTest.bServerBusy,
这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件,不能只看某个布尔量是否变成 TRUE。
片段二:状态推进、边界与输出
iecst
bError => GVL_HttpRealTest.bServerError,
diErrorID => GVL_HttpRealTest.diServerErrorID,
sDiagMsg => GVL_HttpRealTest.sServerDiagMsg,
eLastNbsError => GVL_HttpRealTest.eServerLastNbsError,
eState => GVL_HttpRealTest.eServerState,
eLastError => GVL_HttpRealTest.eServerLastError,
stMetrics => stServerMetrics,
uiActiveConnections => GVL_HttpRealTest.uiServerActiveConnections,
uiLastErrorSlot => GVL_HttpRealTest.uiServerLastErrorSlot,
uiLastRequestSlot => GVL_HttpRealTest.uiServerLastRequestSlot,
sRequestTarget => GVL_HttpRealTest.sServerLastTarget,
sRequestBody => GVL_HttpRealTest.sServerLastBody,
sRxMessage => GVL_HttpRealTest.sServerRxMessage,
sTxMessage => GVL_HttpRealTest.sServerTxMessage,
sLastTarget => GVL_HttpRealTest.sServerLastTarget,
sLastBody => GVL_HttpRealTest.sServerLastBody,
hListenHandle => GVL_HttpRealTest.hServerListenHandle,
aConnectionSnapshots => aServerSnapshots
);
fbHttpClient(
xEnable := GVL_HttpRealTest.bClientEnable,
xExecute := GVL_HttpRealTest.bClientSend,
xReset := GVL_HttpRealTest.bResetResults,
xAbort := GVL_HttpRealTest.bClientAbort,
xCloseConnection := GVL_HttpRealTest.bClientCloseConnection,
bEnable := GVL_HttpRealTest.bClientEnable,
bSend := GVL_HttpRealTest.bClientSend,
udiTimeOut := GVL_HttpRealTest.udiClientTimeoutUs,
sURL := GVL_HttpRealTest.sClientURL,
sServerIP := GVL_HttpRealTest.sClientServerIP,
uiPort := GVL_HttpRealTest.uiClientPort,
sHost := GVL_HttpRealTest.sClientHost,
sPath := GVL_HttpRealTest.sClientPath,
eRequestType := GVL_HttpRealTest.eClientMethod,
eMethod := GVL_HttpRealTest.eClientMethod,
sBody := GVL_HttpRealTest.sClientBody,
sContentType := GVL_HttpRealTest.sClientContentType,
sAdditionalHeader := GVL_HttpRealTest.sClientAdditionalHeader,
udiNowMs := GVL_HttpRealTest.udiNowMs,
xActive => GVL_HttpRealTest.bClientTcpConnected,
xConnected => GVL_HttpRealTest.bClientTcpConnected,
xBusy => GVL_HttpRealTest.bClientBusy,
xDone => GVL_HttpRealTest.bClientDone,
xError => GVL_HttpRealTest.bClientError,
bTcpConnected => GVL_HttpRealTest.bClientTcpConnected,
bBusy => GVL_HttpRealTest.bClientBusy,
bDone => GVL_HttpRealTest.bClientDone,
bError => GVL_HttpRealTest.bClientError,
diErrorID => GVL_HttpRealTest.diClientErrorID,
sDiagMsg => GVL_HttpRealTest.sClientDiagMsg,
eLastNbsError => GVL_HttpRealTest.eClientLastNbsError,
第二段继续展示同一连续源码范围。把它与第一段合起来,才能判断输入怎样被锁存、状态何时推进、边界何时满足,以及错误出口是否保留了足够诊断信息。
验证路径
| 场景 | 操作 | 通过口径 |
|---|---|---|
| URL 拆分 | 标出 scheme、host、port、path | 连接信息与消息信息不混用 |
| 请求跟踪 | 从 Connect 跟到 Parser Done | 每一步都有状态或数据证据 |
| 响应跟踪 | 从路由结果跟到 Client Done | 状态码、长度和 Body 一致 |
| 连接收尾 | 短连接或顺序复用 | 事务状态均被清理 |
场景 1:URL 拆分
执行"标出 scheme、host、port、path"前,先清理上一笔事务的完成脉冲、错误锁存和接收残留,再记录起始状态与计数。操作后同时观察外部报文、角色状态机和诊断量;只有三条证据共同指向"连接信息与消息信息不混用",这一场景才算通过。若只看到外部结果而内部状态未收口,应继续检查资源回收;若内部 Done 已出现而报文不完整,应回到消息边界重新取证。
场景 2:请求跟踪
执行"从 Connect 跟到 Parser Done"前,先清理上一笔事务的完成脉冲、错误锁存和接收残留,再记录起始状态与计数。操作后同时观察外部报文、角色状态机和诊断量;只有三条证据共同指向"每一步都有状态或数据证据",这一场景才算通过。若只看到外部结果而内部状态未收口,应继续检查资源回收;若内部 Done 已出现而报文不完整,应回到消息边界重新取证。
场景 3:响应跟踪
执行"从路由结果跟到 Client Done"前,先清理上一笔事务的完成脉冲、错误锁存和接收残留,再记录起始状态与计数。操作后同时观察外部报文、角色状态机和诊断量;只有三条证据共同指向"状态码、长度和 Body 一致",这一场景才算通过。若只看到外部结果而内部状态未收口,应继续检查资源回收;若内部 Done 已出现而报文不完整,应回到消息边界重新取证。
场景 4:连接收尾
执行"短连接或顺序复用"前,先清理上一笔事务的完成脉冲、错误锁存和接收残留,再记录起始状态与计数。操作后同时观察外部报文、角色状态机和诊断量;只有三条证据共同指向"事务状态均被清理",这一场景才算通过。若只看到外部结果而内部状态未收口,应继续检查资源回收;若内部 Done 已出现而报文不完整,应回到消息边界重新取证。
常见误判
- 把完整 URL 原样交给 TCP,混淆 Host、端口和请求路径的职责。
- 看到 Connect 成功就跳过请求构造、发送和响应接收。
- 只画数据流,不标出事务状态和结束条件,现场仍然无法定位卡点。
这些误判的共同点,是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标,并用相同输入完成回归。
这一篇你最该记住
- 一个 URL 会同时驱动 TCP 连接和 HTTP 请求。
- 完整事务必须走完请求与响应两个方向。
- 排障先定位步骤,再检查对应对象。
系列导航
- 系列:CodeSys HTTP 系列教程,第 02/28 篇。
- 阶段:基础认知,职责线位置 2/4。
- 上一篇:第01篇
- 下一篇:第03篇
- 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。