Laravel 整合 workerman 做聊天室

xingkong12138發表於2019-12-12

在很多時候我們需要做這種聊天室的時候需要實時響應,所以今天介紹一下workerman,如果沒有聽過workerman的小夥伴,傳送門:workerman文件官網

本篇文章是整合laravel 5.8的,使用artisan 命令去管理workerman

如果覺得workerman不好搞,也可以直接使用GatewayWorker,傳送門:GatewayWorker手冊在 Laravel 中使用 GatewayWorker 進行 socket 通訊

第一步 安裝workerman

在專案根目錄執行

composer require workerman/workerman

第二部 建立命令類,自定義啟停workerman

在專案根目錄執行

php artisan make:command Workerman

執行該命令會在app/Console/Commands/下面建立Workerman.php檔案,該檔案程式碼如下:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Workerman\Autoloader;
use Workerman\Worker;

class Workerman extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'Workerman {action} {--daemonize}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'workerman 啟動停止';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        global $argv;//定義全域性變數
        $arg = $this->argument('action');
        $argv[1] = $arg;
        $argv[2] = $this->option('daemonize') ? '-d' : '';//該引數是以daemon(守護程式)方式啟動

        global $text_worker;
        // 建立一個Worker監聽2345埠,使用websocket協議通訊
        $text_worker = new Worker("websocket://0.0.0.0:2345");
        $text_worker->uidConnections = array();//線上使用者連線物件
        $text_worker->uidInfo = array();//線上使用者的使用者資訊
        // 啟動4個程式對外提供服務
        $text_worker->count = 4;
        //引用類檔案
        $handler = \App::make('Handler\WorkermanHandler');
        $text_worker->onConnect = array($handler,"handle_connection");
        $text_worker->onMessage = array($handler,"handle_message");
        $text_worker->onClose = array($handler,"handle_close");
        $text_worker->onWorkerStart = array($handler,"handle_start");

        // 執行worker
        Worker::runAll();

    }
}
注意:action 是啟動引數,daemonize選項是是否後臺執行,如果不需要後臺執行請把有關daemonize的程式碼刪掉。
最最重要的地方來了$argv一定不能刪掉,否則啟動workerman的時候會報錯

然後手動建立app/Handler/WorkermanHandler.php檔案,程式碼如下:

<?php
namespace Handler;

use Illuminate\Console\Command;
use Workerman\Lib\Timer;
use Workerman\Worker;

class WorkermanHandler
{

    private $heartbeat_time = 55;//心跳間隔55秒

    //當客戶端連上來時分配uid,並儲存連結,並通知所有客戶端
    public function handle_connection($connection){
        global $text_worker;

        //判斷是否設定了UID
        if(!isset($connection->uid)){
            //給使用者分配一個UID
            $connection->uid = random_string(20);
            //儲存使用者的uid
            $text_worker->uidConnections["{$connection->uid}"] = $connection;
            //向使用者返回建立成功的資訊
            $connection->send("使用者:[{$connection->uid}] 建立成功");
        }
    }

    public function handle_start(){
        global $text_worker;
        //每秒都判斷客戶端是否已下線
        Timer::add(1, function()use($text_worker){
            $time_now = time();
            foreach($text_worker->connections as $connection) {
                // 有可能該connection還沒收到過訊息,則lastMessageTime設定為當前時間
                if (empty($connection->lastMessageTime)) {
                    $connection->lastMessageTime = $time_now;
                    continue;
                }
                // 上次通訊時間間隔大於心跳間隔,則認為客戶端已經下線,關閉連線
                if ($time_now - $connection->lastMessageTime > $this->heartbeat_time) {
                    $connection->close();
                }
            }
        });
        //每隔30秒就向客戶端傳送一條心跳驗證
        Timer::add(50,function ()use ($text_worker){
            foreach ($text_worker->connections as $conn){
                $conn->send('{"type":"ping"}');
            }
        });
    }

