php workerman 即时通讯聊天系统

寻技术 PHP编程 2024年01月21日 162

项目地址

HTTP

http协议

  • 超文本传输协议
  • 无状态协议
  • 基于tcp协议的一个应用层的协议
  • http是单向的,浏览器发起向服务器的连接,服务器预先并不知道

Alt text

http协议工作过程

  • 客户端和服务端建立连接(三次握手),http开始工作
  • 建立连接后客户端发送给请求服务器
  • 服务器接受到请求后,给予相应的响应信息

WebSoket

websoket协议

  • websocket是H5提出的在单个TCP协议上进行的全双工通讯协议
  • 实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实事通讯的目的
  • WebSokcet是一个持久化的协议

工作过程

  • 客户端发送http请求,经过三次握手,建立TCP连接,在http 请求里面存放 websocket 支持的版本号信息
  • 服务器接收请求,同样以http协议回应
  • 连接成功,客户端与服务器建立持久性的连接

websocket 与 http 差异

相同点

Alt text
都是基于tcp的,都是可靠的性传输协议

不同点

  • websocket是双向通信协议,模拟socket协议,可以双向发送或接受信息
  • websocket是持久化连接,http 是短连接
  • websocket是有状态的,http 是无状态的
  • websocket 连接之后服务器和客户端可以双向发送数据,http只能是客户端发起一次请求之后,服务器才能返回数据

轮询

过程

  • 客户端发起长轮询,如果服务端的数据没有发生变化,就会 hold 住请求,知道服务端的数据发生变化
  • 优点 是解决了http不能实时更新的弊端,实现了 "伪-长连接"
  • 轮询的本质依然是 request <-> response
    Alt text

弊端

  • 推送延迟
  • 服务端压力
  • 推送延迟和服务端压力无法中和

websocket改进

Alt text

JS Websocket

简单示例

ws = new WebSocket('ws://127.0.0.1:2000');
//当 websocket 创建成功后 触发onopen事件
ws.onopen = function () {
    var data = {};
    data.type = 'login';
    //标识  客户还是客服
    data.group = 'member';
    //发送信息
    ws.send(JSON.stringify(data));
}
//收到服务端发来的消息 触发 onmessage
ws.onmessage = function (e) {
    var data = JSON.parse(e.data);
}

Workerman基础

workerman手册

安装

Composer安装:
composer require workerman/workerman

启动停止

# 以debug(调试)方式启动
php start.php start
# 以daemon(守护进程)方式启动
php start.php start -d
# 停止
php start.php stop
# 重启
php start.php restart
# 平滑重启
php start.php reload
# 查看状态
php start.php status

简单示例

实例一、使用HTTP协议对外提供Web服务

创建start.php文件

<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
require_once __DIR__ . '/vendor/autoload.php';

// 创建一个Worker监听2345端口,使用http协议通讯
$http_worker = new Worker("http://0.0.0.0:2345");

// 启动4个进程对外提供服务
$http_worker->count = 4;

// 接收到浏览器发送的数据时回复hello world给浏览器
$http_worker->onMessage = function(TcpConnection $connection, Request $request)
{
    // 向浏览器发送hello world
    $connection->send('hello world');
};

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

实例二、使用WebSocket协议对外提供服务

创建ws_test.php文件

<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';

// 注意:这里与上个例子不同,使用的是websocket协议
$ws_worker = new Worker("websocket://0.0.0.0:2000");

// 启动4个进程对外提供服务
$ws_worker->count = 4;

// 当收到客户端发来的数据后返回hello $data给客户端
$ws_worker->onMessage = function(TcpConnection $connection, $data)
{
    // 向客户端发送hello $data
    $connection->send('hello ' . $data);
};

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

测试

打开chrome浏览器,按F12打开调试控制台,在Console一栏输入(或者把下面代码放入到html页面用js运行)

// 假设服务端ip为127.0.0.1
ws = new WebSocket("ws://127.0.0.1:2000");
ws.onopen = function() {
    alert("连接成功");
    ws.send('tom');
    alert("给服务端发送一个字符串:tom");
};
ws.onmessage = function(e) {
    alert("收到服务端的消息:" + e.data);
};

TP的数据库类

composer require topthink/think-orm

ThinkPhp

安装

# 安装
composer create-project topthink/think tp
# 视图扩展
composer require topthink/think-view
# 多应用扩展
composer require topthink/think-multi-app
# 验证码扩展
composer require topthink/think-captcha

开启多应用

  1. 删除原始的 app/controller 目录
  2. 在项目跟目录下 使用命令 php think build admin 来创建应用
  3. 将全局的 configroute 复制一份到创建的应用里面
    • 开器多应用后全局的route 会失效,
    • 应用里面的config 参数 可以覆盖全选的 config参数
    • 可以针对不同的应用设置不同的配置参数和相同的配置

