laravel+gatewayworker(workerman)+vue实现用户聊天功能

933 阅读3分钟

下载laravel composer create-project --prefer-dist laravel/laravel laravel_worker "5.8.*"

下载gatewayworker www.workerman.net/download/Ga…

将下载下来的项目放到该路径下

下载gatewayclient

composer require workerman/gatewayclient

cnpm install

cnpm install

image.png

监听vue文件变化

npm run watch

可能会出现下面的错误

image.png

此时需要将node_modules目录删除

rm -rf node_modules

运行

npm install vue-template-compiler --save-dev --production=false
cnpm install
npm run watch

image.png

数据库配置

创建数据库

image.png

配置.env

vim .env
#修改下面的值
DB_DATABASE=laravel_worker
DB_USERNAME=root
DB_PASSWORD=root

数据表迁移

php artisan migrate

用户

执行

php artisan make:auth

新增用户

新增三个用户aaaa bbbb cccc

浏览器输入地址:http://192.168.240.131:84/register

image.png

laravel_worker\app\User.php 新增下面代码

/**
 * 用户头像
 * @return string
 */
 public function avatar()
 {
     return 'https://source.unsplash.com/user/erondu/50x50';
 }

新建信息表

php artisan make:model Message -m

laravel_worker\database\migrations\2021_06_08_081347_create_messages_table.php 新增下面代码

public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->integer('user_id');
            $table->integer('room_id')->nullable();
            $table->text('content');
            $table->timestamps();
        });
    }

执行迁移命令

php artisan migrate

用户进入聊天室

ChatRoom聊天界面

laravel_worker\resources\js\components目录下,新增ChatRoom.vue文件,代码如下

<template>
    <div class="container">
        <a href="?room_id=1" class="btn btn-danger">音乐</a>
        <a href="?room_id=2" class="btn btn-primary">游戏</a>
        <hr class="divider">

        <div class="row">
            <div class="col-md-8">
                <div class="panel panel-default">
                    <div class="panel-heading">聊天室</div>
                    <div class="panel-body">
                        <div class="messages">
                            <div class="media">
                                <div class="media-left">
                                    <a href="#">
                                        <img class="media-object img-circle">
                                    </a>
                                </div>
                                <div class="media-body">
                                    <p class="time">time</p>
                                    <h4 class="media-heading">name</h4>
                                    : content
                                </div>
                            </div>
                        </div>

                    </div>
                </div>
            </div>

            <div class="col-md-4">
                <div class="panel panel-default">
                    <div class="panel-heading">聊天室在线用户</div>

                    <div class="panel-body">
                        <ul class="list-group">
                            <li class="list-group-item">
                                <img class="img-circle">
                                user.name
                            </li>
                        </ul>
                    </div>

                </div>
            </div>
        </div>

        <hr class="divider">

        <form @submit.prevent="onSubmit">
            <div class="form-group">
                <label for="user_id">发送给</label>

                <select class="form-control" id="user_id">
                    <option value="">所有人</option>
                    <option>user.name</option>
                </select>
            </div>

            <div class="form-group">
                <label for="content">内容(回车可快速发送)</label>
                <textarea class="form-control" rows="3" id="content"></textarea>
            </div>

            <button type="submit" class="btn btn-dark">提交</button>
        </form>
    </div>
</template>

<script>

    export default {
        data() {
            return {}
        },
        created: function () {

        },
        methods: {}
    }
</script>


<style scoped>
    .panel-body {
        height: 480px;
        overflow: auto;
    }

    .media-object.img-circle {
        width: 64px;
        height: 64px;
    }

    .img-circle {
        width: 48px;
        height: 48px;
    }

    .time {
        float: right;
    }

    .media {
        margin-top: 24px;
    }
</style>

D:\phpstudy\WWW\laravel_worker\resources\js\app.js 新增代码

// 注册组件
Vue.component('chat-room', require('./components/ChatRoom.vue').default);

D:\phpstudy\WWW\laravel_worker\resources\views\home.blade.php 代码

@extends('layouts.app')

@section('content')
    <chat-room></chat-room>
@endsection

打开浏览器

http://192.168.240.131:84/home

image.png

修改socket协议

D:\phpstudy\WWW\laravel_worker\socket\GatewayWorker\Applications\YourApp\start_gateway.php 替换代码

$gateway = new Gateway("websocket://0.0.0.0:8282");

修改Events.php

D:\phpstudy\WWW\laravel_worker\socket\GatewayWorker\Applications\YourApp\Events.php

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

启动gatewayworker

cd socket/GatewayWorker
php start.php start

image.png

客户端建立websocket连接

