适合谁收藏
- 正在实现 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 -> 完整源码加更 -> 综合收束。
网硕互联帮助中心




评论前必须登录!
注册