Thinkphp 使用workerman消息实现消息推送完整示例

1.版本说明:ThinkPHP6.0+ 、PHP8.0+
2.实现原理

应用场景,监听用户登陆,修改用户登陆状态,通过心跳时间计算用户是否下线,其他应用场景自行修改逻辑即可。

  • 第一步:安装workerman
php 复制代码
composer require topthink/think-worker
  • 第二步:修改配置文件
php 复制代码
//  执行以上代码后,config目录下会自动生成三个文件:gateway_worker.php、worker.php、worker_server.php

//  因本人使用的gateway方式,因此需要修改的配置文件是gateway_worker.php,具体根据自己应用场景修改,本人修改的为业务处理逻辑的控制器

<?php

return [
    // 其他配置没有动,所以没复制上,只改的下面的eventHandler
  
    // BusinsessWorker配置
    'businessWorker' => [
        'name' => 'BusinessWorker',
        'count' => 1,
        'eventHandler' => '\app\common\event\WorkerHandler',
    ],

];
  • 第三步:启动服务
php 复制代码
//    windows无法启动的请看本人博客,里面有相关说明,Mac和Linux没问题 https://blog.csdn.net/qq_38997379/article/details/143957149?spm=1001.2014.3001.5501

php think worker:gateway
  • 第四步:连接测试,vue或html都可以,下面有具体封装的方法
  • 第五步:封装业务逻辑处理,下方有封装的方法
  • 第六步:线上部署,修改nginx伪静态
php 复制代码
location /wss {
    proxy_pass http://127.0.0.1:2358;    //    端口号改为自己的端口
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-Real-IP $remote_addr;
}
3.业务处理代码-WorkerHandler.php
php 复制代码
<?php

namespace app\common\event;

use app\common\model\JudgesPublic;
use GatewayWorker\Lib\Gateway;
use think\facade\Log;

use function htmlspecialchars;

/**
 * WebSocket 事件处理器
 */
class WorkerHandler
{
    /**
     * 当客户端连接时触发
     * @param mixed $client_id 客户端 ID
     */
    public static function onConnect(mixed $client_id): void
    {
        try {
            $response = [
                'type' => 'connect',
                'message' => 'Welcome to WebSocket server!',
            ];
            $jsonResponse = json_encode($response);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \Exception('Failed to encode JSON response');
            }
            Gateway::sendToClient($client_id, $jsonResponse);
        } catch (\Exception $e) {
            // 记录日志
            error_log("Error in onConnect: " . $e->getMessage());
        }
    }

    /**
     * 当客户端发送消息时触发
     * @param mixed $client_id 客户端 ID
     * @param string $message 接收到的消息
     */
    public static function onMessage(mixed $client_id, string $message): void
    {
        try {
            $data = json_decode($message, true);
            if (json_last_error() !== JSON_ERROR_NONE || !$data || !isset($data['type'])) {
                Gateway::sendToClient($client_id, json_encode([
                    'type' => 'error',
                    'message' => 'Invalid message format',
                ]));
                return;
            }

            switch ($data['type']) {
                case 'ping': // 心跳包
                    //  保存用户最近一次心跳时间,本人使用redis
                    $uid = Gateway::getUidByClientId($client_id);
                    redis_set('user_judges_public_last_heartbeat_' . $uid, $data['timestamp'], 600);
                    Gateway::sendToClient($client_id, json_encode([
                        'type' => 'pong',
                        'message' => 'Heartbeat received',
                    ]));
                    break;

                case 'login': // 登陆
                    //  绑定用户信息
                    Gateway::bindUid($client_id, $data['user_id']);
                    redis_set('user_client_judges_public_' . $data['user_id'], $client_id, 18 * 3600);
                    //  修改用户状态
                    self::changeJudgeStatus($data['user_id']);
                    Gateway::sendToClient($client_id, json_encode([
                        'type' => 'login',
                        'message' => '登陆成功,user_id' . $data['user_id'],
                    ]));
                    break;

                case 'broadcast': // 广播消息
                    $safeMessage = htmlspecialchars($data['message'] ?? 'No message', ENT_QUOTES, 'UTF-8');
                    Gateway::sendToAll(json_encode([
                        'type' => 'broadcast',
                        'message' => $safeMessage,
                    ]));
                    break;

                case 'private': // 私聊消息
                    if (isset($data['to_client_id']) && is_numeric($data['to_client_id'])) {
                        $toClientId = intval($data['to_client_id']);
                        if (Gateway::isUidOnline($toClientId)) {
                            $safeMessage = htmlspecialchars($data['message'] ?? 'No message', ENT_QUOTES, 'UTF-8');
                            Gateway::sendToClient($toClientId, json_encode([
                                'type' => 'private',
                                'from_client_id' => $client_id,
                                'message' => $safeMessage,
                            ]));
                        } else {
                            Gateway::sendToClient($client_id, json_encode([
                                'type' => 'error',
                                'message' => 'Recipient client ID is not online',
                            ]));
                        }
                    } else {
                        Gateway::sendToClient($client_id, json_encode([
                            'type' => 'error',
                            'message' => 'Invalid recipient client ID',
                        ]));
                    }
                    break;

                default:
                    Gateway::sendToClient($client_id, json_encode([
                        'type' => 'error',
                        'message' => 'Unknown message type',
                    ]));
                    break;
            }
        } catch (\Exception $e) {
            // 记录日志
            error_log("Error in onMessage: " . $e->getMessage());
        }
    }

    /**
     * 当客户端断开连接时触发
     * @param mixed $client_id 客户端 ID
     */
    public static function onClose(mixed $client_id): void
    {
        try {
            $response = [
                'type' => 'disconnect',
                'client_id' => $client_id,
                'message' => 'A client has disconnected',
            ];
            $jsonResponse = json_encode($response);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \Exception('Failed to encode JSON response');
            }
            Gateway::sendToAll($jsonResponse);
        } catch (\Exception $e) {
            // 记录日志
            error_log("Error in onClose: " . $e->getMessage());
        }
    }

    /**
     * @note 错误监听
     * @param mixed $client_id
     * @param mixed $code
     */
    public static function onError(mixed $client_id, mixed $code): void
    {
        try {
            Log::info($code . "_错误码" . $client_id);
            $response = [
                'type' => 'error',
                'client_id' => $client_id,
                'message' => 'An error occurred',
            ];
            $jsonResponse = json_encode($response);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \Exception('Failed to encode JSON response');
            }
//            Gateway::sendToAll($jsonResponse);
        } catch (\Exception $e) {
            error_log("Error in onError: " . $e->getMessage());
        }
    }

    /**
     * @note 修改登陆状态
     * @param mixed $user_id
     * @param int $status 1=登陆 0=未登陆
     */
    protected static function changeJudgeStatus(mixed $user_id, int $status = 1): void
    {
        try {
            $model = new model();
            $info = $model->findOrEmpty($user_id);
            if (!$info->isEmpty()) {
                $info->login_status = $status;
                $info->save();
            } else {
                Log::error($user_id . "_登陆状态修改失败");
            }
        } catch (\Exception $e) {
            Log::error($user_id . "_登陆状态修改失败:" . $e->getMessage());
        }
    }

   
}
4.前端代码封装
  • 封装scoket.js,带心跳
