laravel整合workerman實現websocket多端及時通訊

Crastlin發表於2023-05-08

1、建立啟動命令檔案
在app/Console/Commands/目錄下建立WorkerMan.php

namespace App\Console\Commands;

use GatewayWorker\BusinessWorker;
use GatewayWorker\Gateway;
use GatewayWorker\Register;
use Illuminate\Console\Command;
use Workerman\Worker;

class WorkerMan extends Command
{

    protected $signature = 'wk {action} {--d}';

    protected $description = 'Start a Workerman server.';

    public function handle()
    {
        global $argv;
        $action = $this->argument('action');

        $argv[0] = 'wk';
        $argv[1] = $action;
        $argv[2] = $this->option('d') ? '-d' : '';

        $this->start();
    }

    private function start()
    {
        $this->startGateWay();
        $this->startBusinessWorker();
        $this->startRegister();

        $workerPath = storage_path('workerman/');
        if (!is_dir($workerPath))
            mkdir($workerPath, 0755, true);
        Worker::$pidFile = $workerPath . config('app.name') . '_workman.pid';
        $logPath = $workerPath . date('Ym') . '/';
        if (!is_dir($logPath))
            mkdir($logPath, 0755, true);
        Worker::$logFile = $logPath . date('d') . '.log';

        Worker::runAll();
    }

    private function startBusinessWorker()
    {
        $worker = new BusinessWorker();
        $worker->name = 'BusinessWorker';
        $worker->count = 3;
        $worker->registerAddress = '127.0.0.1:1236';
        $worker->eventHandler = \App\Workerman\Events::class;
    }

    private function startGateWay()
    {
        //http://doc.workerman.net/faq/secure-websocket-server.html
        // 證照最好是申請的證照
//        $context = array(
//            // 更多ssl選項請參考手冊 http://php.net/manual/zh/context.ssl.php
//            'ssl' => array(
        // 請使用絕對路徑
        // 'local_cert' => '/www/wwwroot/cattle_car.com/public/ssl/4035525_niuniu.micropig.cn.pem', // 也可以是crt檔案
        //'local_pk' => '/www/wwwroot/cattle_car.com/public/ssl/4035525_niuniu.micropig.cn.key',
//                'local_cert' => '/www/server/panel/vhost/cert/cattle_car.com/fullchain.pem',
//                'local_pk' => '/www/server/panel/vhost/cert/cattle_car.com/privkey.pem',
//                'verify_peer' => false,
//                'verify_peer_name' => false,
        // 'allow_self_signed' => true, //如果是自簽名證照需要開啟此選項
//            )
//        );
        //$gateway = new Gateway("websocket://0.0.0.0:2346", $context);

        $gateway = new Gateway("websocket://0.0.0.0:2346");
        //$gateway->transport = 'ssl';
        $gateway->name = 'Gateway';
        $gateway->count = 1;
        $gateway->lanIp = '127.0.0.1';
        $gateway->startPort = 2300;
        $gateway->pingInterval = 30;
        $gateway->pingNotResponseLimit = 0;
        $gateway->pingData = '{"type":"ping"}';
        $gateway->registerAddress = '127.0.0.1:1236';
    }

    private function startRegister()
    {
        new Register('text://127.0.0.1:1236');
    }
}
  • 啟動命令
    [root@local]# php artisan wk start
  • 守護模式啟動
    [root@local]# php artisan wk start --d
  • 其它命令:
    檢視狀態:status / 停止: stop / 過載: reload / 重啟: restart [–d]

2、建立windows環境啟動命令檔案
由於windows不支援批次啟動服務,所以每個服務需要單獨啟動


namespace App\Console\Commands;

use GatewayWorker\BusinessWorker;
use GatewayWorker\Gateway;
use GatewayWorker\Register;
use Illuminate\Console\Command;
use Workerman\Worker;

class WorkerManWin extends Command
{

    //相容win
    protected $signature = 'wk
                            {action : action}
                            {--start=all : start}
                            {--d : daemon mode}';

    protected $description = 'Start a Workerman server.';

    public function handle()
    {
        global $argv;
        $action = $this->argument('action');

        //針對 Windows 一次執行,無法註冊多個協議的特殊處理
        if ($action === 'single') {
            $start = $this->option('start');
            if ($start === 'register') {
                $this->startRegister();
            } elseif ($start === 'gateway') {
                $this->startGateWay();
            } elseif ($start === 'worker') {
                $this->startBusinessWorker();
            }
            Worker::runAll();

            return;
        }

        $argv[1] = $action;
        $argv[2] = $this->option('d') ? '-d' : '';

        $this->start();
    }

    private function start()
    {
        $this->startGateWay();
        $this->startBusinessWorker();
        $this->startRegister();
        Worker::runAll();
    }

    private function startBusinessWorker()
    {
        $worker = new BusinessWorker();
        $worker->name = 'BusinessWorker';
        $worker->count = 1;
        $worker->registerAddress = '127.0.0.1:1236';
        $worker->eventHandler = \App\Workerman\Events::class;
    }

    private function startGateWay()
    {
        $gateway = new Gateway("websocket://0.0.0.0:2346");
        $gateway->name = 'Gateway';
        $gateway->count = 1;
        $gateway->lanIp = '127.0.0.1';
        $gateway->startPort = 2300;
        $gateway->pingInterval = 30;
        $gateway->pingNotResponseLimit = 0;
        $gateway->pingData = '{"type":"ping"}';
        $gateway->registerAddress = '127.0.0.1:1236';
    }