<script>
    let ws = new WebSocket('ws://192.168.240.131:8282')

    export default {
        data() {
            return {}
        },
        created: function () {
            ws.onmessage = (e) => {
                let data = JSON.parse(e.data)
                console.log(data)
            }
        },
        methods: {}
    }
</script>

浏览器控制台调试输出

{type: "init", client_id: "7f0000010b5500000002"}

进入聊天室

前端代码

<template>
    <div class="container">
        <a href="?room_id=1" class="btn btn-danger">音乐</a>
        <a href="?room_id=2" class="btn btn-primary">游戏</a>
        <hr class="divider">

        <div class="row">
            <div class="col-md-8">
                <div class="panel panel-default">
                    <div class="panel-heading">聊天室</div>
                    <div class="panel-body">
                        <div class="messages">
                            <div class="media" v-for="message in messages">
                                <div class="media-left">
                                    <a href="#">
                                        <img class="media-object img-circle" :src="message.avatar">
                                    </a>
                                </div>
                                <div class="media-body">
                                    <p class="time">{{message.time}}</p>
                                    <h4 class="media-heading">{{message.name}}</h4>
                                    : {{message.content}}
                                </div>
                            </div>
                        </div>

                    </div>
                </div>
            </div>

            <div class="col-md-4">
                <div class="panel panel-default">
                    <div class="panel-heading">聊天室在线用户</div>

                    <div class="panel-body">
                        <ul class="list-group">
                            <li class="list-group-item">
                                <img class="img-circle">
                                user.name
                            </li>
                        </ul>
                    </div>

                </div>
            </div>
        </div>

        <hr class="divider">

        <form @submit.prevent="onSubmit">
            <div class="form-group">
                <label for="user_id">发送给</label>

                <select class="form-control" id="user_id">
                    <option value="">所有人</option>
                    <option>user.name</option>
                </select>
            </div>

            <div class="form-group">
                <label for="content">内容(回车可快速发送)</label>
                <textarea class="form-control" rows="3" id="content"></textarea>
            </div>

            <button type="submit" class="btn btn-dark">提交</button>
        </form>
    </div>
</template>

<script>
    let ws = new WebSocket('ws://192.168.240.131:8282')

    export default {
        data() {
            return {
                'messages': []
            }
        },
        created: function () {
            ws.onmessage = (e) => {
                let data = JSON.parse(e.data)
                console.log(data)

                //如果没有类型,就为空
                let type = data.type || ''

                switch (type) {
                    case "init":
                        axios.post('/init', {client_id: data.client_id})
                        break;
                    case "say":
                        this.messages.push(data.data)
                        break;
                    default:
                        console.log(data)
                }
            }
        },
        methods: {}
    }
</script>


<style scoped>
    .panel-body {
        height: 480px;
        overflow: auto;
    }

    .media-object.img-circle {
        width: 64px;
        height: 64px;
    }

    .img-circle {
        width: 48px;
        height: 48px;
    }

    .time {
        float: right;
    }

    .media {
        margin-top: 24px;
    }
</style>

后端代码

D:\phpstudy\WWW\laravel_worker\routes\web.php 新增代码

Route::post('/init', 'HomeController@init'); 

D:\phpstudy\WWW\laravel_worker\app\Http\Controllers\HomeController.php

<?php

namespace App\Http\Controllers;

use GatewayClient\Gateway;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
        Gateway::$registerAddress = '127.0.0.1:1238';
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }

    public function init(Request $request)
    {
        //绑定用户
        $this->bind($request);

        //进入聊天室
        $this->login();
    }

    public function login()
    {
        $data = [
            'type' => 'say',
            'data' => [
                'avatar' => Auth::user()->avatar(),
                'name' => Auth::user()->name,
                'content' => '进入了聊天室',
                'time' => date('Y-m-d H:i:s')
            ]
        ];

        Gateway::sendToAll(json_encode($data));
    }

    public function bind($request)
    {
        $id = Auth::id();
        $client_id = $request->client_id;
        Gateway::bindUid($client_id, $id);
    }


}

发送聊天信息

前端代码

