各位工友们,大家晚上好!
欢迎来到今天的“硬核修仙”大会。今天我们不谈架构图上的饼图和柱状图,也不谈那些在PPT里跳来跳去的K线图。今天,我们要聊的是稍微“粗糙”一点、稍微“工业”一点的东西——工业自动化报表系统。
很多人听到“PHP”,第一反应是:“哟,做博客的,写CRUD的,前端切图仔的。” 哎呀呀,这种偏见就像说“诺基亚只能打接电话”一样过时了。PHP这门语言,只要用对了姿势,它不仅能做高并发,还能当调度员,甚至能当那个拿着喇叭喊“开工”的监工。
今天,我们要利用全栈架构,干一件大事:从传感器里抠数据,把冷冰冰的电压电流变成热气腾腾的实时折线图。
准备好了吗?我们要开始给生产线“通灵”了。
第一部分:先别急着敲代码,先搞清楚你在跟谁打交道
在工业自动化领域,最可怕的不是程序跑不通,而是数据没跟上。
想象一下,你是一个温度传感器,你的工作是监测炼钢炉里的温度。你每毫秒产生一个数值,这个数值如果不被采集,炉子就会爆炸(或者生产出来的螺丝全是废品)。
我们的目标是构建一个系统:传感器 -> 数据管道 -> 报表系统 -> 大屏幕 -> CEO点头。
在这个系统里,PHP是大脑,WebSocket是神经,Redis是仓库,Chart.js是画笔。别觉得PHP不行,PHP就像瑞士军刀,虽然它没有手术刀那么专业,但在工业现场,它能把你那个螺丝钉精准地装上去。
第二部分:模拟传感器——让数据自己动起来
没有传感器?没关系,我们这里先来点“玄学编程”。我们写一个脚本来模拟传感器数据。在真实世界里,这是Python跑在树莓派上,或者C++跑在单片机上。但在我们的PHP演示里,我们要展示PHP的“并发模拟能力”。
我们要模拟正态分布的数据。为什么是正态分布?因为现实世界不是均匀分布的,它是“大部分正常,偶尔发疯”。
<?php
// sensor_simulation.php
// 这是一个伪装成传感器的PHP脚本,别告诉任何人
class Sensor {
private $baseTemp = 25; // 基础温度25度
private $fluctuation = 5; // 波动范围5度
// 模拟正态分布的随机数(数学不好别慌,这里用Box-Muller变换偷个懒)
private function getNormalRandom($mean, $stddev) {
$u1 = mt_rand(-100000, 100000) / 100000;
$u2 = mt_rand(-100000, 100000) / 100000;
$z0 = sqrt(-2.0 * log($u1)) * cos(2.0 * M_PI * $u2);
return $z0 * $stddev + $mean;
}
public function read() {
$currentTemp = $this->getNormalRandom($this->baseTemp, $this->fluctuation);
// 随机制造一个“故障”信号,模拟传感器失效
if (mt_rand(1, 1000) > 990) {
return [
'status' => 'error',
'value' => null,
'message' => '传感器连接丢失!'
];
}
return [
'status' => 'ok',
'value' => round($currentTemp, 2),
'timestamp' => time(),
'machine_id' => 'MACH-' . mt_rand(1, 10)
];
}
}
$sensor = new Sensor();
// 轮询输出数据,假装我们在不断地读取硬件
while (true) {
$data = $sensor->read();
echo json_encode($data) . "n";
// 模拟硬件采样周期:100毫秒
usleep(100000);
}
看,这段代码虽然简单,但它模拟了现实中的“噪音”和“断点”。接下来,我们的PHP后端要像捕食者一样,死死盯住这个输出,把数据抢过来,然后通过 WebSocket 传给前端。
第三部分:后端架构——为什么我们需要 WebSocket?
你说用 AJAX 轮询行不行?行,当然行。但是,这就像是你每隔10分钟去问女朋友“你爱我吗”,如果她回答“爱”,你很高兴;如果她不回答,你还在等。这种效率在工业现场是致命的。
我们需要的是WebSocket。它像是一条永远不挂断的电话线。
为了实现这个,我们要用 Ratchet,一个著名的 PHP WebSocket 库。当然,在生产环境,我会推荐用 Swoole,那个简直是加速器。但为了保持代码通俗易懂,我们用 Ratchet,它基于 Swoole 的思想,但不需要你安装复杂的扩展。
首先,安装 Ratchet:
composer require cboden/ratchet
然后,我们的“接线员”代码来了:
<?php
// server.php
require dirname(__FILE__) . '/vendor/autoload.php';
use RatchetMessageComponentInterface;
use RatchetConnectionInterface;
class MachineDataServer implements MessageComponentInterface {
protected $clients;
public function __construct() {
// 所有的客户端都住在这个房间里
$this->clients = new SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn) {
// 客户端上线了,给他分配个ID,顺便欢迎一下
$conn->resourceId = $conn->resourceId;
$this->clients->attach($conn);
echo "新客户端加入: {$conn->resourceId}n";
// 给新来的发个问候,告诉他现在几点了
$conn->send(json_encode(['type' => 'system', 'message' => '欢迎加入工业监控大屏!']));
}
public function onMessage(ConnectionInterface $from, $msg) {
// 客户端发来的消息通常心跳包或者指令,这里我们暂不处理
echo "收到客户端 {$from->resourceId} 的消息: $msgn";
}
public function onClose(ConnectionInterface $conn) {
// 客户端下线了,从房间里赶出去
$this->clients->detach($conn);
echo "客户端 {$conn->resourceId} 离开n";
}
public function onError(ConnectionInterface $conn, Exception $e) {
// 客户端挂了,或者协议搞错了,默默处理
$conn->close();
echo "发生错误: {$e->getMessage()}n";
}
// 核心功能:广播数据
public function broadcastData($data) {
// 遍历房间里的每一个人,把数据塞进他们手里
foreach ($this->clients as $client) {
// 防止发送失败导致整个服务崩溃(虽然Ratchet处理得不错,但保险起见)
if ($client->isConnected()) {
$client->send(json_encode($data));
}
}
}
}
// 启动服务器
$loop = ReactEventLoopFactory::create();
$websock = new RatchetServerIoServer(
new RatchetHttpHttpServer(
new RatchetWebSocketWsServer(
new MachineDataServer
)
),
8080 // 监听8080端口
);
echo "服务器启动中,监听端口 8080...n";
$websock->run();
这段代码的逻辑很简单:监听8080端口,谁连接进来,就告诉服务器“我来了”。然后,服务器每隔一点点时间(比如10毫秒),就把传感器读到的数据推送给所有连接的人。
这叫什么?这就叫“不问自取”。
第四部分:前端展示——让图表飞起来
现在,后端已经是一个忠实的信使了。接下来是前端。我们需要一个 HTML 页面,它能连接 WebSocket,然后画图。
很多人用 ECharts,它确实牛逼,但对于这种实时流,Chart.js 更轻量,更容易理解。我们来做个简单的仪表盘。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>我是AI,但我也是工厂监工</title>
<!-- 引入 Chart.js 和 Bootstrap 简单美化一下 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-dark text-white">
<div class="container mt-5">
<h1 class="text-center mb-4">🏭 工业大数据实时看板</h1>
<div class="row">
<div class="col-md-6">
<div class="card bg-secondary mb-3">
<div class="card-header">实时温度监控</div>
<div class="card-body">
<canvas id="tempChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-success mb-3">
<div class="card-header">系统状态</div>
<div class="card-body">
<p>连接状态: <span id="status" class="badge bg-danger">未连接</span></p>
<p>最新数值: <span id="lastValue" class="display-4">--</span> °C</p>
<p>采样率: 100ms</p>
</div>
</div>
</div>
</div>
</div>
<script>
// 1. 配置图表
const ctx = document.getElementById('tempChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [], // 时间轴
datasets: [{
label: '实时温度 (°C)',
data: [], // 数据轴
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.4, // 让曲线变平滑
fill: true
}]
},
options: {
scales: {
x: { display: false }, // 隐藏时间轴,看起来更像心跳图
y: { beginAtZero: false }
},
animation: false, // 关闭动画,提高性能
plugins: { legend: { display: false } }
}
});
// 2. WebSocket 连接
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = function() {
document.getElementById('status').innerText = "在线";
document.getElementById('status').className = "badge bg-success";
};
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
// 如果是系统消息
if (data.type === 'system') {
console.log(data.message);
return;
}
// 如果是传感器数据
if (data.status === 'ok') {
updateChart(data.value);
} else {
// 传感器报错
alert('警告:' + data.message);
}
};
function updateChart(newValue) {
const now = new Date().toLocaleTimeString();
// 添加数据点
chart.data.labels.push(now);
chart.data.datasets[0].data.push(newValue);
// 保持数据点数量不超过50个,防止内存溢出(如果真的连接了一万台机器,这里需要优化)
if (chart.data.labels.length > 50) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
// 更新 DOM 显示的数值
document.getElementById('lastValue').innerText = newValue;
chart.update();
}
// 错误处理
socket.onerror = function(error) {
console.error('WebSocket Error: ', error);
document.getElementById('status').innerText = "错误";
};
socket.onclose = function() {
document.getElementById('status').innerText = "断开";
document.getElementById('status').className = "badge bg-warning";
};
</script>
</body>
</html>
看到没?代码这么少,效果却很震撼。前端做的事情非常纯粹:连接 -> 收消息 -> 塞进图表 -> 刷新。没有任何繁琐的 DOM 操作,这就是现代前端框架(哪怕是原生JS)的魅力。
第五部分:数据处理——Redis 是你的避难所
好了,如果你直接运行上面的代码,可能会发现一个性能瓶颈:当有1000个传感器同时发送数据,而且有5000个客户端在同时监听,这个 PHP 进程可能会瞬间崩溃。
为什么?因为 PHP 是单线程的(除非你用了 Swoole),IO 操作(比如写数据库、写文件、发 WebSocket)在默认情况下是阻塞的。当所有人都排队等厕所的时候,厕所(CPU/内存)就堵死了。
这时候,我们需要引入 Redis。Redis 是个内存数据库,它的速度比 MySQL 快 100 倍,而且是单线程模型,天然不怕并发。
我们的新架构变成了:传感器 -> Redis List (队列) -> PHP Worker (监听队列) -> WebSocket -> 客户端。
修改一下我们的 Worker 代码,让它去抢 Redis 里的活:
<?php
// worker.php
require dirname(__FILE__) . '/vendor/autoload.php';
use RatchetMessageComponentInterface;
use RatchetConnectionInterface;
use RatchetServerIoServer;
use RatchetHttpHttpServer;
use RatchetWebSocketWsServer;
use ReactEventLoopFactory;
// 假设我们有一个叫 "sensor_data" 的 Redis 列表
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
class RedisWorker {
private $clients;
public function __construct() {
$this->clients = new SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
}
public function onMessage(ConnectionInterface $from, $msg) {
// 这里暂时留空,主要靠 Redis 驱动
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
}
public function onError(ConnectionInterface $conn, Exception $e) {
$conn->close();
}
public function broadcastFromRedis() {
// 这是一个死循环,专门负责去 Redis 里偷数据
while (true) {
// BLPOP 是阻塞读取,如果没有数据,线程就睡觉,不占 CPU
// 参数格式: key1, key2
$result = $redis->brPop('sensor_data_queue', 1);
if ($result) {
$data = json_decode($result[1], true);
// 推送给所有客户端
foreach ($this->clients as $client) {
if ($client->isConnected()) {
$client->send(json_encode($data));
}
}
}
}
}
}
// 启动 WebSocket 服务器
$loop = Factory::create();
$websock = new IoServer(
new HttpServer(
new WsServer(new RedisWorker)
),
8080
);
// 启动 Redis 监听线程
// 注意:在生产环境,这通常需要 Supervisor 守护进程
$worker = new RedisWorker();
$worker->broadcastFromRedis();
echo "启动 Redis 监听线程...n";
// 让这两个进程同时跑起来
$loop->run();
通过这个改动,PHP 就变成了一个“消费者”。它不负责产生数据(那是传感器的事),它只负责把数据从 Redis 拿出来,分发给前端。哪怕前端瞬间断线了,数据也只会丢在 Redis 里,不会丢失。
第六部分:数据库报表——历史数据的存档
WebSocket 解决了“实时”的问题,但老板更在乎“历史”。老板说:“上周二那台机器怎么罢工了?给我调出来看看!”
这时候,我们就需要 MySQL 了。虽然写 MySQL 很枯燥,但它是唯一的真相之源。
我们需要一个脚本,每隔一段时间(比如5分钟),把 Redis 队列里的数据清空,存入 MySQL。
<?php
// reporter.php
// 这个脚本由系统定时任务(Crontab)每分钟执行一次
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$conn = new mysqli('localhost', 'root', 'password', 'factory_db');
// 创建表(如果不存在)
$conn->query("CREATE TABLE IF NOT EXISTS temperature_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
machine_id VARCHAR(50),
value DECIMAL(10, 2),
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// 从 Redis 队列取数据并写入 MySQL
while ($result = $redis->brPop('sensor_data_queue', 0)) {
$data = json_decode($result[1], true);
if ($data['status'] === 'ok') {
$stmt = $conn->prepare("INSERT INTO temperature_logs (machine_id, value) VALUES (?, ?)");
$stmt->bind_param("sd", $data['machine_id'], $data['value']);
$stmt->execute();
}
}
echo "报表生成完毕n";
这个脚本很聪明。它使用 brPop(..., 0),这个 0 表示“一直等,直到有数据”。如果 Redis 里没数据,脚本就停在这里,等下一分钟 Crontab 再唤醒它。这就保证了我们不会像无头苍蝇一样每秒查询一次数据库。
第七部分:故障排查与运维——当工厂断电时
写代码的时候觉得自己是乔布斯,一旦上线,你就是救火队员。
场景一:数据没发出来。
首先,看 PHP 进程还在不在?ps aux | grep php。如果进程挂了,检查内存溢出。工业数据量大的时候,图表实例没销毁,内存蹭蹭往上涨,最后 Out of memory。记得在 JS 里写 chart.data.labels.shift()。
场景二:前端一直在转圈圈。
检查 WebSocket 的端口防火墙是否开放。如果是 HTTPS 网站,WebSocket 握手协议会有点特殊,需要配置 Nginx 的 Upgrade 头。
场景三:Redis 连接不上。
这通常是网络问题或者 Redis 没启动。工业现场有时候会有电磁干扰,网线稍微一碰,数据就断了。这时候你的前端代码里要有 reconnect 机制,别傻乎乎地等 WebSocket 自己重启。
第八部分:进阶——监控自己
既然我们做的是监控系统,那我们的监控系统本身,是不是也应该被监控?
我们可以写一个脚本,去检查端口 8080 是否响应。
#!/bin/bash
# monitor.sh
while true; do
if ! netstat -an | grep 8080 | grep LISTEN > /dev/null; then
echo "报警!PHP WebSocket 服务挂了!正在尝试重启..."
/usr/bin/php /path/to/worker.php > /dev/null 2>&1 &
fi
sleep 10
done
把这个脚本扔到后台跑。这样,只要你的系统挂了,这个脚本会立马拉它起来。
结语:全栈的真谛
回过头来看,我们用 PHP 做了什么?
- 前端:处理用户交互,渲染 Canvas。
- 后端:处理逻辑,维持连接。
- 中间件:Redis 做缓冲。
- 数据库:持久化。
这就是全栈。不要觉得 PHP 只能写简单的 CRUD。在工业自动化这个领域,PHP 的灵活性、部署的简单性,加上 Redis 和 WebSocket 的加持,足以吊打那些动不动就要装 Java 虚拟机、配置几十个 Nginx 反向代理的复杂架构。
很多老工程师喜欢说:“不要重复造轮子。” 但今天我们造的这个轮子,是为了让生产线转得更快,让数据跑得更稳。
好了,代码都在这儿了。你可以试着跑一下,看看那个红线是不是在疯狂跳动。如果跳动得让你心慌,那就说明系统正常工作了。
祝大家代码无 Bug,工厂不炸膛!