webman 不能直接当 iot 数据网关用,需通过外部进程桥接 mqtt/modbus/grpc;设备不可直连 http/websocket 路由,应走“设备→mqtt broker→webman 消费进程→redis→websocket”链路,并规范串口采集与连接管理。

Webman 不能直接当 IoT 数据网关用,它不监听 1883(MQTT)、不解析 Modbus RTU、也不原生支持 gRPC 流式通信——所有这些都得靠「外部进程 + 协议桥接」来补足。核心思路是:让它只做它最擅长的事——稳定分发、状态广播、API 管理和 Redis/MySQL 协调。
为什么不能把设备直连 Webman 的 HTTP 或 WebSocket 路由
常见错误现象包括:设备上报后前端状态延迟 5–30 秒才更新、同一设备在多个页面显示不同在线状态、Redis 缓存里出现大量过期或重复的 device:1001:status 键。
- HTTP 路由是同步阻塞的,每秒上百台设备轮询会迅速吃满 worker 进程,
onRequest里做 CRC 校验或 JSON 解析会拖垮整个事件循环 - WebSocket 连接没有内置心跳和重连管理,设备网络抖动后连接静默断开,但
$connection->isConnected()仍返回true,导致“假在线” - Webman 多进程模型下,
$_SESSION和全局变量无法跨 worker 共享,你在一个 worker 里更新了设备温度,另一个 worker 查不到
用 Process + MQTT Broker 构建可靠数据通道
真实链路必须是:设备 → MQTT Broker(如 EMQX)→ Webman 消费进程 → WebSocket 广播给前端。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
-
MqttSubscriber进程需在app/process/MqttSubscriber.php中实现,使用Workerman\Mqtt\Client订阅/device/+/status主题 - 收到消息后,不要直接
send()给 WebSocket 连接,而是先写入 Redis:SET device:1001:status "{...}" EX 60,再通过PUBLISH device:1001:update "{...}" - WebSocket 服务(如
app/process/WebSocket.php)用Redis::subscribe()监听device:*:update,确保多 worker 下状态变更能广播到所有相关连接 - 避免在
onMessage回调里做耗时操作(如查 MySQL),全部转成协程异步或丢进Task进程处理
TCP 自定义协议接入串口/485 设备的实操要点
若设备走 RS485(如电表、温控器),必须用 Webman 的 Process 启动独立串口采集进程,不能塞进 HTTP 路由。
- 串口路径必须用绝对路径,如
/dev/ttyUSB0,且运行用户(如www-data)要加入dialout用户组:sudo usermod -a -G dialout www-data - 在
onWorkerStart中封装带重试的初始化:fopen('/dev/ttyUSB0', 'rb+') !== false,失败就exit(1)触发 Workerman 自动重启 - 帧结构必须含长度域和 CRC 校验,解码逻辑放在
app/Protocol/ModbusProtocol.php,用unpack('n', $buffer)读大端寄存器地址,校验失败直接丢弃整包 - 每次读取后调用
stream_set_timeout($fp, 0, 50000)(50ms 超时),并usleep(10000)让出 CPU,防止死循环占满单核
WebSocket 连接管理最容易被忽略的三个细节
很多项目上线后出现“设备掉线不通知”“前端反复重连打崩服务”,问题几乎都出在这三处。
- 前端必须主动发
{"type":"ping"}(建议 25s 间隔),服务端onMessage里识别后立即回{"type":"pong","ts":1717811340},不能依赖浏览器自动心跳 - 服务端不要用定时器轮询
$connection->isConnected()来踢连接,应只信任onClose回调——这是唯一可靠的断连信号 - 每个设备 ID 必须绑定唯一
$connection实例,可通过$connection->device_id = '1001'手动挂载,避免用户开两个标签页导致状态覆盖