<template>
    <div class="container">
        <a href="?room_id=1" class="btn btn-danger">音乐</a>
        <a href="?room_id=2" class="btn btn-primary">游戏</a>
        <hr class="divider">

        <div class="row">
            <div class="col-md-8">
                <div class="panel panel-default">
                    <div class="panel-heading">聊天室</div>
                    <div class="panel-body">
                        <div class="messages">
                            <div class="media" v-for="message in messages">
                                <div class="media-left">
                                    <a href="#">
                                        <img class="media-object img-circle" :src="message.avatar">
                                    </a>
                                </div>
                                <div class="media-body">
                                    <p class="time">{{message.time}}</p>
                                    <h4 class="media-heading">{{message.name}}</h4>
                                    : {{message.content}}
                                </div>
                            </div>
                        </div>

                    </div>
                </div>
            </div>

            <div class="col-md-4">
                <div class="panel panel-default">
                    <div class="panel-heading">聊天室在线用户</div>

                    <div class="panel-body">
                        <ul class="list-group">
                            <li class="list-group-item">
                                <img class="img-circle">
                                user.name
                            </li>
                        </ul>
                    </div>

                </div>
            </div>
        </div>

        <hr class="divider">

        <form @submit.prevent="onSubmit">
            <div class="form-group">
                <label for="user_id">发送给</label>

                <select class="form-control" id="user_id">
                    <option value="">所有人</option>
                    <option>user.name</option>
                </select>
            </div>

            <div class="form-group">
                <label for="content">内容(回车可快速发送)</label>
                <textarea class="form-control" rows="3" id="content" v-model="content"></textarea>
            </div>

            <button type="submit" class="btn btn-dark">提交</button>
        </form>
    </div>
</template>

<script>
    let ws = new WebSocket('ws://192.168.240.131:8282')

    export default {
        data() {
            return {
                'messages': [],
                'content': '',
            }
        },
        created: function () {
            ws.onmessage = (e) => {
                let data = JSON.parse(e.data)
                console.log(data)

                //如果没有类型,就为空
                let type = data.type || ''

                switch (type) {
                    case "init":
                        axios.post('/init', {client_id: data.client_id})
                        break;
                    case "say":
                        this.messages.push(data.data)
                        this.$nextTick(function () {
                            $('.panel-body').animate({scrollTop: $('.messages').height()})
                        })
                        break;
                    default:
                        console.log(data)
                }
            }
        },
        methods: {
            onSubmit() {
                axios.post('/say', {content: this.content})
                this.content = ''
            },
        }
    }
</script>


<style scoped>
    .panel-body {
        height: 480px;
        overflow: auto;
    }

    .media-object.img-circle {
        width: 64px;
        height: 64px;
    }

    .img-circle {
        width: 48px;
        height: 48px;
    }

    .time {
        float: right;
    }

    .media {
        margin-top: 24px;
    }
</style>

后端代码

D:\phpstudy\WWW\laravel_worker\routes\web.php

Route::post('/say', 'HomeController@say');

D:\phpstudy\WWW\laravel_worker\app\Message.php

protected $guarded = [];

测试

打开两个浏览器

image.png

聊天历史记录

前端代码

只需要在switch中新增下面代码

case "history":
    this.messages = data.data
    break;

后端代码

D:\phpstudy\WWW\laravel_worker\app\Http\Controllers\HomeController.php

public function init(Request $request)
{
    //绑定用户
    $this->bind($request);

    //历史记录
    $this->history();

    //进入聊天室
    $this->login();
}

/**
     * 最新的5条聊天历史记录
     */
public function history()
{
    $data = ['type' => 'history'];
    $messages = Message::with('user')->orderBy('id', 'desc')->limit(5)->get();
    $data['data'] = $messages->map(function ($item, $key) {
        return [
            'avatar' => $item->user->avatar(),
            'name' => $item->user->name,
            'content' => $item->content,
            'time' => $item->created_at->format('Y-m-d H:i:s')
        ];
    });
	$data['data'] = array_reverse(json_decode($data['data'], true));
    
    Gateway::sendToUid(Auth::id(), json_encode($data));
}

D:\phpstudy\WWW\laravel_worker\app\Message.php

