start.php
bash
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
// 创建 WebSocket 服务,监听 0.0.0.0:2346
$ws_worker = new Worker("websocket://0.0.0.0:2346");
// 进程数,建议与 CPU 核数一致
$ws_worker->count = 4;
// ✅ 【修复点】静态数组存储用户连接,全局共享
static $user_connections = [];
// 新连接建立时触发
$ws_worker->onConnect = function(TcpConnection $connection) {
echo "新连接建立:{$connection->id}\n";
};
// 客户端发送消息时触发(用于用户登录绑定)
$ws_worker->onMessage = function(TcpConnection $connection, $data) {
global $user_connections;
// ✅ 【修复点】json_decode 增加异常判断,容错非JSON数据
$data = json_decode($data, true);
if (json_last_error() !== JSON_ERROR_NONE || !$data || !isset($data['type'])) {
$connection->send(json_encode(['code' => 400, 'msg' => '数据格式错误']));
return;
}
// 类型1:用户登录,绑定 user_id 与连接
if ($data['type'] == 'login') {
$user_id = isset($data['user_id']) ? intval($data['user_id']) : 0;
if (!$user_id) {
$connection->send(json_encode(['code' => 401, 'msg' => '用户ID不能为空']));
return;
}
// ✅ 【修复点】覆盖旧连接,解决重复登录内存泄漏问题
$user_connections[$user_id] = $connection;
// 绑定用户ID到连接属性,方便后续关闭时快速删除
$connection->user_id = $user_id;
// 回复登录成功
$connection->send(json_encode(['code' => 200, 'msg' => '登录成功']));
echo "用户 {$user_id} 绑定连接 {$connection->id}\n";
}
};
// ✅ 【新增核心方法】处理PHP推送端的TCP请求,转发消息给指定WebSocket客户端
$ws_worker->onWorkerStart = function($worker) {
// 创建一个内部TCP服务,专门接收PHP业务端的推送请求,监听 2347 端口
$inner_worker = new Worker('tcp://127.0.0.1:2347');
$inner_worker->onMessage = function($connection, $recv_data) {
global $user_connections;
$recv_data = trim($recv_data);
if (empty($recv_data)) {
$connection->send(json_encode(['code' => 400, 'msg' => '空数据']));
$connection->close();
return;
}
// 解析推送端发来的JSON数据
$data = json_decode($recv_data, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($data['to_user_id']) || !isset($data['msg'])) {
$connection->send(json_encode(['code' => 400, 'msg' => '推送数据格式错误']));
$connection->close();
return;
}
$to_user_id = intval($data['to_user_id']);
$msg = $data['msg'];
$send_status = false;
// ✅ 【修复点】遍历所有进程的连接,实现多进程共享连接(核心!)
$workers = Worker::getAllWorkers();
foreach ($workers as $wk) {
foreach ($wk->connections as $conn) {
if (isset($conn->user_id) && $conn->user_id == $to_user_id) {
// 给客户端推送最终的消息体
$conn->send(json_encode([
'type' => 'message',
'data' => $msg,
'time' => date('Y-m-d H:i:s')
]));
$send_status = true;
echo "成功推送给用户 {$to_user_id} 消息\n";
}
}
}
// 返回推送结果给业务端
if ($send_status) {
$connection->send(json_encode(['code' => 200, 'msg' => '推送成功']));
} else {
$connection->send(json_encode(['code' => 404, 'msg' => '用户未在线/未找到连接']));
}
$connection->close();
};
$inner_worker->listen();
echo "内部推送服务启动成功:tcp://127.0.0.1:2347\n";
};
// 连接关闭时触发
$ws_worker->onClose = function(TcpConnection $connection) {
global $user_connections;
// ✅ 【修复点】通过连接属性快速删除,无需遍历,效率极高
if (isset($connection->user_id)) {
unset($user_connections[$connection->user_id]);
echo "用户 {$connection->user_id} 连接关闭\n";
}
};
// 启动服务
Worker::runAll();
push.php
bash
<?php
class Push
{
/**
* 推送站内信给指定用户
* @param int $user_id 接收用户ID
* @param array $msg 消息内容 ['title'=>'标题','content'=>'内容','time'=>'时间']
* @return bool true=推送成功 false=推送失败
*/
public static function sendMsg($user_id, $msg)
{
$user_id = intval($user_id);
if (!$user_id || empty($msg)) {
return false;
}
// ✅ 【核心修复】连接【内部TCP推送服务】2347端口,不是WebSocket的2346端口
$client = stream_socket_client("tcp://127.0.0.1:2347", $errno, $errstr, 3);
if (!$client) {
// 可选:记录日志 $errno, $errstr
return false;
}
// 构造推送数据
$data = json_encode([
'type' => 'message',
'to_user_id' => $user_id,
'msg' => $msg
]);
// ✅ 【修复点】检测是否发送成功 + 增加结束符避免粘包
$send_len = fwrite($client, $data . PHP_EOL);
if (!$send_len) {
fclose($client);
return false;
}
// ✅ 【新增】接收服务端的推送结果,确保推送状态准确
$response = trim(fgets($client));
$res_data = json_decode($response, true);
$success = isset($res_data['code']) && $res_data['code'] == 200;
// ✅ 【修复点】优雅关闭连接
fclose($client);
return $success;
}
}
// 例如:用户下单后推送通知
$push_res = Push::sendMsg(1002, [
'title' => '订单通知',
'content' => '你的订单已发货,请注意查收',
'time' => date('Y-m-d H:i:s')
]);
var_dump($push_res); // true=成功 false=失败
index.html
bash
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>站内信通知</title>
</head>
<body>
<h3>我的消息</h3>
<div id="msg_list" style="border:1px solid #ccc;padding:10px;height:300px;overflow-y:auto;"></div>
<script>
// 当前用户ID(实际从后端登录态获取,如 $_SESSION['user_id'])
const USER_ID = 1001;
// 连接 WebSocket 服务
const ws = new WebSocket("ws://127.0.0.1:2346");
// 连接成功时,发送登录绑定
ws.onopen = function() {
ws.send(JSON.stringify({
type: 'login',
user_id: USER_ID
}));
};
// 接收服务端推送的消息
ws.onmessage = function(event) {
const res = JSON.parse(event.data);
console.log(res)
if (res.code == 200) {
console.log(res.msg);
return;
}
// 渲染消息到页面
const msgList = document.getElementById('msg_list');
const msgHtml = `<div style="margin:5px 0;padding:5px;border-bottom:1px dashed #eee;">
<h4>${res.msg.title}</h4>
<p>${res.msg.content}</p>
<small>${res.msg.time}</small>
</div>`;
msgList.innerHTML += msgHtml;
// 滚动到底部
msgList.scrollTop = msgList.scrollHeight;
};
// 连接关闭重连
ws.onclose = function() {
setTimeout(() => window.location.reload(), 3000);
};
</script>
</body>
</html>