运行thinkphp

直接运行tp
php think run
设置端口
php think run -p 8081
访问地址
http://127.0.0.1:8000/

开启多应用后 通过 地址+应用名 +参数 来访问不同的应用
http://127.0.0.1:8000/admin
默认的应用是index可以忽略不写
http://127.0.0.1:8000/index
更多的配置查看 手册

创建对应的控制器

php think make:controller admin@Service --plain
php think make:controller admin@Service --plain

获取URL

//助手函数 返回buildUrl() 
//如果需要返回客户端 需要先强制转换为字符串类型后再返回。
url();
(string)url();
//控制器方法路径 参数
// suffix URL后缀 
// domain domain
// root  入口文件
url('index/blog/read', ['id'=>5])
    ->suffix('html')
    ->domain(true)
    ->root('/index.php');

中间件

生成命令

//多应用模式
php think make:middleware admin@Check

在 对应的应用 route/app.php文件里面注册 路由中间件

use think\facade\Route;
Route::group(function(){
    Route::get('index/index','index/index');
    Route::get('service/index','service/index');
})->middleware(\app\admin\middleware\Check::class);
Route::role('login/login','login/login','get|post');

使用验证码扩展

验证码库需要开启Session才能生效。

app/middleware.php 设置

// 全局中间件定义文件
return [
    // Session初始化
    \think\middleware\SessionInit::class
];

config/captcha.php 为验证码的配置文件

示例

<!-- 获取验证码 -->
<div>{:captcha_img()}</div>
<div><img src="{:captcha_src()}" alt="captcha" /></div>
//两种方式
//校验验证码
$this->validate($data,[
    'captcha|验证码'=>'require|captcha'
]);
if(!captcha_check($captcha)){
 // 验证失败
};

HTTP Requests for PHP

安装

文档 https://requests.ryanmccue.info/download/
composer require rmccue/requests

使用案例

$response = WpOrg\Requests\Requests::get('https://api.github.com/events');

var_dump($response->body);
// string(42865) "[{"id":"15624773365","type":"PushEvent","actor":{...
//post请求
$response = WpOrg\Requests\Requests::post('https://httpbin.org/post');

//设置请求头
$url = 'https://api.github.com/some/endpoint';
$headers = array('Content-Type' => 'application/json');
$data = array('some' => 'data');
$response = WpOrg\Requests\Requests::post($url, $headers, json_encode($data));

即时通讯聊天系统

简单的群聊功能

前端页面

聊天框内容分析
Alt text

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="/static/layui/css/layui.css">

    <title>客户端聊天窗口</title>
    <style type="text/css">
        html,
        body {
            width: 98%;
            height: 100%;
            margin: 0 auto;
            padding: 0px
        }

        .head_icon {
            display: inline-block;
            width: 50px;
            height: 50px;
            overflow: hidden;
            border-radius: 20%;
        }
        #contentor{
            overflow-y: auto; /* 垂直方向滚动 */  
            height: 500px; /* 高度自适应 */  
            width: 100%; /* 宽度自适应 */  
        }
    </style>


</head>

