You need to enable JavaScript to run this app. Laravel+Ratchet如何从独立类向特定WebSocket客户端主动推送通知
优惠活动
大模型
产品
解决方案
定价
更多

Laravel+Ratchet如何从独立类向特定WebSocket客户端主动推送通知

问题核心本质

你遇到的障碍完全来自两个容易被新手忽略的底层原理:

  • 你通过命令行启动的Ratchet WebSocket服务是独立的常驻PHP进程,和Laravel框架下处理HTTP请求、运行队列监听器的进程完全隔离,二者内存空间不共享。
  • 你在LogNotification监听器里哪怕实例化WebSocketController类,拿到的也是当前业务进程内存里的全新空对象,和常驻WS进程里存储的所有客户端连接没有任何关联,自然无法完成推送。

所有能跑通的主动推送方案,核心都是围绕跨进程通信设计的,没有捷径。

适配你现有Ratchet技术栈的实现步骤

1. 改造常驻WebSocket服务的内置逻辑

首先把连接管理的核心逻辑完全放在WS进程内部,不要分散到其他进程里:

  • 在你的WebSocketController类里增加全局连接映射属性,用来存所有在线的客户端连接:
    // 在WebSocketController类中定义
    protected array $userConnections = [];
    
  • 改造onOpen方法:在客户端建立WS连接时,从握手指纹、Cookie、JWT等请求信息中解析出连接对应的登录用户ID,把连接实例和用户ID做绑定:
    public function onOpen(ConnectionInterface $conn)
    {
        // 从握手的HTTP请求中解析当前登录用户ID
        $userId = $this->parseUserIdFromHandshake($conn->httpRequest);
        // 给连接实例附加用户标识
        $conn->userId = $userId;
        // 把连接存入映射表,支持同一个用户多标签页/多设备同时在线
        $this->userConnections[$userId][$conn->resourceId] = $conn;
    }
    
  • 改造onClose方法:客户端断开连接时及时清理映射表中的无效连接,避免内存泄漏、向已关闭连接发送消息报错:
    public function onClose(ConnectionInterface $conn)
    {
        if (!isset($conn->userId)) return;
        // 删除对应当前连接的映射
        unset($this->userConnections[$conn->userId][$conn->resourceId]);
        // 如果该用户没有剩余在线连接,清空对应的空数组
        if (empty($this->userConnections[$conn->userId])) {
            unset($this->userConnections[$conn->userId]);
        }
    }
    
  • 给WS服务的事件循环挂一个公共通道监听器:让常驻WS进程持续监听一个所有Laravel业务进程都能写入的消息通道,收到通道消息后就从连接映射表中找到目标用户的连接,直接发送内容。对于已接入Laravel生态的项目,用Redis做这个公共通道是成本最低的方案:
    你可以在启动WS服务的命令脚本中,给ReactPHP事件循环加一个轮询逻辑,检查Redis中是否有新的推送指令:
    // 在WS服务启动逻辑中,获取到EventLoop实例后
    $loop->addPeriodicTimer(0.01, function() use ($websocketController) {
        $redis = new \Redis();
        $redis->pconnect(
            config('database.redis.default.host'),
            config('database.redis.default.port')
        );
        // 从推送队列中取出消息(1秒超时,不阻塞事件循环)
        $message = $redis->brPop(['ws_push_queue'], 1);
        if (!$message) return;
    
        $payload = json_decode($message[1], true);
        $targetUserId = $payload['user_id'];
        $pushContent = $payload['data'];
    
        // 检查目标用户是否有在线连接,逐连接发送消息
        if (isset($websocketController->userConnections[$targetUserId])) {
            foreach ($websocketController->userConnections[$targetUserId] as $conn) {
                $conn->send(json_encode($pushContent));
            }
        }
    });
    

2. 改造你的监听器逻辑

监听器完全不需要接触WS连接实例,只需要按照约定格式把推送指令写入刚才的公共Redis队列即可:

public function handle(NotificationSent $event)
{
    // 从通知事件中获取接收推送的目标用户ID
    $targetUserId = $event->notifiable->id;
    // 组装要推送给客户端的内容
    $pushData = [
        'type' => 'new_notification',
        'content' => $event->notification->toArray()
    ];

    // 把推送指令写入公共队列,和WS进程形成解耦
    \Illuminate\Support\Facades\Redis::lpush('ws_push_queue', json_encode([
        'user_id' => $targetUserId,
        'data' => $pushData
    ]));
}
容易踩的坑
  • 不要尝试在业务进程中新建WebSocketController实例操作推送,跨进程的对象内存不互通,这种操作永远找不到真实的客户端连接。
  • 连接映射的清理逻辑必须覆盖所有断开场景:网络超时、客户端直接关闭页面、异常断连等,否则会出现长期运行后的内存泄漏。
  • 确保WS常驻进程和业务进程连接的是同一个Redis实例,不然消息会出现写入后WS进程完全收不到的问题。
  • 如果业务逻辑对实时性要求不高,哪怕用数据库表当公共通道也能跑通,核心逻辑不变:所有连接操作都收敛在WS常驻进程内部,业务进程只发推送指令。

内容的提问来源于stack exchange,提问作者jgarcias

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.22 18:15:03