websocket实现站内信

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>
相关推荐
2401_865854882 小时前
ssl证书使用中可能会遇到的问题
网络·网络协议·ssl
美团骑手阿豪2 小时前
Unity适配 安卓15+三键导航模式下的 底部UI被遮挡
android·智能手机
张海龙_China2 小时前
Android 上架Google Play ~16KB内存页机制适配指南
android
blackorbird2 小时前
Android Pixel 9 的零点击漏洞利用链全解析:从发送杜比音频解码到内核提权
android·音视频
liux35282 小时前
MySQL执行计划与索引优化全面解析(三)
android·mysql·adb
捷米研发三部2 小时前
CANopen 转 Modbus TCP:智能机床控制器与上位机监控系统的无缝对接方案
网络·网络协议
上天_去_做颗惺星 EVE_BLUE3 小时前
Android设备与Mac/Docker全连接指南:有线到无线的完整方案
android·linux·macos·adb·docker·容器·安卓
zhangphil3 小时前
Android显示系统性能分析:trace的HWUI All Memory与HWUI Misc Memory
android
txinyu的博客3 小时前
UDP & TCP
网络协议·tcp/ip·udp