[记录]利用workerman在laravel中实现网页聊天室

github地址:github.com/buckychen/larachat

1.安装扩展包

首先引入workerman/gateway-worker扩展包

 $ composer require workerman/gateway-worker

2.创建启动命令文件

在 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

3.创建事件监听Events文件

创建一个 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));
        }
    }

}

4.创建前端文件

在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');
});

5. 启动 Workerman 服务端

在命令行中执行

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

6.最后通过上面注册的路由访问



参考文章:博客:在 Laravel 中使用 Workerman 进行 socket 通讯

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 1年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 6

咦,貌似看到了我贡献的代码。。。

2年前 评论
BUCKYCHEN (楼主) 2年前

前端样式怎么加载不出来

1年前 评论
BUCKYCHEN (楼主) 1年前

請問上面的 css 是有哪裏可以下載?

1年前 评论
BUCKYCHEN (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!