php workerman 即时通讯聊天系统

kkcode
kkcode
2024-08-30阅读 1664

项目地址

项目下载好后

进入 tp 目录 启动 tp 框架
执行 composer install 安装依赖
使用命令 php think run
进入 workman 目录 启动 workman 服务
执行 composer install 安装依赖
使用命令 php .\ws_formal.php start

浏览器
进入 http://127.0.0.1:8000/ 模拟客户
进入 http://127.0.0.1:8000/admin/login/login 后台登录页面
用户名 admin
密码 123456
验证 随便填 后台取消了验证


走到这一步就成功了

HTTP

http 协议

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

http 协议工作过程

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

WebSoket

websoket 协议

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

工作过程

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

websocket 与 http 差异

相同点


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

不同点

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

轮询

过程

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

弊端

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

websocket 改进

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. 将全局的 config 和 route 复制一份到创建的应用里面
    • 开器多应用后全局的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));

即时通讯聊天系统

简单的群聊功能

前端页面

聊天框内容分析

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta >
    <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" >
                    </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[]');
            //发送按钮点击后
            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();

游客 客服聊天

大体框架

游客前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta >
    <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 ></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[]').val(),
            }
            if (data.msg.trim == '' || data.msg.length == 0) {
                return layer.alert('信息不能为空', { icon: 0 })
            }
            ws.send(JSON.stringify(data));
            $('textarea[]').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 >
    <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" 
                                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" 
                                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" 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" >
                        <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 >
    <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" >
                    </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[]');
            //发送按钮点击后
            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 但是如果游客的 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();
            }
        })

原文地址 www.cnblogs.com

评论数量:1
2024-09-07