php 复制代码
// src/utils/websocket.js
export default class WebSocketService {
    constructor(url) {
        this.url = url; // WebSocket 服务器地址
        this.socket = null; // WebSocket 实例
        this.isConnected = false; // 连接状态
        this.messageCallback = null; // 消息回调
        this.errorCallback = null; // 错误回调
    }

    // 初始化 WebSocket
    connect() {
        if (!this.socket) {
            this.socket = new WebSocket(this.url);

            // 监听连接成功
            this.socket.onopen = () => {
                this.isConnected = true;
                console.log("WebSocket connected success");
                this.startHeartbeat(); // 开始心跳
            };

            // 监听消息
            this.socket.onmessage = (event) => {
                if (this.messageCallback) {
                    this.messageCallback(JSON.parse(event.data));
                }
            };

            // 监听错误
            this.socket.onerror = (error) => {
                console.error("WebSocket error:", error);
                if (this.errorCallback) {
                    this.errorCallback(error);
                }
            };

            // 监听连接关闭
            this.socket.onclose = () => {
                this.isConnected = false;
                console.log("WebSocket disconnected");
                this.stopHeartbeat(); // 停止心跳
                this.reconnect(); // 尝试重新连接
            };

        }
    }

    // 重新连接逻辑
    reconnect() {
        console.log("Reconnecting WebSocket...");
        setTimeout(() => {
            this.connect();
        }, 5000); // 5 秒后重连
    }

    // 发送消息
    sendMessage(message) {
        if (this.isConnected && this.socket) {
            this.socket.send(JSON.stringify(message));
        } else {
            console.error("WebSocket is not connected");
        }
    }

    // 设置消息回调
    onMessage(callback) {
        this.messageCallback = callback;
    }

    // 设置错误回调
    onError(callback) {
        this.errorCallback = callback;
    }

    // 手动关闭 WebSocket
    close() {
        if (this.socket) {
            this.socket.close();
            this.socket = null;
            this.isConnected = false;
        }
    }

    // 添加心跳机制
    startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
            if (this.isConnected) {
                this.sendMessage({type: 'ping', message: 'Heartbeat', timestamp: Date.now()});
                // console.log('Heartbeat sent');
            }
        }, 10000); // 每 10 秒发送一次心跳
    }

    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
        }
    }
}
  • main.js引入
php 复制代码
//  自己地址
import WebSocketService from './utils/socket/websocket';

// 创建一个全局的 WebSocket 实例
// const wsService = new WebSocketService('ws://127.0.0.1:2358');
const wsService = new WebSocketService('wss://xs.qgmykj.com/wss');
wsService.connect();



// 将 WebSocket 实例通过 provide 注入
app.provide('websocket', wsService);

app.mount("#app");
  • 页面引入
php 复制代码
const websocket = inject('websocket'); // 获取 WebSocket 实例
const message = ref(null); // 保存收到的消息

onMounted(() => {

  websocket.onMessage((data) => {
    message.value = data;
    console.log('收到消息:', data);
  });

})

// 发送消息
const sendMessage = (userId) => {
  const data = {type: 'login', user_id: userId, message: 'Hello, WebSocket!'};
  websocket.sendMessage(data);
};
相关推荐
微扬嘴角1 小时前
springcloud篇2-feign、gateway
gateway·springcloud·feign
m0_748241122 小时前
Gateway Timeout504 网关超时的完美解决方法
gateway
小汤猿人类2 小时前
nacos-gateway动态路由
java·前端·gateway
新知图书11 小时前
ThinkPHP 8开发环境安装
thinkphp·thinkphp8
抓哇小菜鸡12 小时前
WebSocket
java·websocket
龙哥·三年风水13 小时前
workman服务端开发模式-应用开发-vue-element-admin挂载websocket
分布式·websocket·vue
binqian14 小时前
【WebSocket】tomcat内部处理websocket的过程
websocket
非凡的世界14 小时前
PHP高性能webman管理系统EasyAdmin8
php·thinkphp·高性能·webman
桃园码工20 小时前
21_HTML5 WebSocket --[HTML5 API 学习之旅]
websocket·学习·html5