webman不能直接用http路由接收设备数据,因其同步阻塞特性会拖垮事件循环;正确做法是用独立process进程处理串口/mqtt通信,通过redis等与主进程解耦。

Webman 本身不直接处理设备物理层通信(如串口、485、USB),它只负责 HTTP/MQTT 上层服务;高频采集必须绕过 PHP-FPM 的请求生命周期限制,用常驻进程 + 异步 I/O 才能扛住每秒数百设备的心跳与数据上报。
为什么不能直接用 Webman 的 HTTP 路由接收设备数据
HTTP 是无状态、有开销的协议:每次请求都要走完整的 TCP 握手、SSL 协商(若启用)、PHP 请求初始化、路由匹配、中间件执行、响应构造、连接关闭。在设备数超 50、上报频率 ≥10s/次时,fpm 模式会迅速打满 worker 进程,而 Webman 虽常驻,但若把串口读取、Modbus 解析、CRC 校验等逻辑塞进 onRequest 回调里,依然会阻塞事件循环 —— 因为这些操作全是同步阻塞的。
常见错误现象包括:
- 设备在线状态频繁抖动(
online/offline切换) - Redis 缓存写入延迟飙升,
LPUSH超时或丢包 -
workerman: status显示大量idle进程卡在sleep()或fread()
正确做法是把「设备通信」和「Web 服务」彻底拆开:Webman 只做 API 管理、设备配置下发、历史数据查询;真实采集交给独立的 Process 或 Timer 子进程,通过 Redis / Unix Socket / Message Queue 与 Webman 主进程通信。
如何用 Webman Process 实现稳定串口采集(Modbus RTU over RS485)
Webman 的 support\Process 是最轻量、最可控的采集入口。它启动后常驻内存,可独占一个串口资源,避免多进程抢锁问题。
关键实操建议:
- 串口设备路径必须用绝对路径,如
/dev/ttyS1或/dev/ttyUSB0,且 Webman 进程用户要有读写权限(sudo usermod -a -G dialout www-data) - 不要在
onWorkerStart里直接fopen(),应封装成带重试的初始化函数,失败时记录日志并exit(1)触发 Workerman 自动重启 - Modbus CRC 校验必须在采集进程内完成,别依赖前端或数据库校验 —— 错误帧传到 Redis 就污染了整个管道
- 使用
stream_set_timeout($fp, 0, 50000)设置 50ms 超时,防止某台设备断线拖垮全链路 - 每轮采集后主动
usleep(10000)(10ms),给事件循环留出调度时间,避免 CPU 占用 100%
示例片段(简化):
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
class ModbusCollector extends Process
{
protected function onWorkerStart()
{
$fp = @fopen('/dev/ttyUSB0', 'rb+');
if (!$fp) { Log::error('Failed to open serial port'); exit(1); }
stream_set_timeout($fp, 0, 50000);
while (true) {
$frame = $this->readModbusFrame($fp);
if ($frame && $this->validateCrc($frame)) {
Redis::lPush('modbus:raw', json_encode(['ts' => time(), 'frame' => bin2hex($frame)]));
}
usleep(10000);
}
}
}
MQTT 订阅与设备状态心跳如何不干扰 Webman 主事件循环
Webman 原生不内置 MQTT 客户端,需引入 workerman/mqtt 或 php-mqtt/client。但直接在 onWorkerStart 启动一个长连接 MQTT Client,容易因网络抖动、重连风暴、未处理的 onError 导致整个进程僵死。
更稳妥的做法是:让 MQTT 客户端运行在独立 Process 中,并用 Redis Pub/Sub 或 AtomicCounter 与主业务进程共享设备在线状态。
关键细节:
- MQTT
client_id必须唯一且带随机后缀,否则多个实例会踢掉彼此(Connection lost: Connection refused) -
keepalive设为 60 秒,但设备端心跳间隔必须 ≤20 秒,否则平台判定离线(规则:离线 = 最后消息时间 × 3) - 不要在
onMessage里执行 DB 写入或复杂计算,只做原子操作:Redis::setex("device:online:{$clientId}", 60, time()) - Webman 的控制器查设备状态时,应优先读
Redis::get("device:online:xxx"),而不是实时查数据库
这样即使 MQTT 进程崩溃,设备状态缓存还能维持 60 秒,给运维留出反应窗口。
高频场景下 Redis 缓存设计的三个硬约束
当设备达 200+、每 5 秒上报一次,原始数据量轻松破万条/分钟。此时 Redis 不是“可选组件”,而是整个系统的缓冲阀和流量削峰器。
必须遵守的约束:
- 所有设备原始帧统一进
LPUSH modbus:raw队列,**禁止用SET device:123:data逐个存** —— 高频SET会快速打满 Redis 连接数与内存带宽 - 消费端必须用
BRPOP modbus:raw 0阻塞式拉取,避免空轮询浪费 CPU - 缓存键名必须带 TTL,如
SETEX device:123:last_seen 180 {timestamp},180 秒对应 3 倍心跳周期,超时自动剔除离线设备
漏掉任意一条,都会在第 3 天凌晨 2 点左右出现 Redis 内存暴涨、OOM command not allowed when used memory > 'maxmemory' 报错,进而引发采集雪崩。










