Workerman 不能直接作 PLC 驱动,因其为纯 PHP 非阻塞事件框架,而主流 PLC 协议(如 Modbus TCP)的原生 PHP 实现默认同步阻塞,会卡住事件循环,导致服务中断、延迟飙升或 CPU 100% 空转。

Workerman 本身不提供 PLC 协议支持,不能直接当 PLC 驱动用;它只负责网络事件调度。要读写 PLC,必须在 Workerman 的 onMessage 或定时任务中,调用真正能跟 PLC 通信的底层库(如 Modbus TCP 客户端),且必须规避阻塞操作。
为什么不能直接用 Workerman 做 PLC 驱动?
Workerman 是纯 PHP 的事件驱动框架,所有 I/O 操作需非阻塞。但绝大多数 PLC 通信协议(如 Modbus TCP、S7Comm、MC Protocol)的原生 PHP 实现默认是同步阻塞的——比如 fsockopen + fread 会卡住事件循环,导致整个 Worker 进程无法响应其他连接。
常见错误现象包括:
- PLC 读取一卡,所有 WebSocket 客户端断连或延迟飙升
-
onMessage回调执行完后,后续消息堆积、超时 - Worker 进程 CPU 100% 却无实际吞吐,实为阻塞等待 socket 响应
推荐方案:用异步 Modbus 库 + Workerman 定时轮询
目前最可行的是基于 pymodbus(Python)或 swoole_coroutine(PHP+Swoole)做异步 Modbus,但若坚持纯 Workerman(无 Swoole),只能退而求其次:用子进程或独立服务解耦。
更务实的做法是——把 PLC 通信抽成独立 CLI 脚本,由 Workerman 通过 proc_open 或消息队列触发:
- 启动一个常驻的
php plc-poller.php,使用stream_socket_client+stream_set_timeout控制阻塞时间,配合usleep做轻量轮询 - Workerman 的
onWorkerStart中用proc_open启动该脚本,并通过管道或 Redis pub/sub 交换数据 - 避免在
onMessage中直接调用 socket 操作;所有 PLC 请求统一走内部队列,由专用 Worker 处理
示例关键逻辑(非完整代码):
// 在 onWorkerStart 中启动轮询子进程
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['file', '/dev/null', 'a']
];
$process = proc_open('php /path/to/plc-poller.php', $descriptors, $pipes);
if (is_resource($process)) {
stream_set_blocking($pipes[1], false); // 管道读非阻塞
}
Modbus TCP 请求构造容易踩的坑
即使绕过 Workerman 直接发 TCP 包,Modbus TCP 协议字段也极易出错,导致 PLC 返回异常响应或静默丢包:
-
Transaction ID必须每帧递增,重复会导致 PLC 拒绝响应(尤其三菱/欧姆龙) -
Protocol ID固定为\x00\x00,写成\x00\x01会被某些 PLC 当作非法协议丢弃 - 寄存器地址从 0 开始还是从 1 开始?不同品牌差异大:
40001在西门子是 DB1.DBD0,在 Modbus 协议里对应地址0,但 pymodbus 默认按 1-based 解析,需显式设address=0 - 字节序(Endianness)未对齐:
INT类型在 AB PLC 中是高位在前(AB CD),而多数 PHPunpack('n', $data)默认低位在前,结果翻倍错乱
真正稳定的 PLC 驱动不在 Workerman 内部实现,而在隔离层:要么用 C 扩展封装工业协议栈,要么用 Python/Golang 写专用采集服务暴露 HTTP 接口供 Workerman 调用。硬塞进 Workerman 的 onMessage 里,迟早因一次超时拖垮整个实时通道。


















