适合谁收藏
- 正在实现 PLC 端 HTTP Server 的工程师。
- 正在排查监听、多连接、请求边界或响应发送问题的人。
- 需要建立 Server 真机验收清单的读者。
本篇位置服务器篇,第 2/7 篇;主系列第 06/28 篇。
现场问题
调试 PLC Server 时,经常先看到端口 8088 已经可以连接,于是开始在业务代码里查为什么没有路径和 Body。问题通常不在业务层,而在监听句柄之后还有一条没有走完的链。
NBS.TCP_Server负责监听,NBS.TCP_Connection负责接入。只有接入句柄绑定到一个 HTTP 连接槽位,并且该槽位持续执行 Read,协议层才可能获得完整请求。
先给结论
Server 的第一条验收线不是“端口能连”,而是“监听句柄有效、连接槽位激活、请求进入解析器、响应可以返回”。四个状态缺一个都不能宣布跑通。
读图重点
这张图只压缩本篇的判断路径。读图时先找“Disabled”对应的输入边界,再沿着“接入并服务所有槽位”检查状态怎样推进,最后用“聚合请求和错误”确认输出是否已经形成验收证据。
把对象和边界分开
| 对象或阶段 | 工程职责 | 现场观察点 |
|---|---|---|
| Disabled | 未使能或已复位 | 监听句柄必须为 0 |
| Init | 清理历史错误与句柄 | 准备进入 Listen |
| Listen | 周期调用 TCP_Server | 等待有效 hServer |
| Running | 接入并服务所有槽位 | 聚合请求和错误 |
从协议约束到代码职责
协议约束
Server 的第一条验收线不是“端口能连”,而是“监听句柄有效、连接槽位激活、请求进入解析器、响应可以返回”。四个状态缺一个都不能宣布跑通。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。
工程抽象
Server 外层状态机只管理监听和槽位调度。单连接收发交给FB_HttpServerConnection,这避免一个连接的半包或错误阻塞其他连接。
监听状态必须对外暴露xListening、bListening和hListenHandle。现场如果只给一个bError,就无法判断故障发生在监听、接入还是 HTTP 解析阶段。
- Disabled:工程职责是“未使能或已复位”。它不能只停留在命名层面,运行时必须能通过“监听句柄必须为 0”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- Init:工程职责是“清理历史错误与句柄”。它不能只停留在命名层面,运行时必须能通过“准备进入 Listen”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- Listen:工程职责是“周期调用 TCP_Server”。它不能只停留在命名层面,运行时必须能通过“等待有效 hServer”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- Running:工程职责是“接入并服务所有槽位”。它不能只停留在命名层面,运行时必须能通过“聚合请求和错误”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
程序单元
本篇主证据来自FB_HttpServer.st中以CASE eState OF为定位点的连续源码。这里不是为了展示语法,而是把协议约束落到确定程序单元:输入先进入结构体或缓冲区,状态机只在本周期处理可确认的部分,长度和结束条件决定能否前进,错误码与指标负责把失败原因带出对象边界。这样一来,“监听、接入和 HTTP 请求是三个不同事件。”可以在代码、在线变量和外部报文之间逐项对照,而不是依赖经验猜测。
本篇核心源码片段
下面两段代码来自同一个真实文件FB_HttpServer.st,以CASE eState OF为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看“未使能或已复位”怎样进入对象,以及“聚合请求和错误”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“端口检查必须继续追到请求和响应证据。”,就不能把局部代码截图当成实现证据。
片段一:入口、声明与前置条件
udiNowMs := udiNowMs ); aConnectionSnapshots[uiSlotIndex] := aConnections[uiSlotIndex].stConnection; END_FOR M_Reset(); RETURN; END_IF CASE eState OF E_HttpServerState.iDisabled: eState := E_HttpServerState.iInit; E_HttpServerState.iInit: hServer := 0; hListenHandle := 0; bError := FALSE; eLastError := E_HttpError.iNoError; sDiagMsg := ''; eState := E_HttpServerState.iListen; E_HttpServerState.iListen, E_HttpServerState.iRunning: fbServer( xEnable := TRUE, ipAddr := ipBindAddr, uiPort := uiPort, eError => eTcpError ); hServer := fbServer.hServer; hListenHandle := hServer; IF fbServer.xError THEN eLastNbsError := eTcpError; M_SetServerError( eError := E_HttpError.iTcpServerFailed, sMessage := 'TCP server listen failed' ); eState := E_HttpServerState.iFault; ELSIF hServer <> 0 THEN eState := E_HttpServerState.iRunning;这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件,不能只看某个布尔量是否变成 TRUE。
片段二:状态推进、边界与输出
M_AcceptNewConnection(); M_ServiceConnections(); END_IF E_HttpServerState.iFault: fbServer( xEnable := FALSE, ipAddr := ipBindAddr, uiPort := uiPort ); ELSE eState := E_HttpServerState.iFault; END_CASE M_UpdateServerOutputs(); // === METHOD M_AcceptNewConnection === /// ======================================================================= /// 名称 : M_AcceptNewConnection /// 功能 : 周期调用所有 NBS.TCP_Connection 接入槽位。 /// 说明 : 所有槽位每周期都调用,符合 NBS 电平触发语义。 /// ======================================================================= {attribute 'hide_all_locals'} METHOD PRIVATE M_AcceptNewConnection // === IMPLEMENTATION === // 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。 // 边界说明:执行前后保持输出和错误码可被在线诊断追踪。 bAcceptError := FALSE; FOR uiSlotIndex := 1 TO GVL_Http.cnMaxClientSlots DO aTcpAccept[uiSlotIndex]( xEnable := hServer <> 0, hServer := hServer ); IF aTcpAccept[uiSlotIndex].xActive THEN IF (NOT aConnections[uiSlotIndex].bActive) OR (aConnections[uiSlotIndex].stConnection.hConnection <> aTcpAccept[uiSlotIndex].hConnection) THEN // 原因:接入阶段只绑定 NBS 句柄,读写统一留给 M_ServiceConnections; // 避免新连接在同一扫描周期被 TCP_Read 调用两次,导致大 body 半包场景被误判为接收错误。 aConnections[uiSlotIndex].M_Attach(第二段继续展示同一连续源码范围。把它与第一段合起来,才能判断输入怎样被锁存、状态何时推进、边界何时满足,以及错误出口是否保留了足够诊断信息。
验证路径
| 场景 | 操作 | 通过口径 |
|---|---|---|
| 监听 | hListenHandle 非 0,状态为 Running | 证明端口由 PLC 持有 |
| 接入 | AcceptedConnections 递增 | 证明外部连接进入槽位 |
| 请求 | uiLastRequestSlot 和目标路径更新 | 证明 HTTP 请求完成 |
| 响应 | 外部工具获得合法响应 | 证明事务闭环 |
场景 1:监听
启动 Server 后检查hListenHandle非 0、状态为 Running,并用外部工具确认目标端口可以建立 TCP 连接。这一步只证明端口由 PLC 持有,不代表连接已经进入 HTTP 连接槽,更不代表请求已经解析完成。
场景 2:接入
保持连接但暂不发送完整请求,观察AcceptedConnections、活动连接数和槽位快照。计数递增且某个槽位进入接收态,才说明 Accept 结果已经交给连接实例。如果端口能连但这些量不变化,应查接入调度,不要先改 Parser。
场景 3:请求
发送一条完整请求后,uiLastRequestSlot应指向实际处理槽位,最近目标路径应与请求行一致,接收缓冲的完成边界应由 Parser 给出。只有这三项同时成立,才可以把请求交给路由;仅看到 Read 返回字节数还不算收到 HTTP 请求。
场景 4:响应
最后核对外部工具收到的状态行、Header 和 Body,并观察发送状态退出、连接按策略关闭或回到可复用态。外部响应正确但槽位一直占用,说明事务只完成了报文发送,没有完成资源回收;这种状态不能算 Server 跑通。
常见误判
- 看到 hListenHandle 非零就宣布 Server 已经收到 HTTP 请求。
- 监听失败、接入失败和协议解析失败只共用一个模糊错误码。
- 新连接接入后没有持续服务连接槽,句柄存在但请求永远不推进。
这些误判的共同点,是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标,并用相同输入完成回归。
这一篇你最该记住
- 监听、接入和 HTTP 请求是三个不同事件。
- Server 外层只做生命周期和调度。
- 端口检查必须继续追到请求和响应证据。
系列导航
- 系列:CodeSys HTTP 系列教程,第 06/28 篇。
- 阶段:服务器篇,职责线位置 2/7。
- 上一篇:第05篇
- 下一篇:第07篇
- 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。