性能实测
我们在实际项目里跑了这套方案,设备数约200台,每台每5秒上报一次温湿度数据。实测数据:
| 指标 | 数值 |
|---|---|
| MQTT消息处理延迟 | 小于5ms |
| WebSocket推送延迟 | 小于50ms |
| 内存占用 | 约80MB |
| CPU占用 | 单核10%以下 |
200台设备每秒约40条消息,Swoole处理起来很轻松。瓶颈不在PHP,而在EMQX的消息路由和前端图表渲染。
虎王科技在物联网平台开发中积累了大量Swoole实战经验,更多技术文章和开源项目可以访问我们的GitHub仓库(github.com/huwangkeji)和技术博客(heicat.com)。团队长期承接物联网平台后端开发、嵌入式固件定制和边缘计算方案设计,欢迎交流合作。
PHP Swoole实现物联网MQTT数据订阅与WebSocket实时推送
物联网项目中,设备端走MQTT协议上报数据到Broker,后端需要把数据实时推送到Web大屏。传统方案是前端轮询接口,延迟高、服务器压力大。本文分享我们在沧州虎王科技物联网平台中,用PHP Swoole搭建的MQTT订阅转WebSocket推送方案,一套PHP进程同时处理协议转换和实时推送。
技术选型:为什么是Swoole而不是Node.js
物联网平台后端语言选型经常在PHP和Node.js之间犹豫。我们团队之前用Node.js写过类似的推送中间件,后来切到Swoole,原因有三点:
| 对比项 | Node.js + ws | PHP + Swoole |
|---|---|---|
| 长连接性能 | 基于libuv事件循环 | 基于Reactor线程+Worker进程 |
| 与已有业务整合 | 需要独立服务 | 可复用PHP框架和ORM |
| 开发者成本 | 前端转后端有门槛 | PHP开发者直接上手 |
| 进程管理 | 需pm2守护 | Swoole自带进程管理 |
Swoole的Master-Worker进程模型天然适合这种场景:Reactor线程负责网络IO,Task Worker跑MQTT订阅,普通Worker处理WebSocket连接,各进程之间通过管道通信,互不阻塞。
架构设计
整体数据流向:
设备 -> MQTT Broker(EMQX) -> Swoole Task Worker(订阅) -> Table共享内存 -> WebSocket Worker(推送) -> 前端大屏
核心思路是把MQTT订阅和WebSocket推送放在同一个Swoole服务里。用Swoole Table做进程间数据共享,避免引入Redis增加复杂度。MQTT订阅放在Task Worker进程,收到数据后写入Table,WebSocket Worker定时读Table推给前端。
MQTT订阅端实现
先装依赖包,用bluerhinos/phpMQTT作为MQTT客户端:
bash
composer require bluerhinos/php-mqtt
pecl install swoole
MQTT订阅跑在一个独立的Task进程里,连接到EMQX Broker订阅设备数据Topic:
php
<?php
use Swoole\Table;
use Swoole\WebSocket\Server;
$ws = new Server("0.0.0.0", 9502);
$ws->set([
'worker_num' => 2,
'task_worker_num' => 1,
'heartbeat_idle_time' => 60,
'heartbeat_check_interval' => 30,
]);
// 创建共享内存表,存最新设备数据
$deviceTable = new Table(1024);
$deviceTable->column('data', Table::TYPE_STRING, 4096);
$deviceTable->column('timestamp', Table::TYPE_INT, 8);
$deviceTable->create();
$ws->on("WorkerStart", function ($server, $workerId) use ($deviceTable) {
if ($workerId === 0) {
$mqtt = new Bluerhinos\PhpMqtt\Client(
"192.168.1.100", 1883, "php-subscriber"
);
$mqtt->connect();
$mqtt->subscribe("device/+/telemetry", function ($topic, $msg) use ($deviceTable) {
preg_match('/device\/(.+)\/telemetry/', $topic, $m);
$deviceId = $m[1] ?? 'unknown';
$payload = json_decode($msg, true);
$payload['device_id'] = $deviceId;
$payload['timestamp'] = time();
$deviceTable->set($deviceId, [
'data' => json_encode($payload),
'timestamp' => time(),
]);
});
while (true) {
$mqtt->loop(1);
}
}
});
```
这段代码的关键点是`WorkerStart`回调里判断`$workerId`。Task Worker的ID从0开始,所以第一个Task进程专门跑MQTT订阅。MQTT消息进来后直接写Swoole Table,不走网络,延迟在微秒级。