    private function startRegister()
    {
        new Register('text://0.0.0.0:1236');
    }
}
  • 啟動命令
    [root@local]# php artisan wk single --start=gateway
    [root@local]# php artisan wk single --start=worker
    [root@local]# php artisan wk single --start=register

    3、定義事件類
    在app/Workerman/目錄下建立Events.php

namespace App\Workerman;

use App\Helpers\Jwt;
use App\Models\UserModel;
use GatewayWorker\BusinessWorker;
use GatewayWorker\Lib\Gateway;
use Illuminate\Support\Facades\Log;
use Workerman\Lib\Timer;

class Events
{


    /**
     * 業務服務啟動事件
     * @param BusinessWorker $businessWorker
     * @return void
     */
    public static function onWorkerStart(BusinessWorker $businessWorker)
    {
        self::log(__FUNCTION__, $businessWorker->workerId);
        Timer::add(1, function () use ($businessWorker) {
            $time_now = time();
            foreach ($businessWorker->connections as $connection) {
                // 有可能該connection還沒收到過訊息,則lastMessageTime設定為當前時間
                if (empty($connection->lastMessageTime)) {
                    $connection->lastMessageTime = $time_now;
                    continue;
                }
                // 上次通訊時間間隔大於心跳間隔,則認為客戶端已經下線,關閉連線
                if ($time_now - $connection->lastMessageTime > 30) {
                    if ($connection->id) {
                        //todo
                    }
                    //斷開後的回撥
                    echo "Client ip {$connection->getRemoteIp()} timeout!!!\n";
                    $connection->close();
                }
            }
        });
    }

    /**
     * 客戶端連線事件
     * @param string $clientId
     * @return void
     */
    public static function onConnect(string $clientId)
    {
        self::log(__FUNCTION__, $clientId);
    }

    /**
     * 客戶端websocket 連線事件
     * @param string $clientId
     * @param mixed $data
     * @return void
     */
    public static function onWebSocketConnect(string $clientId, $data)
    {
        self::log(__FUNCTION__, $clientId, $data);
    }

    /**
     * 客戶端websocket訊息
     * @param string $clientId
     * @param string $messageJson
     * @return void
     */
    public static function onMessage(string $clientId, string $messageJson)
    {
        self::log(__FUNCTION__, $clientId, $messageJson);
        $message = json_decode($messageJson);
        if (empty($message->type)) {
            self::sendMessage(500, '請配置type');
            return;
        }
        switch ($message->type) {
            case 'login':
                // 登入業務
                break;
            case 'ping':
                self::sendMessage(201, 'pong');
                break;
            default:
                self::sendMessage(500, '訊息型別不支援');
        }
    }


    /**
     * 關閉客戶端websocket
     * @param string $clientId
     * @return void
     */
    public static function onClose(string $clientId)
    {
        self::log(__FUNCTION__, $clientId);
        Gateway::destoryClient($clientId);
    }


    /**
     * 寫日誌
     * @param string $title
     * @param $data
     * @return void
     */
    protected static function log(string $title, ...$data): void
    {
        if (config('app.debug')) {
            var_dump("========== {$title} ==========");
            var_dump($data);
            Log::info("{$title} | " . json_encode($data, 256));
        }
    }


    /**
     * 傳送客戶端訊息
     * @param int $code
     * @param mixed $message
     * @param array|null $data
     * @param string $clientId
     * @return void
     */
    protected static function sendMessage(int $code, $message, ?array $data = null, string $clientId = ''): void
    {
        $sendMessage = json_encode([
            'code' => $code,
            'msg' => $message,
            'data' => $data,
        ]);
        if ($clientId)
            Gateway::sendToClient($clientId, $sendMessage);
        else
            Gateway::sendToCurrentClient($sendMessage);
    }

}

4、js連線websocket服務

let ws = new WebSocket('ws://192.168.0.100:2346');
// 獲取連線狀態
console.log('ws連線狀態:' + ws.readyState);
//監聽是否連線成功
ws.onopen = function () {
    console.log('ws連線狀態:' + ws.readyState);
    //連線成功則傳送登入請求
 let message =  {type: "login", token: "JWT授權碼"};
 ws.send(JSON.stringify(message));
}
// 接聽伺服器發回的資訊並處理展示
ws.onmessage = function (data) {
    console.log('接收到來自伺服器的訊息:');
    console.log(data);
    //完成通訊後關閉WebSocket連線
    ws.close();
}
// 監聽連線關閉事件
ws.onclose = function () {
    // 監聽整個過程中websocket的狀態
    console.log('ws連線狀態:' + ws.readyState);
}
// 監聽並處理error事件
ws.onerror = function (error) {
    console.log(error);
}

5、後端向客戶端傳送訊息

\GatewayClient\Gateway::$registerAddress = '127.0.0.1:1236';
        Gateway::sendToUid(29, '{"type": "update", "data": {"name": "張三", "aratar": "..."}}');
  • 注意:此處是單向api方式傳送訊息,registerAddress地址與啟動服務註冊的registerAddress地址保持對應,如本地可直接使用預設設定。內網呼叫時,服務與客戶端使用內網IP。

  • Gateway服務端與客戶端使用方法,請查閱官方手冊:www.workerman.net/doc/gateway-work...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章