
适合谁收藏正在实现 PLC 端 HTTP Server 的工程师。正在排查监听、多连接、请求边界或响应发送问题的人。需要建立 Server 真机验收清单的读者。本篇位置服务器篇第 3/7 篇主系列第 07/28 篇。现场问题单客户端测试很容易掩盖连接管理问题。第二个工具一接入常见现象是请求串到一起、旧句柄没有释放、某个错误让所有客户端一起掉线。当前实现使用 4 个固定槽位每个槽位拥有自己的 TCP_Read、TCP_Write、Parser、Builder、缓冲区和连接快照。固定上限不是保守而是让 PLC 的内存和扫描时间可计算。先给结论多客户端 Server 的最小可靠模型是一个监听器加固定数量的独立连接槽。每个槽位只服务一个句柄完成、错误和清理都在槽位边界内收口。读图重点这张图只压缩本篇的判断路径。读图时先找“接入”对应的输入边界再沿着“关闭或错误后释放句柄”检查状态怎样推进最后用“槽位回到可复用状态”确认输出是否已经形成验收证据。把对象和边界分开对象或阶段工程职责现场观察点接入新句柄绑定空闲槽位记录 accepted 计数服务每周期调用所有活跃槽位不因某槽位忙而跳过其他槽位聚合汇总最近请求、最近错误和快照保留槽位编号清理关闭或错误后释放句柄槽位回到可复用状态从协议约束到代码职责协议约束多客户端 Server 的最小可靠模型是一个监听器加固定数量的独立连接槽。每个槽位只服务一个句柄完成、错误和清理都在槽位边界内收口。 这条结论先限定消息什么时候成立再限定哪个角色可以消费结果。若绕过协议边界直接驱动业务半包、超时、重复执行和连接残留就会进入应用层。工程抽象M_AcceptNewConnection只负责把 NBS 句柄交给槽位真正的 Read/Write 留到统一服务阶段。源码中特别避免在同一扫描周期重复读取新连接否则大 Body 的半包场景可能被瞬时读错误误杀。连接快照记录槽位、状态、句柄、目标路径和计数。它把“偶发第二个客户端失败”从难以复现的问题变成可以按槽位复盘的证据。接入工程职责是“新句柄绑定空闲槽位”。它不能只停留在命名层面运行时必须能通过“记录 accepted 计数”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。服务工程职责是“每周期调用所有活跃槽位”。它不能只停留在命名层面运行时必须能通过“不因某槽位忙而跳过其他槽位”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。聚合工程职责是“汇总最近请求、最近错误和快照”。它不能只停留在命名层面运行时必须能通过“保留槽位编号”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。清理工程职责是“关闭或错误后释放句柄”。它不能只停留在命名层面运行时必须能通过“槽位回到可复用状态”观察到输入、状态或结果否则这一层即使有代码也没有形成可验证边界。程序单元本篇主证据来自FB_HttpServer.st中以METHOD PRIVATE M_AcceptNewConnection为定位点的连续源码。这里不是为了展示语法而是把协议约束落到确定程序单元输入先进入结构体或缓冲区状态机只在本周期处理可确认的部分长度和结束条件决定能否前进错误码与指标负责把失败原因带出对象边界。这样一来“一个端口不等于一个连接。”可以在代码、在线变量和外部报文之间逐项对照而不是依赖经验猜测。本篇核心源码片段下面两段代码来自同一个真实文件FB_HttpServer.st以METHOD PRIVATE M_AcceptNewConnection为中心连续截取没有改写变量、删除分支或用伪代码替代。第一段用于确认入口与前置条件第二段用于确认状态、边界和输出。核对时重点看“新句柄绑定空闲槽位”怎样进入对象以及“槽位回到可复用状态”怎样证明本次处理已经结束。若两段之间的连续关系无法解释“连接错误必须限制在单槽位内。”就不能把局部代码截图当成实现证据。片段一入口、声明与前置条件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( hConnection : aTcpAccept[uiSlotIndex].hConnection ); stMetrics.udiAcceptedConnections : stMetrics.udiAcceptedConnections 1; END_IF END_IF IF aTcpAccept[uiSlotIndex].xError THEN bAcceptError : TRUE; eTcpError : aTcpAccept[uiSlotIndex].eError; uiLastErrorSlot : uiSlotIndex; END_IF END_FOR IF bAcceptError THEN eLastNbsError : eTcpError; M_SetServerError( eError : E_HttpError.iTcpServerFailed, sMessage : TCP accept failed ); END_IF // METHOD M_ServiceConnections /// /// 名称 : M_ServiceConnections /// 功能 : 周期服务所有 HTTP 连接槽位。这一段先回答对象在什么输入和状态下开始工作。阅读时要核对变量的初值、长度上限和启动条件不能只看某个布尔量是否变成 TRUE。片段二状态推进、边界与输出/// 说明 : 请求、响应、错误和清理均由连接 FB 输出聚合。 /// {attribute hide_all_locals} METHOD PRIVATE M_ServiceConnections // IMPLEMENTATION // 工程说明本段集中处理状态、边界或诊断避免跨周期残留。 // 边界说明执行前后保持输出和错误码可被在线诊断追踪。 // 原因每个连接槽都必须每周期服务NBS 句柄状态变化可能发生在任意扫描周期。 // 诊断最近请求槽、最近错误槽和连接快照用于外部探针复盘多连接真实通信路径。 uiActiveCount : 0; FOR uiSlotIndex : 1 TO GVL_Http.cnMaxClientSlots DO aConnections[uiSlotIndex]( bEnable : aConnections[uiSlotIndex].bActive, xCloseConnection : xCloseConnection, uiSlot : uiSlotIndex, hConnectionIn : aConnections[uiSlotIndex].stConnection.hConnection, bConnectionActiveIn : aConnections[uiSlotIndex].bActive, uiResponseStatusCode: uiResponseStatusCode, sResponseBody : sResponseBody, sResponseContentType: sResponseContentType, sAdditionalHeader : sAdditionalHeader, udiNowMs : udiNowMs ); IF aConnections[uiSlotIndex].bActive THEN uiActiveCount : uiActiveCount 1; END_IF IF aConnections[uiSlotIndex].bRequestReady THEN uiLastRequestSlot : uiSlotIndex; sLastTarget : aConnections[uiSlotIndex].stRequest.sTarget; sLastBody : aConnections[uiSlotIndex].stRequest.sBody; sRequestTarget : aConnections[uiSlotIndex].stRequest.sTarget; sRequestBody : aConnections[uiSlotIndex].stRequest.sBody; sRxMessage : aConnections[uiSlotIndex].sRxMessage; stMetrics.udiRequestCount : stMetrics.udiRequestCount 1; END_IF IF aConnections[uiSlotIndex].bResponseSent THEN sTxMessage : aConnections[uiSlotIndex].sTxMessage;第二段继续展示同一连续源码范围。把它与第一段合起来才能判断输入怎样被锁存、状态何时推进、边界何时满足以及错误出口是否保留了足够诊断信息。验证路径场景操作通过口径并发接入两个以上客户端分别获得槽位请求不串线独立错误一个客户端发送坏报文其他槽位继续服务主动断开客户端关闭连接槽位被清理并可再次接入资源上限超过槽位数量行为可解释且无内存漂移场景 1并发接入同时打开两个以上测试客户端让每个客户端发送带有不同路径或 Body 标识的请求。在线变量里要能看到不同槽位分别锁存自己的句柄、目标路径和收发计数外部响应也必须回到对应客户端。只要出现 A 客户端读到 B 的路径、某个槽位计数被另一个槽位覆盖或者所有连接共用同一份接收状态就说明多连接模型还没有成立。场景 2独立错误保留一个正常客户端持续访问/api/ping再让另一个客户端发送缺失 Header 结束符或非法请求行。坏连接所在槽位应进入明确错误和关闭流程正常槽位的请求计数、响应内容和状态推进不能被带偏。这个场景看的不是“有没有报错”而是错误有没有被限制在单个槽位内。场景 3主动断开让客户端完成一次请求后主动断开再观察对应槽位的句柄、Rx 长度、Tx 长度、最后错误和快照是否回到可复用状态。随后用新的客户端占用同一槽位并再次请求确认旧路径、旧 Body 和旧错误不会残留。这个验证能直接暴露“看似断开、实际槽位不可复用”的问题。场景 4资源上限用超过cnMaxClientSlots的客户端同时连接重点看第 N1 条连接如何被拒绝、等待或关闭。通过口径不是所有连接都成功而是已占用槽位保持稳定超限连接有可解释诊断计数器和快照不出现越界或漂移。PLC 里固定上限必须可观测不能靠偶然跑过去。常见误判多个客户端共用一份 Rx 缓冲区第二条连接到来后请求发生串线。只调用当前活跃槽位漏掉句柄刚发生变化的其他槽位。连接断开后不清理快照和句柄槽位看似空闲却无法再次接入。这些误判的共同点是拿一个局部现象替代完整事务。定位时必须回到本篇的输入、状态、边界和输出四个坐标并用相同输入完成回归。这一篇你最该记住一个端口不等于一个连接。固定槽位让资源和诊断都可控。连接错误必须限制在单槽位内。系列导航系列CodeSys HTTP 系列教程第 07/28 篇。阶段服务器篇职责线位置 3/7。上一篇第06篇下一篇第08篇发布顺序基础认知 - Server - Client - 完整源码加更 - 综合收束。