[記錄]利用workerman在laravel中實現網頁聊天室

BUCKYCHEN發表於2021-04-19

github地址:github.com/buckychen/larachat

首先引入workerman/gateway-worker擴充套件包

 $ composer require workerman/gateway-worker

在 app/Console/Commands 目錄下建立命令列檔案。

<?php

namespace App\Console\Commands;

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

class WorkermanCommand extends Command
{

    protected $signature = 'workman {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();
        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');
    }
}

如果專案環境是在win中執行的話需要對啟動檔案進行修改,並且在專案根目錄中建立一個start_for_win.bat檔案一次性執行多個協議

<?php

namespace App\Console\Commands;

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

class WorkermanCommand extends Command{

    //相容win
    protected $signature = 'workerman
                            {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');
    }
}

start-for_win.bat 檔案中寫入

start /b php artisan workerman single --start=register
start /b php artisan workerman single --start=gateway
start /b php artisan workerman single --start=worker
pause

建立一個 app/Workerman/Events.php 檔案來監聽處理 workman 的各種事件。

<?php

namespace App\Workerman;

use GatewayWorker\Lib\Gateway;

class Events
{

    public static function onWorkerStart($businessWorker)
    {
        echo "BusinessWorker    Start\n";
    }

    public static function onConnect($client_id)
    {
        Gateway::sendToClient($client_id,json_encode(['type' => 'init','client_id' => $client_id]));
    }

    public static function onWebSocketConnect($client_id, $data)
    {

    }

    public static function onMessage($client_id, $message)
    {
        // debug
        echo "client:{$_SERVER['REMOTE_ADDR']}:{$_SERVER['REMOTE_PORT']} gateway:{$_SERVER['GATEWAY_ADDR']}:{$_SERVER['GATEWAY_PORT']}  client_id:$client_id session:".json_encode($_SESSION)." onMessage:".$message."\n";

        // 客戶端傳遞的是json資料
        $message_data = json_decode($message, true);
        if(!$message_data)
        {
            return ;
        }

        switch ($message_data['type']){
            // 客戶端回應服務端的心跳
            case 'pong':
                return;
            // 客戶端登入 message格式: {type:login, name:xx, room_id:1} ,新增到客戶端,廣播給所有客戶端xx進入聊天室
            case 'login':
                if(!isset($message_data['room_id'])){
                    throw new \Exception("\$message_data['room_id'] not set. client_ip:{$_SERVER['REMOTE_ADDR']} \$message:$message");
                }

                // 把房間號暱稱放到session中
                $room_id = $message_data['room_id'];
                $client_name = htmlspecialchars($message_data['client_name']);
                $_SESSION['room_id'] = $room_id;
                $_SESSION['client_name'] = $client_name;

                //獲取房間內使用者
                $clients_list = Gateway::getClientSessionsByGroup($room_id);
                foreach ($clients_list as $tmp_client_id => $item){
                    $clients_list[$tmp_client_id] = $item['client_name'];
                }
                $clients_list[$client_id] = $client_name;
                $new_message = array('type' => $message_data['type'],'client_id' => $client_id,'client_name' => htmlspecialchars($client_name),'time' => date('Y-m-d H:i:s'));
                //給房間內使用者傳送資訊
                Gateway::sendToGroup($room_id, json_encode($new_message));
                //當前使用者加入到房間內
                Gateway::joinGroup($client_id, $room_id);

                // 給當前使用者傳送使用者列表
                $new_message['client_list'] = $clients_list;
                Gateway::sendToCurrentClient(json_encode($new_message));
                return;
            case 'say':
                if(!isset($_SESSION['room_id']))
                {
                    throw new \Exception("\$_SESSION['room_id'] not set. client_ip:{$_SERVER['REMOTE_ADDR']}");
                }

                $room_id = $_SESSION['room_id'];
                $client_name = $_SESSION['client_name'];

                if($message_data['to_client_id'] != 'all'){
                    $new_message = array(
                        'type' => 'say',
                        'from_client_id' => $client_id,
                        'from_client_name' => $client_name,
                        'to_client_id' => $message_data['to_client_id'],
                        'content' => "<b>對你說: </b>".nl2br(htmlspecialchars($message_data['content'])),
                        'time' => date('Y-m-d H:i:s'),
                    );
                    Gateway::sendToClient($message_data['to_client_id'],json_encode($new_message));
                    $new_message['content'] = "<b>你對".htmlspecialchars($message_data['to_client_name'])."說: </b>".nl2br(htmlspecialchars($message_data['content']));
                    return Gateway::sendToCurrentClient(json_encode($new_message));

                }

                $new_message = array(
                    'type'=>'say',
                    'from_client_id'=>$client_id,
                    'from_client_name' =>$client_name,
                    'to_client_id'=>'all',
                    'content'=>nl2br(htmlspecialchars($message_data['content'])),
                    'time'=>date('Y-m-d H:i:s'),
                );
                return Gateway::sendToGroup($room_id ,json_encode($new_message));
        }
    }