新增用户关联

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Message extends Model
{
    //
    protected $guarded = [];

    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

D:\phpstudy\WWW\laravel_worker\app\User.php

新增信息关联

public function messages()
{
    return $this->hasMany('App\Message');
}

在线用户列表

前端代码

<div class="col-md-4">
    <div class="panel panel-default">
        <div class="panel-heading">聊天室在线用户</div>

        <div class="panel-body">
            <ul class="list-group">
                <li class="list-group-item" v-for="user in users">
                    <img class="img-circle" :src="user.avatar">
                    {{user.name}}
                </li>
            </ul>
        </div>

    </div>
</div>
<form @submit.prevent="onSubmit">
    <div class="form-group">
        <label for="user_id">发送给</label>

<select class="form-control" id="user_id">
    <option value="">所有人</option>
<option :value="user.id" v-for="user in users">{{user.name}}</option>
</select>
</div>

<div class="form-group">
    <label for="content">内容(回车可快速发送)</label>
<textarea class="form-control" rows="3" id="content" v-model="content"></textarea>
</div>

<button type="submit" class="btn btn-dark">提交</button>
</form>
data() {
    return {
        'messages': [],
        'content': '',
        'users': []
    }
},
    
case "users":
	this.users = data.data
break;

后端代码

public function init(Request $request)
{
    //绑定用户
    $this->bind($request);

    //历史记录
    $this->history();

    //获取在线用户列表
    $this->users();

    //进入聊天室
    $this->login();
}

public function users()
{
    $data = [
        'type' => 'users',
        'data' => Gateway::getAllClientSessions()
    ];

    Gateway::sendToAll(json_encode($data));
}

public function bind($request)
{
    $id = Auth::id();
    $client_id = $request->client_id;
    Gateway::bindUid($client_id, $id);

    Gateway::setSession($client_id, [
        'id' => $id,
        'avatar' => Auth::user()->avatar(),
        'name' => Auth::user()->name,
    ]);
}

image.png

私聊

前端代码

<form @submit.prevent="onSubmit">
    <div class="form-group">
        <label for="user_id">发送给</label>

        <select class="form-control" id="user_id" v-model="user_id">
            <option value="">所有人</option>
            <option :value="user.id" v-for="user in users">{{user.name}}</option>
        </select>
    </div>

    <div class="form-group">
        <label for="content">内容(回车可快速发送)</label>
        <textarea class="form-control" rows="3" id="content" v-model="content"></textarea>
    </div>

    <button type="submit" class="btn btn-dark">提交</button>
</form>
data() {
    return {
        'messages': [],
        'content': '',
        'users': [],
        'user_id': '',
    }
},
    
 methods: {
     onSubmit() {
         axios.post('/say', {content: this.content, user_id: this.user_id})
         this.content = ''
     },
 }

后端代码

public function say(Request $request)
{
    //所有人
    $data = [
        'type' => 'say',
        'data' => [
            'avatar' => Auth::user()->avatar(),
            'name' => Auth::user()->name,
            'content' => $request->input('content'),
            'time' => date('Y-m-d H:i:s')
        ]
    ];

    //私聊
    if ($request->user_id) {
        $data['data']['name'] = Auth::user()->name . '对 ' . User::find($request->user_id)->name . '说:';
        Gateway::sendToUid(Auth::id(), json_encode($data));
        Gateway::sendToUid($request->user_id, json_encode($data));

        //私聊信息,只发给对应用户,不存数据库了
        return;
    }

    Gateway::sendToAll(json_encode($data));

    //存到数据库
    Message::create([
        'user_id' => Auth::id(),
        'content' => $request->input('content')
    ]);

}

image.png

房间

前端代码

<a href="?room_id=1" class="btn btn-danger">音乐</a>
<a href="?room_id=2" class="btn btn-primary">游戏</a>

后端代码

public function index(Request $request)
{
    $room_id = $request->room_id ? $request->room_id : '1';
    session()->put('room_id', $room_id);

    return view('home');
}
public function bind($request)
{
    $id = Auth::id();
    $client_id = $request->client_id;
    Gateway::bindUid($client_id, $id);
    Gateway::joinGroup($client_id, session('room_id'));

    Gateway::setSession($client_id, [
        'id' => $id,
        'avatar' => Auth::user()->avatar(),
        'name' => Auth::user()->name,
    ]);
}
public function say(Request $request)
{
    //存到数据库
    Message::create([
        'user_id' => Auth::id(),
        'room_id' => session('room_id'),
        'content' => $request->input('content')
    ]);

}
//将所有
Gateway::sendToAll(json_encode($data));
//改为
Gateway::sendToGroup(session('room_id'), json_encode($data));

用户退出

前端代码

case "logout":
	this.$delete(this.users, data.client_id)
break;

后端代码

D:\phpstudy\WWW\laravel_worker\socket\GatewayWorker\Applications\YourApp\Events.php

改完需要重新启动socket服务


/**
    * 当用户断开连接时触发
    * @param int $client_id 连接id
    */
public static function onClose($client_id)
{

    // 向所有人发送
    GateWay::sendToAll(json_encode([
        'type' => 'logout',
        'client_id' => $client_id
    ]));
}

心跳

前端代码

case "ping":
    ws.send('pong');
    console.log(data)
break;

后端代码

D:\phpstudy\WWW\laravel_worker\socket\GatewayWorker\Applications\YourApp\start_gateway.php

改完需要重新启动socket服务

// 心跳间隔
$gateway->pingInterval = 10;
// 心跳数据
$gateway->pingData = '{"type":"ping"}';