workerman即時通訊聊天系統

来源:https://www.cnblogs.com/henordinary/archive/2023/10/11/17756531.html
-Advertisement-
Play Games

本文深入探討了Go語言中方法的各個方面,包括基礎概念、定義與聲明、特性、實戰應用以及性能考量。文章充滿技術深度,通過實例和代碼演示,力圖幫助讀者全面理解Go方法的設計哲學和最佳實踐。 關註【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品 ...


項目地址

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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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();
            }
        })

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 常用的物聯網管理系統主要有以下幾種:智能家居系統:通過物聯網技術,將家庭設備和電器互聯起來,實現智能化控制和管理的系統。智能家居系統可以實現家庭設備的遠程式控制制、智能化場景設置、安防監控等功能,方便用戶提高家居生活的便利性和舒適度。智能工廠系統:利用物聯網技術,通過互聯的工廠設備、感測器和電腦系統來 ...
  • 一、引言 在當今數字化時代,零售業正迅速發展,消費者的購物行為和期望發生了巨大的變化。為了滿足不斷增長的需求,零售企業必須構建高度靈活、穩健可靠的商品系統。 本文將深入探討零售商品系統的底層邏輯,聚焦領域驅動設計(DDD)和複雜業務系統架構經驗,揭示其在零售業務中的應用和價值。 二、面臨的挑戰 商品 ...
  • 一、項目地址 https://github.com/LinFeng-BingYi/DailyAccountBook 二、新增 1. 增加刪除記錄功能 1.1 功能詳述 點擊刪除按鈕後,獲取對應行的數據組成字典,用字典的鍵值對匹配到對應日期的記錄元素; 接著用該字典數據沖正存款賬戶餘額(實現思路為新增 ...
  • 集合 集合用於在單個變數中存儲多個項。集合是 Python 中的 4 種內置數據類型之一,用於存儲數據集合,其他 3 種是列表(List)、元組(Tuple)和字典(Dictionary),它們都具有不同的特性和用途。集合是一種無序、不可更改(*)、無索引的集合。 創建一個集合 集合用大括弧表示。 ...
  • 我們先瞭解下,為什麼需要配置日期格式化? 通常情況下,發起一個 Http 請求,Spring Boot 會根據請求路徑映射到指定 Controller 上的某個方法的參數上,接著,Spring 會自動進行類型轉換。 對於日期類型的參數,Spring 預設是沒有配置如何將字元串轉換成日期類型的 未配置 ...
  • 前置知識 \(1.\) 艾佛森括弧: \([P]=\begin{cases}1 & \mathtt{(if\ P\ is \ true)}\\0 & \mathtt{(otherwise)}\end{cases}\) \(2.\) \(a\mid b\) 表示 \(a\) 是 \(b\) 的因數 \ ...
  • 1、ZGC簡介 1.1 介紹 ZGC 是一款低延遲的垃圾回收器,是 Java 垃圾收集技術的最前沿,理解了 ZGC,那麼便可以說理解了 java 最前沿的垃圾收集技術。 從 JDK11 中作為試驗特性推出以來,ZGC 一直在不停地發展中。 從 JDK14 開始,ZGC 開始支持 Windows。 在 ...
  • 作者:椰子Tyshawn 來源:https://blog.csdn.net/litianxiang_kaola 最近公司的在做服務化, 需要把所有model包里的類都實現Serializable介面, 同時還要顯示指定serialVersionUID的值. 聽到這個需求, 我腦海裡就突然出現了好幾個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...