    public static function onClose($client_id)
    {
        // debug
        echo "client:{$_SERVER['REMOTE_ADDR']}:{$_SERVER['REMOTE_PORT']} gateway:{$_SERVER['GATEWAY_ADDR']}:{$_SERVER['GATEWAY_PORT']}  client_id:$client_id onClose:''\n";
        if(isset($_SESSION['room_id'])){
            $room_id = $_SESSION['room_id'];

            $new_message = array(
                'type' => 'logout',
                'from_client_id' => $client_id,
                'from_client_name' => $_SESSION['client_name'],
                'time' => date('Y-m-d H:i:s'),
            );

            Gateway::sendToGroup($room_id,json_encode($new_message));
        }
    }

}

在resources/views目錄中中建立chatList.blade.php檔案

<html>
<head>
  <meta charset="UTF-8">
  <title>聊天室</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
    <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css'>
    <link rel="stylesheet" href="{{ asset('css/styleChat.css') }}">
    <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('css/jquery-sinaEmotion-2.1.0.min.css') }}" rel="stylesheet">

    <script type="text/javascript" src="{{ asset('js/swfobject.js') }}"></script>
    <script type="text/javascript" src="{{ asset('js/web_socket.js') }}"></script>
    <script type="text/javascript" src="{{ asset('js/jquery.min.js') }}"></script>
    <script type="text/javascript" src="{{ asset('js/jquery-sinaEmotion-2.1.0.min.js') }}"></script>

    <script type="text/javascript">
        if (typeof console == "undefined") {    this.console = { log: function (msg) {  } };}
        // 如果瀏覽器不支援websocket,會使用這個flash自動模擬websocket協議,此過程對開發者透明
        WEB_SOCKET_SWF_LOCATION = "{{ asset('swf/WebSocketMain.swf') }}";
        // 開啟flash的websocket debug
        WEB_SOCKET_DEBUG = true;
        var ws, name, client_list={}, client_id, send_to_user_client_id='all',send_to_username='all';


        function connect(){
            ws = new WebSocket("ws://"+document.domain+":2346");
            ws.onopen = onopen;
            // 當有訊息時根據訊息型別顯示不同資訊
            ws.onmessage = onmessage;
            ws.onclose = function() {
                console.log("連線關閉,定時重連");
                connect();
            };
            ws.onerror = function() {
                console.log("出現錯誤");
            };
        }

        function onopen(){
            if(!name){
                show_prompt();
            }
            // 登入
            var login_data = '{"type":"login","client_name":"'+name.replace(/"/g, '\\"')+'","room_id":"<?php echo isset($_GET['room_id']) ? $_GET['room_id'] : 1?>"}';
            console.log("websocket握手成功,傳送登入資料:"+login_data);
            ws.send(login_data);
            $("#user_name").text(name);
        }

        function onmessage(e){
            console.log(e.data);
            var data = JSON.parse(e.data);

            switch (data['type']){
                //服務端ping客戶端
                case 'ping':
                    ws.send('{"type":"pong"}');
                    break;
                case 'login':
                    say(data['client_id'], data['client_name'], data['client_name']+' 加入了聊天室', data['time']);
                    if(data['client_list']){
                        client_list = data['client_list'];
                    }else{
                        client_list[data['client_id']] = data['client_name'];
                    }
                    flush_client_list();
                    console.log(data['client_name']+"登入成功");
                    break;
                case 'say':
                    say(data['from_client_id'], data['from_client_name'], data['content'], data['time']);
                    break;
                case 'logout':
                    say(data['from_client_id'],data['from_client_name'],data['from_client_name']+' 退出了',data['time']);
                    delete client_list[data['from_client_id']];
                    flush_client_list();
                    break;
                case 'init':
                    client_id = data['client_id'];
                    break;
            }
        }

        // 輸入姓名
        function show_prompt(){
            name = prompt('輸入你的名字:', '');
            if(!name || name=='null'){
                name = '遊客';
            }
        }

        function say(from_client_id,from_client_name,content,time){
            // $("#msg_list_ul").append(
            //     '<li class="clearfix"><div class="message-data align-right"><span class="message-data-time" >'+time+'</span> &nbsp; &nbsp;<span class="message-data-name" >'+from_client_name+'</span> <i class="fa fa-circle me"></i></div><div class="message other-message float-right">'+content+'</div></li>'
            // )
            if(from_client_id == client_id){
                $("#msg_list_ul").append(
                    '<li class="clearfix"><div class="message-data align-right"><span class="message-data-time" >'+time+'</span> &nbsp; &nbsp;<span class="message-data-name" >'+from_client_name+'</span> <i class="fa fa-circle me"></i></div><div class="message other-message float-right">'+content+'</div></li>'
                )
            }else{
                $("#msg_list_ul").append(
                    '<li><div class="message-data"><span class="message-data-name"><i class="fa fa-circle online"></i>'+from_client_name+'</span><span class="message-data-time">'+time+'</span></div><div class="message my-message">'+content+'</div></li>'
                )
            }

            document.getElementById("chat-history-div").scrollTop=document.getElementById("chat-history-div").scrollHeight;

        }

        function flush_client_list(){
            var client_list_ul = $("#client_list_ul");
            var img = '{{ asset("img/t1.png") }}';
            client_list_ul.empty();
            client_list_ul.append(
                '<li class="clearfix" onclick="click_user(this)" id="all"><img src="'+img+'" alt="avatar" /><div class="about"><div class="name">all</div><div class="status"><i class="fa fa-circle online"></i> online</div></div></li>'
            );
            for(var p in client_list){
                //client_list_ul.append('<li id="'+p+'">'+client_list[p]+'</li>')
                client_list_ul.append(
                    '<li class="clearfix" onclick="click_user(this)" id="'+p+'"><img src="'+img+'" alt="avatar" /><div class="about"><div class="name">'+client_list[p]+'</div><div class="status"><i class="fa fa-circle online"></i> online </div></div></li>'
                );
            }
        }

        function submit(){
            var input = $("#message-to-send");
            var input_text = input.val();
            var to_client_id = send_to_user_client_id;
            var to_client_name = send_to_username;
            ws.send('{"type":"say","to_client_id":"'+to_client_id+'","to_client_name":"'+to_client_name+'","content":"'+input_text.replace(/"/g, '\\"').replace(/\n/g,'\\n').replace(/\r/g, '\\r')+'"}');

            input.val('');
            input.focus();
        }

        function click_user(e){
            var user_client_id = $(e).attr('id');
            var user_name = $(e).children('div').children('.name').text();

            if(client_id == user_client_id){
                return;
            }
            send_to_user_client_id = user_client_id;
            send_to_username = user_name;

            $("#message-to-send").attr('placeholder','send to '+user_name);

        }
    </script>
</head>
<body onload="connect()">
<!-- partial:index.partial.html -->
  <div class="container clearfix">
    <div class="people-list" id="people-list">
      <div class="search">
        <input type="text" placeholder="search" />
        <i class="fa fa-search"></i>
      </div>
{{--        線上使用者列表--}}
      <ul class="list" id="client_list_ul">
{{--        <li class="clearfix" onclick="click_user(this)" id="all">--}}
{{--          <img src="{{ asset('img/t1.png') }}" alt="avatar" />--}}
{{--          <div class="about">--}}
{{--            <div class="name">all</div>--}}
{{--            <div class="status">--}}
{{--              <i class="fa fa-circle online"></i> online--}}
{{--            </div>--}}
{{--          </div>--}}
{{--        </li>--}}
      </ul>
    </div>

    <div class="chat">
{{--        使用者資訊--}}
      <div class="chat-header clearfix">
        <img src="{{ asset('img/t1.png') }}" alt="avatar" />

        <div class="chat-about">
          <div class="chat-with" id="user_name"></div>
          <div class="chat-num-messages">already 1 902 messages</div>
        </div>
        <i class="fa fa-star"></i>
      </div> <!-- end chat-header -->
{{--        聊天列表--}}
      <div class="chat-history" id="chat-history-div">
        <ul id="msg_list_ul">
{{--          <li class="clearfix">--}}
{{--            <div class="message-data align-right">--}}
{{--              <span class="message-data-time" >10:10 AM, Today</span> &nbsp; &nbsp;--}}
{{--              <span class="message-data-name" >Olia</span> <i class="fa fa-circle me"></i>--}}

{{--            </div>--}}
{{--            <div class="message other-message float-right">--}}
{{--              Hi Vincent, how are you? How is the project coming along?--}}
{{--            </div>--}}
{{--          </li>--}}

{{--            正在輸入--}}
{{--          <li>--}}
{{--            <div class="message-data">--}}
{{--              <span class="message-data-name"><i class="fa fa-circle online"></i> Vincent</span>--}}
{{--              <span class="message-data-time">10:31 AM, Today</span>--}}
{{--            </div>--}}
{{--            <i class="fa fa-circle online"></i>--}}
{{--            <i class="fa fa-circle online" style="color: #AED2A6"></i>--}}
{{--            <i class="fa fa-circle online" style="color:#DAE9DA"></i>--}}
{{--          </li>--}}

        </ul>

      </div> <!-- end chat-history -->

      <div class="chat-message clearfix" id="msg_text">
          <a href="{{url('test/chat?room_id=1')}}">房間1</a> &nbsp; <a href="{{url('test/chat?room_id=2')}}">房間2</a> &nbsp; <a href="{{url('test/chat?room_id=3')}}">房間3</a>
          <textarea name="message-to-send" id="message-to-send" placeholder ="" rows="3"></textarea>

        <i class="fa fa-file-o"></i> &nbsp;&nbsp;&nbsp;
        <i class="fa fa-file-image-o"></i>

        <button onclick="submit()">傳送</button>

      </div> <!-- end chat-message -->

    </div> <!-- end chat -->

  </div> <!-- end container -->

<!-- partial -->
<script src='{{ asset('js/list.min.js') }}'></script>

<script type="text/javascript">
    // 動態自適應螢幕
    document.write('<meta name="viewport" content="width=device-width,initial-scale=1">');
    $("#message-to-send").on("keydown", function(e) {
        // 按enter鍵自動提交
        if(e.keyCode === 13 && !e.ctrlKey) {
            e.preventDefault();
            submit();
            return false;
        }

        // 按ctrl+enter組合鍵換行
        if(e.keyCode === 13 && e.ctrlKey) {
            $(this).val(function(i,val){
                return val + "\n";
            });
        }
    });
</script>

</body>
</html>

並在routes/web.php路由檔案中註冊路由,訪問該前端檔案

Route::get('/test/chat',function (){
    return view('chatList');
});

在命令列中執行

php artisan workman start -d

如在win環境中,雙擊建立的start_for_win.bat檔案啟動服務
如果顯示下面的結果,那麼workerman就已經啟動成功了

----------------------- WORKERMAN -----------------------------
Workerman version:4.0.19          PHP version:7.4.3
------------------------ WORKERS -------------------------------
worker               listen                              processes status
Register             text://0.0.0.0:1236                 1         [ok]
----------------------- WORKERMAN -----------------------------
Workerman version:4.0.19          PHP version:7.4.3
------------------------ WORKERS -------------------------------
worker               listen                              processes status
Gateway              websocket://0.0.0.0:2346            1         [ok]
----------------------- WORKERMAN -----------------------------
Workerman version:4.0.19          PHP version:7.4.3
------------------------ WORKERS -------------------------------
worker               listen                              processes status
BusinessWorker       none                                1         [ok]
BusinessWorker    Start



參考文章:部落格:在 Laravel 中使用 Workerman 進行 socket 通訊

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

相關文章