<body>

    <div class="layui-panel" >
        <div class="layui-row layui-col-space32
        " style="padding: 32px;">
            <div class="layui-col-xs12 ">
                <div style="border: 1px solid #f9f9f9;" id="contentor">
                </div>
            </div>
            <div class="layui-col-md12">

                <div class="layui-row">
                    <span class="layui-col-xs8">
                        <input type="text" name="send" placeholder="输入要发送的内容" class="layui-input">
                    </span>
                    <span class="layui-col-xs4">
                        <button type="button" class="layui-btn layui-bg-blue btn" style="width: 100%;">发送信息</button>
                    </span>
                </div>
            </div>
        </div>
    </div>


    <script src="/static/layui/layui.js"></script>
    <script type="text/javascript">

        layui.use(function () {
            $ = layui.jquery;
            layer = layui.layer;
            ws_connect()
            send()

        })

        //发送消息
        function send() {
            var button = $('.btn'),
                text = $('input[name="send"]');
            //发送按钮点击后
            button.click(function () {
                //给框体里面添加对应的显示代码
                // 获取输入框内容
                if (text.val() === "") {
                    layer.msg('请输入内容')
                } else {
                    data = { msg: text.val() };
                    ws.send(JSON.stringify(data));
                    data['avatarRam'] = avatarRam;

                    auto_chat(data);
                    //清空内容框
                    text.val('')
                }
            })
        }

        function ws_connect() {
            ws = new WebSocket('ws://127.0.0.1:2000');
            //当 websocket 创建成功后 触发onopen事件
            ws.onopen = function () {
                // auto_chat('你是零基础的吗','老手覅');
                // setTimeout(()=>{auto_chat('同学你好','老手覅')}, 2000)
                var data = {};
                data.type = 'login';
                //标识  客户还是客服
                data.group = 'member';
                ws.send(JSON.stringify(data));
            }
            //收到服务端发来的消息 触发 onmessage
            ws.onmessage = function (e) {
                var data = JSON.parse(e.data);
                if (data.type == 'login') {
                    avatarRam = data.avatarRam
                    return ''
                }
                auto_chat(data)
            }
        }
        //发送消息
        function auto_chat(data) {
            let html_other = `
                <div class="layui-col-md12">
                        <div class="layui-row ">
                            <div class=" layui-col-xs1" style="text-align: left;">
                                <div class="head_icon">
                                    <img src="/img/avatar${data.avatarRam}.png" alt=""
                                        style="width: 100%;height: auto;display: inline-block;">
                                </div>
                            </div>
                            <div class=" layui-col-xs11">
                                <strong>游客${data.uid}</strong>
                                <span class="layui-font-green layui-font-16"> ${getCurrentTime()} </span>
                                <br>
                                <button class="layui-btn layui-btn-radius">${data.msg}</button>
                            </div>
                        </div>
                    </div>
                `
            let html_my = `
                    <div class="layui-col-md12">
                        <div class="layui-row">
                            <div class=" layui-col-xs11 " style="text-align: right;">
                                <span style="display: inline-block;" class="layui-font-green layui-font-16">  ${getCurrentTime()}
                                </span>
                                <br>
                                <button class="layui-btn  layui-bg-blue  layui-btn-radius">${data.msg}</button>
                            </div>
                            <div class="layui-col-xs1" style="text-align: right;">
                                <div class="head_icon " style="display: inline-block;">
                                    <img src="/img/avatar${data.avatarRam}.png" alt=""
                                        style="width: 100%;height: auto;">
                                </div>
                            </div>
                        </div>
                    </div>
                    `;
            console.log(data);
            console.log(data.uid);

            //将信息添加到对应的框体内
            if (data.uid === undefined) {
                $('#contentor').append(html_my);
            } else {
                $('#contentor').append(html_other);
            }
        }
        //获取当前时间
        function getCurrentTime() {
            const now = new Date();
            const formattedTime = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`;
            return formattedTime;
        }

    </script>
</body>
</html>

workerman代码

<?php

use Workerman\Worker;
use Workerman\Connection\TcpConnection;

require_once __DIR__ . '/vendor/autoload.php';

// 注意:这里与上个例子不同,使用的是websocket协议
$ws_worker = new Worker("websocket://0.0.0.0:2000");

$global_uid = 0;
//有新的客户端与workman建立连接后
$ws_worker->onConnect = function (TcpConnection $connection) use (&$global_uid, $ws_worker) {
    //用户id 
    $connection->uid = ++$global_uid;
    //用户头像
    $connection->avatarRam = mt_rand(0,5);
};
$ws_worker->onMessage = function (TcpConnection $connection, $data) use ($ws_worker) {
    $data = json_decode($data,true);
    $data['uid'] = $connection->uid;
    $data['avatarRam'] =  $connection->avatarRam;
    //如果是login表示初次登录 返回 avatarRam  头像信息
    if($data['type']=='login'){
        $connection->send(json_encode($data));
    }
    foreach ($ws_worker->connections as $conn) {
        //除了自身之外 其他人都发送
        if ($connection->id != $conn->id) {
            //返回的信息包含id和 头像 和 接受的msg
            $conn->send(json_encode($data));
        }
    }
    // $connection->send("游客{$connection->uid}:$data");
};
// 运行worker
Worker::runAll();

游客 客服聊天

大体框架
Alt text

Alt text

游客前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>客户端</title>
    <link href="//unpkg.com/layui@2.8.17/dist/css/layui.css" rel="stylesheet">
    <style>
        .box1 {
            margin: auto;
            border: 1px solid red;
            width: 800px;
            height: 500px;
            position: relative;
            /* margin-top: 1%; */
            /* float: left; */
        }
        .member-list {
            float: left;
            background-color: #dbe4ff;
            width: 200px;
            height: 100%;
            display: inline;
        }
        .msg-container {
            float: left;
            width: 596px;
            height: 100%;
            border-color: black;
        }
        .msg-container .msg-list {
            height: 400px;
            width: 100%;
            background-color: bisque;
        }
        .msg-container .msg-send {
            height: 100px;
            background-color: black;
        }
        .member-item {
            width: 100%;
            height: 50px;
            font-size: 20px;
            /* color:rgb(22, 186, 170); */
        }
    </style>
</head>

<body>
    <div class="box1">
        <div class="member-list layui-row"> </div>
        <div class="msg-container">
            <!-- 聊天区域 -->
            <div class="msg-list ">
            </div>
            <div class="msg-send">
                <div class="layui-row">
                    <div class="layui-col-xs10">
                        <textarea name="desc" placeholder="多行文本框" class="layui-textarea"></textarea>
                    </div>
                    <div class="layui-col-xs2">
                        <button type="button" id="send" onclick="sendmsg(this)" from_id="" class="layui-btn"
                            style="width: 100%;height: 100px;">发送</button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="//unpkg.com/layui@2.8.17/dist/layui.js"></script>
    <script>
        layui.use(['layer'], function () {
            layer = layui.layer,
                $ = layui.jquery;

            //客服点击发送信息
            // $('#send').click(function(){
            //     //获取当前回复的客服id
            //     from_id=parseInt($(this).attr('from_id'));
            //     if(isNaN(from_id )){
            //         //说明为空 没有选中
            //         layer.alert('请选择一个用户');
            //         return '';
            //     }
            // });

        })

        //客服发送信息
        function sendmsg(obj) {
            touid = parseInt($(obj).attr('from_id'));
            if (isNaN(touid)) {
                //说明为空 没有选中
                return layer.alert('请选择一个用户');
            }
            //发送信息
            //获取信息
            var data = {
                type: 'msg',
                group: 'admin',
                touid: touid,
                msg: $('textarea[name="desc"]').val(),
            }
            if (data.msg.trim == '' || data.msg.length == 0) {
                return layer.alert('信息不能为空', { icon: 0 })
            }
            ws.send(JSON.stringify(data));
            $('textarea[name="desc"]').val('');
            auto_chat(data)
        }

        // 客户列表
        var userList = [];
        ws = new WebSocket('ws://127.0.0.1:2000')
        //建立连接后触发 onopen时间
        ws.onopen = function () {
            let data = {};
            //进行登录
            data.type = 'login';
            //用户标识 为客服
            data.group = 'admin';
            // 发送信息
            ws.send(JSON.stringify(data));
            init_load_user_list();
        }

        //接收到服务器发送的消息后
        ws.onmessage = function (e) {
            var data = JSON.parse(e.data);
            //拉取客户列表
            if (data.type == 'load_user_list') {
                var uList = data.userlist;
                $.each(uList, function (i, v) {
                    userList.push(v);
                })
                //构建客户列表
                get_user_list();
                return false;
            }
            //用用户退出
            if (data.type == 'logout') {
                // 判断是否在列表里面
                var index = $.inArray(data.disc_id, userList);
                if (index > -1) {
                    $('#' + data.disc_id).remove();
                    $('#member_' + data.disc_id).remove();
                    if ($('#send').attr('from_id') == data.disc_id) {
                        $('#send').attr('from_id', '')
                    }
                }
            }
            //有新用户来
            if (data.type == 'login') {
                //如果没有找到 说明没有这个用户的信息
                if ($.inArray(data.from_id, userList) == -1) {
                    userList.push(data.from_id)
                }
                get_user_list();
                return;
            }
            //收到新的消息
            if (data.type == 'msg') {
                //将信息显示到对应的框里面
                // var member_id = data.from_id;

                // 获取到对应的对话框
                // $('#member_'+member_id).
                data.avatarRam = Math.ceil(5);
                auto_chat(data)
            }
        }
        //初始化拉取客户列表
        function init_load_user_list() {
            $data = {
                type: 'load_user_list',
                group: 'admin'
            };
            ws.send(JSON.stringify($data))
        }
        //部署客户端客户列表ui 并初始化对应的聊天框
        function get_user_list() {
            var html = '';
            $.each(userList, function (i, v) {
                html += `<div class="member-item layui-col-xs12 layui-btn layui-btn-primary layui-btn-fluid " style="margin:0" id="${v}" onclick="checkme(this)" member_id="${v}">客户${v}</div>`;
                var htmlmsg = `<div id="member_${v}" class="layui-row" style="display: none;"> </div>`;
                $('.msg-list').append(htmlmsg);

            });
            $('.member-list').html(html);
            //
        }
        // 和单独某个用户聊天
        function checkme(obj) {
            $(obj).removeClass('layui-btn-primary').siblings('div').addClass('layui-btn-primary');
            var member_id = $(obj).attr('member_id');
            //创建对应的内容显示体
            //如果等于0 说明不存在 进行创建 并且设置为显示
            console.log($(`#member_${member_id}`).length);
            if ($(`#member_${member_id}`).length <= 0) {
                var htmlmsg = `<div id="member_${member_id}" class="layui-row" > <div>`;
                $('.msg-list').append(htmlmsg);
            }

            $(`#member_${member_id}`).show().siblings().hide();
            $('#send').attr('from_id', member_id);
        }

        //发送消息
        function auto_chat(data) {
            let html_other = `
                <div class="layui-col-md12">
                        <div class="layui-row ">
                            <div class=" layui-col-xs1" style="text-align: left;">
                                <div class="head_icon">
                                    <img src="/img/avatar${data.avatarRam}.png" alt=""
                                        style="width: 100%;height: auto;display: inline-block;">
                                </div>
                            </div>
                            <div class=" layui-col-xs11">
                                <strong>用户${data.from_id}</strong>
                                <span class="layui-font-green layui-font-16"> ${getCurrentTime()} </span>
                                <br>
                                <button class="layui-btn layui-btn-radius">${data.msg}</button>
                            </div>
                        </div>
                    </div>
                `
            let html_my = `
                    <div class="layui-col-md12">
                        <div class="layui-row">
                            <div class=" layui-col-xs11 " style="text-align: right;">
                                <span style="display: inline-block;" class="layui-font-green layui-font-16">  ${getCurrentTime()}
                                </span>
                                <br>
                                <button class="layui-btn  layui-bg-blue  layui-btn-radius">${data.msg}</button>
                            </div>
                            <div class="layui-col-xs1" style="text-align: right;">
                                <div class="head_icon " style="display: inline-block;">
                                    <img src="https://pic.qqtn.com/up/2017-12/15132234795879682.jpg" alt=""
                                        style="width: 100%;height: auto;">
                                </div>
                            </div>
                        </div>
                    </div>
                    `;
            //将信息添加到对应的框体内
            //如果  group = admin  说明是发消息
            if (data.group === 'admin') {
                $('#member_' + data.touid + '').append(html_my);
            } else {
                //收到消息
                $('#member_' + data.from_id + '').append(html_other);
            }
        }

        //获取当前时间
        function getCurrentTime() {
            const now = new Date();
            const formattedTime = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`;
            return formattedTime;
        }
    </script>
</body>
</html>

后端workerman

<?php


use Workerman\Worker;
use Workerman\Timer;
use Workerman\Connection\TcpConnection;
use WpOrg\Requests\Requests;
use think\facade\Db;
require_once __DIR__ . '/vendor/autoload.php';

$ws_workder = new Worker("websocket://127.0.0.1:2000");

//接受到信息
$ws_workder->onMessage = function (TcpConnection $connection, $data) use ($ws_workder) {
    $data = json_decode($data, true);
    //说明是初次登录 上线操作
    if ($data['type'] == 'login') {
        //设置分组
        $connection->group = $data['group'];
        //给链接对象添加属性 isreplied false
        $connection->isreplied = false;
        //当客户进来的时候
        if ($connection->group == 'member') {
            $serviceList = [];
            foreach ($ws_workder->connections as $conn) {
                // 找当前在线的客服
                if ($conn->group == 'admin') {
                    $serviceList[] = $conn->id;
                }
            }
            //如果当前客服有在线的
            if (!empty($serviceList)) {
                //随机取出一个客服的id
                $connection->touid = $serviceList[array_rand($serviceList, 1)];
                foreach ($ws_workder->connections as $conn) {
                    // 找到对应的客服id 准备接待
                    if ($connection->touid == $conn->id) {
                        $data['from_id'] = $connection->id;
                        $conn->send(json_encode($data));
                        $connection->isreplied = true;
                    }
                }
            }
        }
    }
    //有新消息发送
    if ($data['type'] == 'msg') {
        //客户发送信息
        if ($data['group'] == 'member') {
            // 获取当前用户的id
            foreach ($ws_workder->connections as $conn) {
                //如果有客服并且客服在线  
                //如果用户的touid 等于 连接的id 说明匹配到了对应的客服
                if ($conn->id == $connection->touid) {
                    $data['from_id'] = $connection->id;
                    //把消息客服发送消息
                    $conn->send(json_encode($data));
                    $posts = ['from_id' => $connection->id, 'to_id' => $connection->touid, 'msg' => $data['msg']];
                    //方案1 提交保存
$response=Requests::post('http://127.0.0.1:8000/admin/service/save_msg',data:$posts);
                    return ;
                }
                // 如果没有客服 还没有制作 可以直接保存发送的信息到数据库 等客服上线后发送给客服
            }
        }
        //当客服发送信息
        if ($data['group'] == 'admin') {
            $touid =  $data['touid'];
            foreach ($ws_workder->connections as $con) {
                if ($touid == $con->id) {
                    // 发送信息
                    $msgData=[
                        'type'=>'msg',
                        'from_id'=>$connection->id,
                        'msg'=>$data['msg'],
                    ];
                    $con->send(json_encode($msgData));
                   
                    $posts = ['from_id' => $connection->id, 'to_id' => $touid, 'msg' => $data['msg']];
                     //方案1 提交保存
                    // $response=Requests::post('http://127.0.0.1:8000/admin/service/save_msg',data:$posts);
                    //方案2 直接保存到数据库
                    // Db::table('msg')->save($posts);
                    return ;
                }
            }
        }
    }
    // 客服发来消息,请求客户列表
    if ($connection->group == 'admin' and $data['type'] == 'load_user_list') {
        $userlist = [];
        foreach ($ws_workder->connections as $conn) {
            //如果这个人是客户 并且 没有客服对象
            if ($conn->group == 'member' and !$conn->isreplied) {
                $userlist[] = $conn->id;
                $conn->isreplied = true;
                $conn->touid = $connection->id;
            }
        }
        $data['type'] = 'load_user_list';
        $data['userlist'] = $userlist;
        $connection->send(json_encode($data));
    }
};
//连接断开
$ws_workder->onClose = function (TcpConnection $connection) use ($ws_workder) {
    //客户断开连接 发送给对应的客服发送下线提醒
    if ($connection->group == 'member') {
        //遍历当前客户所属客服的id
        foreach ($ws_workder->connections as $conn) {
            //如果有分配客服
            if (!empty($connection->touid) and $conn->id == $connection->touid) {
                $data['type'] = 'logout';
                $data['disc_id'] = $connection->id;
                $conn->send(json_encode($data));
            }
        }
    };
    //客服 下线
    if ($connection->group == 'admin') {
        // 遍历出这位客服所管理的在线客户
        foreach ($ws_workder->connections as $conn) {
            //如果有分配客服
            if ($conn->group == 'member' and $conn->touid == $connection->id) {
                //将所管理的客户回归
                $conn->isreplied = false;

            }
        }
    };
};


//存储交流的信息
// 1 发送到tp服务器去存储 
// 2. 直接在workerman中去请求mysql存储

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

客服代码

登录前端

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>登录页面</title>
    <!-- 请勿在项目正式环境中引用该 layui.css 地址 -->
    <link href="//unpkg.com/layui@2.8.17/dist/css/layui.css" rel="stylesheet">
</head>

<body>
    <style>
        .demo-login-container {
            width: 320px;
            margin: 21px auto 0;
        }
        .demo-login-other .layui-icon {
            position: relative;
            display: inline-block;
            margin: 0 2px;
            top: 2px;
            font-size: 26px;
        }
        .layui-panel {
            height: 98vh;
            min-height: 500px;
            display: flex;
            align-items: center;
            justify-content: center;
            /* 如果你也希望水平居中 */
        }

        .layui-panel>div {
            width: 360px;
            height: 330px;
            border: 1px solid red;
        }
    </style>

    <div class="layui-panel">
        <div style="padding: 32px;">
            <form class="layui-form">
                <div class="demo-login-container">
                    <div class="layui-form-item">
                        <div class="layui-input-wrap">
                            <div class="layui-input-prefix">
                                <i class="layui-icon layui-icon-username"></i>
                            </div>
                            <input type="text" name="username" value="" lay-verify="required" placeholder="用户名"
                                lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <div class="layui-input-wrap">
                            <div class="layui-input-prefix">
                                <i class="layui-icon layui-icon-password"></i>
                            </div>
                            <input type="password" name="password" value="" lay-verify="required" placeholder="密   码"
                                lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <div class="layui-row">
                            <div class="layui-col-xs7">
                                <div class="layui-input-wrap">
                                    <div class="layui-input-prefix">
                                        <i class="layui-icon layui-icon-vercode"></i>
                                    </div>
                                    <input type="text" name="captcha" value="" lay-verify="required|captcha"
                                        placeholder="验证码" lay-reqtext="请填写验证码" autocomplete="off" class="layui-input"
                                        lay-affix="clear">
                                </div>
                            </div>
                            <div class="layui-col-xs5">
                                <div style="margin-left: 10px;">
                                    <img width="100%" src="{:captcha_src()}"
                                        onclick="this.src='{:captcha_src()}?_='+ new Date().getTime();">
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <input type="checkbox" name="remember" lay-skin="primary" title="记住密码">
                        <a href="#forget" style="float: right; margin-top: 7px;">忘记密码?</a>
                    </div>
                    <div class="layui-form-item">
                        <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">登录</button>
                    </div>
                    <div class="layui-form-item demo-login-other">
                        <label>社交账号登录</label>
                        <span style="padding: 0 21px 0 6px;">
                            <a href="javascript:;"><i class="layui-icon layui-icon-login-qq"
                                    style="color: #3492ed;"></i></a>
                            <a href="javascript:;"><i class="layui-icon layui-icon-login-wechat"
                                    style="color: #4daf29;"></i></a>
                            <a href="javascript:;"><i class="layui-icon layui-icon-login-weibo"
                                    style="color: #cf1900;"></i></a>
                        </span>
                        或 <a href="#reg">注册帐号</a>
                    </div>
                </div>
            </form>

        </div>
    </div>


    <!-- 请勿在项目正式环境中引用该 layui.js 地址 -->
    <script src="//unpkg.com/layui@2.8.17/dist/layui.js"></script>
    <script>
        layui.use(function () {
            var form = layui.form;
            var layer = layui.layer;
            var $ = layui.jquery;
            //自定义验证
            form.verify({
                captcha: function (value, elem) {
                    var msg = '';
                    // console.log(value);
                    if (value.length == 4) {
                        //如果为4发送ajax验证测试验证码是否通过 

                        var obj = $.ajax({
                            url: '{:url("admin/login/captchaCheck")}',
                            method: 'post',
                            async: false,
                            data: { captcha: value },

                        })

                        obj.done((res) => {
                            if (res.code == 0) {

                            } else {
                                //没有通过验证
                                // return res.msg;
                                msg = res.msg;
                            }
                            // console.log(res);
                        })
                        obj.fail((err) => {
                            // console.log(err);
                        })

                        return msg;
                    } else {
                        return '长度不对'
                    }
                }
            })
            // 提交事件
            form.on('submit(demo-login)', function (data) {
                var field = data.field; // 获取表单字段值
                $.ajax({
                    url: '{:url(domain:true)}',
                    method: 'post',
                    data: field,
                }).then((res) => {
                    if (res.code != 0) {
                        layer.msg(res.msg)
                    } else {
                        location.href = res.href
                    }
                })
                return false; // 阻止默认 form 跳转
            });
        });
    </script>

</body>

</html>

后端登录校验

Login.php

<?php
declare(strict_types=1);

namespace app\admin\controller;

use app\BaseController;

class Login extends BaseController
{
    function login()
    {
        if ($this->request->isGet()) {
            return view('index/login');
        } elseif ($this->request->isPost()) {
            // return '12345';
            $data = $this->request->post();
            //进行登录验证
            $this->validate($data, [
                //校验规则
                'username' => 'require',
                'password' => 'require',
                // 'captcha|验证码'=>'require|captcha'
            ], [
                //校验校验失败返回
                'username.require' => '用户名不能为空',
                'password.require' => '密码不能为空',
                // 'captcha.require'=>'验证码不能为空',
            ]);
            //就不查询数据库处理了
            $pwd = md5('123456');
            // 直接判断
            if ($data['username'] == 'admin' and  md5($data['password']) == $pwd) {
                //通过校验 跳转到控制页面
                //让前端去跳转页面
                //将用户登录信息写入到 缓存 或者session中去
                $href = (string)url('admin/index/index');
                session('user',$data['username']);
                return json(['code' => 0, 'msg' => '登录成功', 'href' => $href]);
                // return view('index/index');
            }
            //没有通过校验 返回错误信息
            return json(['code' => 1001, 'msg' => '密码错误']);
        }
    }

    function captchaCheck()
    {
        return json(['code' => 0, 'msg' => '通过']);
        // return '12345';
        //单独对验证码进行校验
        //获取验证码内容
        $captcha = $this->request->param('captcha');
        if (!captcha_check($captcha)) {
            //失败
            return json(['code' => 1001, 'msg' => '验证码错误']);
        } else {
            return json(['code' => 0, 'msg' => '通过']);
        }
    }
}

客服页面

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="/static/layui/css/layui.css">

    <title>客户端聊天窗口</title>
    <style type="text/css">
        html,
        body {
            width: 98%;
            height: 100%;
            margin: 0 auto;
            padding: 0px
        }

        .head_icon {
            display: inline-block;
            width: 50px;
            height: 50px;
            overflow: hidden;
            border-radius: 20%;
        }
        #contentor{
            overflow-y: auto; /* 垂直方向滚动 */  
            height: 500px; /* 高度自适应 */  
            width: 100%; /* 宽度自适应 */  
        }
    </style>


</head>

<body>

    <div class="layui-panel" >
        <div class="layui-row layui-col-space32
        " style="padding: 32px;">
            <div class="layui-col-xs12 ">
                <div style="border: 1px solid #f9f9f9;" id="contentor">

                </div>
            </div>
            <div class="layui-col-md12">

                <div class="layui-row">
                    <span class="layui-col-xs8">
                        <input type="text" name="send" placeholder="输入要发送的内容" class="layui-input">
                    </span>
                    <span class="layui-col-xs4">
                        <button type="button" class="layui-btn layui-bg-blue btn" style="width: 100%;">发送信息</button>
                    </span>
                </div>
            </div>

        </div>
    </div>

    <script src="/static/layui/layui.js"></script>
    <script type="text/javascript">
        layui.use(function () {
            $ = layui.jquery;
            layer = layui.layer;
            ws_connect()
            send()

        })

        //发送消息
        function send() {
            var button = $('.btn'),
                text = $('input[name="send"]');
            //发送按钮点击后
            button.click(function () {
                //给框体里面添加对应的显示代码
                // 获取输入框内容
                if (text.val() === "") {
                    layer.msg('请输入内容')
                } else {
                    data = { 
                        group:'member',
                        type:'msg',
                        msg: text.val()
                     };
                    ws.send(JSON.stringify(data));
                    // data['avatarRam'] =avatarRam;

                    data['avatarRam'] = 1;

                    auto_chat(data);
                    //清空内容框
                    text.val('')
                }
            })
        }

        function ws_connect() {
            ws = new WebSocket('ws://127.0.0.1:2000');
            //当 websocket 创建成功后 触发onopen事件
            ws.onopen = function () {
                // auto_chat('你是零基础的吗','老手覅');
                // setTimeout(()=>{auto_chat('同学你好','老手覅')}, 2000)
                var data = {};
                data.type = 'login';
                //标识  客户还是客服
                data.group = 'member';
                ws.send(JSON.stringify(data));
            }
            //收到服务端发来的消息 触发 onmessage
            ws.onmessage = function (e) {
                var data = JSON.parse(e.data);
                if (data.type == 'login') {
                    // avatarRam = data.avatarRam
                    avatarRam = 1
                    return ''
                }
                auto_chat(data)
            }
        }

        //发送消息
        function auto_chat(data) {
            let html_other = `
                <div class="layui-col-md12">
                        <div class="layui-row ">
                            <div class=" layui-col-xs1" style="text-align: left;">
                                <div class="head_icon">
                                    <img src="https://pic.qqtn.com/up/2017-12/15132234795879682.jpg" alt=""
                                        style="width: 100%;height: auto;display: inline-block;">
                                </div>
                            </div>
                            <div class=" layui-col-xs11">
                                <strong>游客${data.uid}</strong>
                                <span class="layui-font-green layui-font-16"> ${getCurrentTime()} </span>
                                <br>
                                <button class="layui-btn layui-btn-radius">${data.msg}</button>
                            </div>
                        </div>
                    </div>
                `
            let html_my = `
                    <div class="layui-col-md12">
                        <div class="layui-row">
                            <div class=" layui-col-xs11 " style="text-align: right;">
                                <span style="display: inline-block;" class="layui-font-green layui-font-16">  ${getCurrentTime()}
                                </span>
                                <br>
                                <button class="layui-btn  layui-bg-blue  layui-btn-radius">${data.msg}</button>
                            </div>
                            <div class="layui-col-xs1" style="text-align: right;">
                                <div class="head_icon " style="display: inline-block;">
                                    <img src="/img/avatar${data.avatarRam}.png" alt=""
                                        style="width: 100%;height: auto;">
                                </div>
                            </div>
                        </div>
                    </div>
                    `;
            console.log(data);
            console.log(data.uid);

            //将信息添加到对应的框体内
            if (data.group === 'member') {
                $('#contentor').append(html_my);
            } else {
                $('#contentor').append(html_other);
            }
        }
        //获取当前时间
        function getCurrentTime() {
            const now = new Date();
            const formattedTime = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`;
            return formattedTime;
        }

    </script>
</body>

</html>

简单总结

课程链接

课程链接
课程链接

用到的知识点

  • php
    • thinkphp
    • wrokerman
    • http request
  • 前端
    • jquery
    • layui

存在问题

以游客的身份也无法进行鉴权
可以通过 $connection->getRemoteIp()获得对方ip 但是如果游客的ip也在变化就没啥用了
注意:onConnect事件仅仅代表客户端与Workerman完成了TCP三次握手,这时客户端还没有发来任何数据,此时除了通过$connection->getRemoteIp()获得对方ip,

可以通过隐藏对话框来模拟关闭

页面不够美观

代码

 layer.open({
            type: 2,
            closeBtn: 0,
            maxmin: true,
            title: '聊天通信',
            area: ['800px', '800px'],
            // btn: ['发送'],
            shade: 0,
            content: '/index/index/chat',
            //点击按钮后触发的函数
            yes: function (index, layero) {
                //获取到打开iframe对象
                var iframeWin = window[layero.find('iframe')[0]['name']];
                // console.log(iframeWin);
                //调用对应的send方法
                iframeWin.send();
            }
        })
关闭

用微信“扫一扫”