适合谁收藏
- 正在实现 PLC 端 HTTP Server 的工程师。
- 正在排查监听、多连接、请求边界或响应发送问题的人。
- 需要建立 Server 真机验收清单的读者。
本篇位置 服务器篇,第 1/7 篇;主系列第 05/28 篇。
现场问题
如果一开始就同时调试多连接、POST、chunked 和 keep-alive,任何失败都可能来自十几个位置。更稳妥的入口,是固定一个端口、一条 GET、一个短 Body 和短连接,把监听、接入、解析、路由、构造、发送和关闭全部走通。
/api/ping 不承担业务含义,它只回答一个问题:这套 PLC HTTP Server 能否完成一笔合法事务。
先给结论
最小 Server 验收不是端口可连接,而是 curl 收到 HTTP/1.1 200 OK、正确的 Content-Length 和 pong,同时 PLC 在线变量显示监听、接入、请求、响应和连接回收依次完成。

读图重点
这张图只压缩本篇的判断路径。读图时先找“Enable”对应的输入边界,再沿着“首轮采用短连接”检查状态怎样推进,最后用“响应后槽位可回收”确认输出是否已经形成验收证据。
把对象和边界分开
| Enable | 启动 Server 并绑定端口 | Listening 与句柄有效 |
| GET /api/ping | 最小无 Body 请求 | 目标路径解析正确 |
| 200 + pong | 最小合法响应 | 状态码、长度和 Body 一致 |
| Connection: close | 首轮采用短连接 | 响应后槽位可回收 |
从协议约束到代码职责
协议约束
最小 Server 验收不是端口可连接,而是 curl 收到 HTTP/1.1 200 OK、正确的 Content-Length 和 pong,同时 PLC 在线变量显示监听、接入、请求、响应和连接回收依次完成。 这条结论先限定消息什么时候成立,再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务,半包、超时、重复执行和连接残留就会进入应用层。
工程抽象
最小闭环故意减少变量:不带请求 Body,不复用连接,不依赖 JSON,也不接入真实设备动作。它先证明协议栈的骨架成立,再逐步增加连接槽、消息边界和业务路由。
外部结果与 PLC 证据必须对应同一笔请求。只看 curl 结果无法发现槽位泄漏,只看在线变量也无法证明对端收到了合法响应。
- Enable:工程职责是“启动 Server 并绑定端口”。它不能只停留在命名层面,运行时必须能通过“Listening 与句柄有效”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- GET /api/ping:工程职责是“最小无 Body 请求”。它不能只停留在命名层面,运行时必须能通过“目标路径解析正确”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- 200 + pong:工程职责是“最小合法响应”。它不能只停留在命名层面,运行时必须能通过“状态码、长度和 Body 一致”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
- Connection: close:工程职责是“首轮采用短连接”。它不能只停留在命名层面,运行时必须能通过“响应后槽位可回收”观察到输入、状态或结果;否则这一层即使有代码,也没有形成可验证边界。
程序单元
本篇主证据来自 PLC_PRG.st 中以 fbHttpServer( 为定位点的连续源码。这里不是为了展示语法,而是把协议约束落到确定程序单元:输入先进入结构体或缓冲区,状态机只在本周期处理可确认的部分,长度和结束条件决定能否前进,错误码与指标负责把失败原因带出对象边界。这样一来,“先跑通一笔最小事务,再增加复杂度。”可以在代码、在线变量和外部报文之间逐项对照,而不是依赖经验猜测。
本篇核心源码片段
下面两段代码来自同一个真实文件 PLC_PRG.st,以 fbHttpServer( 为中心连续截取,没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件,第二段用于确认状态、边界和输出。核对时重点看“启动 Server 并绑定端口”怎样进入对象,以及“响应后槽位可回收”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“外部证据与 PLC 在线证据缺一不可。”,就不能把局部代码截图当成实现证据。
片段一:入口、声明与前置条件
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,
bError => GVL_HttpRealTest.bServerError,
diErrorID => GVL_HttpRealTest.diServerErrorID,
sDiagMsg => GVL_HttpRealTest.sServerDiagMsg,
eLastNbsError => GVL_HttpRealTest.eServerLastNbsError,
这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件,不能只看某个布尔量是否变成 TRUE。
片段二:状态推进、边界与输出
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,
第二段继续展示同一连续源码范围。把它与第一段合起来,才能判断输入怎样被锁存、状态何时推进、边界何时满足,以及错误出口是否保留了足够诊断信息。
验证路径
| 端口监听 | 启动 Server | Listening、Running 和句柄一致 |
| 合法请求 | curl GET /api/ping | 返回 200、长度 4、Body 为 pong |
| 错误路径 | 请求 /api/missing | 返回 404 而不是静默断开 |
| 连续执行 | 重复请求 20 次 | 槽位可回收,计数持续递增 |
场景 1:端口监听
启用 Server 后先不发送请求,只观察 Listening、Running 和监听句柄。三者必须同时成立,通信工具连接目标端口也不能被拒绝。若布尔状态显示运行但句柄无效,应先查 NBS 初始化和端口占用,不能继续把问题归到 HTTP 路由。
场景 2:合法请求
用 curl 发送 GET /api/ping HTTP/1.1,同时记录原始响应和 PLC 在线变量。通过条件不是“浏览器显示了内容”,而是状态行等于 200 OK、Content-Length 等于 4、Body 逐字等于 pong,并且 Server 的请求计数和最近目标同步更新。
场景 3:错误路径
把路径改为 /api/missing。连接仍应正常完成一次 HTTP 事务,但业务结果必须是 404,而不是空响应、TCP 复位或继续返回 pong。这样才能证明路由失败属于应用层结果,不会被错误包装成底层通信故障。
场景 4:连续执行
连续发送 20 次 /api/ping,每次都校验状态码、长度和 Body。结束后活动连接数应回到预期值,请求计数累计增加,连接槽可以再次接入。若前几次正常、随后只能重启 PLC 恢复,优先检查 Close、槽位释放和完成脉冲复位。
常见误判
- 端口能连接就宣布最小 Server 已经跑通。
- 只看 curl 的 Body,不核对状态行、Content-Length 和连接回收。
- 最小验证同时引入 JSON、POST、keep-alive 和真实设备动作,失败后无法分层。
这些误判的共同点,是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标,并用相同输入完成回归。
这一篇你最该记住
- 先跑通一笔最小事务,再增加复杂度。
- 200、Header、Body 和关闭策略必须一起验证。
- 外部证据与 PLC 在线证据缺一不可。
系列导航
- 系列:CodeSys HTTP 系列教程,第 05/28 篇。
- 阶段:服务器篇,职责线位置 1/7。
- 上一篇:第04篇
- 下一篇:第06篇
- 发布顺序:基础认知 -> Server -> Client -> 完整源码加更 -> 综合收束。
网硕互联帮助中心





评论前必须登录!
注册