    //當客戶端傳送訊息過來時,轉發給所有人
    public function handle_message($connection,$data){
        global $text_worker;
        //debug
        //echo "data_info:".$data.PHP_EOL;
        $connection->lastMessageTime = time();
        $data_info=json_decode($data,true);
        if(!$data_info){
            return ;
        }
        //判斷業務型別
        switch($data_info['type'])
        {
            case 'login':
                //判斷使用者資訊是否存在
                if(empty($data_info['user_id'])){
                    $connection->send("{'type':'error','msg':'非法請求'}");
                    return $connection->close();
                }
                //判斷使用者是否已經登入了
                $user_ids=array_column($text_worker->uidInfo,"user_id");
                if(in_array($data_info['user_id'],$user_ids)){
                    $connection->send("{'type':'error','msg':'你在其它地方已登入'}");
                    return $connection->close();
                }
                //儲存使用者資訊
                $text_worker->uidInfo["{$connection->uid}"]=array(
                    "user_id"=>$data_info['user_id'],
                    "user_name"=>htmlspecialchars($data_info['user_name']),
                    "user_header"=>$data_info['user_header'],
                    "create_time"=>date("Y-m-d H:i"),
                );
                //返回資料
                if($data_info['to_uid'] == "all"){
                    $return_data=array(
                        "type"=>"login",
                        "uid"=>$connection->uid,
                        "user_name"=>htmlspecialchars($data_info['user_name']),
                        "user_header"=>$data_info['user_header'],
                        "send_time"=>date("Y-m-d H:i",time()),
                        "user_lists"=>$text_worker->uidInfo
                    );
                    $curral_data=array(
                        "type"=>"login_uid",
                        "uid"=>$connection->uid,
                    );
                    $connection->send(json_encode($curral_data));
                    //給所有使用者傳送一條資料
                    foreach($text_worker->connections as $conn){
                        $conn->send(json_encode($return_data));
                    }
                }else{
                    return ;
                }
                return;
            //使用者發訊息
            case 'say':
                if(!isset($text_worker->uidInfo["{$connection->uid}"]) || empty($text_worker->uidInfo["{$connection->uid}"])){
                    $connection->send('{"type":"error","msg":"你已經掉線了"}');
                }
                //獲取到當前使用者的資訊
                $user_info=$text_worker->uidInfo["{$connection->uid}"];

                //判斷是私聊還是群聊
                if($data_info['to_uid'] != "all"){
                    //私聊
                    $return_data=array(
                        "type"=>"say",
                        "from_uid"=>$connection->uid,
                        "from_user_name"=>$user_info['user_name'],
                        "from_user_header"=>$user_info['user_header'],
                        "to_uid"=>$data_info['to_uid'],
                        "content"=>nl2br(htmlspecialchars($data_info['content'])),
                        "send_time"=>date("Y-m-d H:i")
                    );
                    if($data_info['to_uid'] == $connection->uid){
                        $connection->send(json_encode($return_data));
                        return;
                    }
                    //判斷使用者是否存在,並向對方傳送資料
                    if(isset($text_worker->uidConnections["{$data_info['to_uid']}"])){
                        $to_connection=$text_worker->uidConnections["{$data_info['to_uid']}"];
                        $to_connection->send(json_encode($return_data));
                    }
                    //向你自己傳送一條資料
                    $connection->send(json_encode($return_data));
                }else{
                    //群聊
                    $return_data=array(
                        "type"=>"say",
                        "from_uid"=>$connection->uid,
                        "from_user_name"=>$user_info['user_name'],
                        "from_user_header"=>$user_info['user_header'],
                        "to_uid"=>"all",
                        "content"=>nl2br(htmlspecialchars($data_info['content'])),
                        "send_time"=>date("Y-m-d H:i")
                    );
                    //向所有使用者傳送資料
                    foreach($text_worker->connections as $conn){
                        $conn->send(json_encode($return_data));
                    }
                }
                return;
            case "pong":
                return;
        }
    }

    //當客戶端斷開時,廣播給所有客戶端
    public function handle_close($connection){
        global $text_worker;
        $user_name=$text_worker->uidInfo[$connection->uid]['user_name'] ?? "";
        unset($text_worker->uidConnections["{$connection->uid}"]);
        unset($text_worker->uidInfo["{$connection->uid}"]);
        if(!empty($user_name)){
            $return_data=array(
                "type"=>"logout",
                "uid"=>$connection->uid,
                "user_name"=>$user_name,
                "create_time"=>date("Y-m-d H:i:s"),
            );
            foreach($text_worker->connections as $conn){
                $conn->send(json_encode($return_data));
            }
        }

    }

}

該程式碼是我這邊自己的程式碼,有些東西沒有貼出來,只做參考,比如怎麼實現維持心跳和分配使用者唯一id等。

開啟composer.json檔案增加一段"app/Handler"於classmap中,下面是我的部分內容:

...
    "autoload": {
        "classmap": [
            "database/seeds",
            "database/factories",
            "app/Handler" 
        ],
        "psr-4": {
            "App\\": "app/"
        }
    },
...

執行命令: composer dump-autoload

執行命令啟動workerman

php artisan Workerman start --daemonize

再說一遍:app\Console\Commands\Workerman.php 裡的程式碼 $arg = $this->argument('action'); $argv [1] = $arg; 如果這段程式碼不寫那麼就無法啟動服務會報Usage: php yourfile.php {start|stop|restart|reload|status|connections} [-d]

原因: 參考接收位置錯誤. 程式碼位置: \vendor\workerman\workerman\Worker.php 673 684, 問題位置 673行中, $argv 這個全域性變數取的key的位置錯亂的.

如果你是debug模式啟動workerman的話,就把app\Console\Commands\Workerman.php 裡的 關於daemonize的程式碼刪掉,啟動命令就是php artisan Workerman start

參考資料

laravel使用Workerman搭建簡易聊天室

laravel-workerman簡易聊天室

可以參觀一下 我的部落格

本作品採用《CC 協議》,轉載必須註明作者和本文連結
我的部落格:www.zhangkaixing.com

相關文章