利用workerman构建一个客服系统(1)

PHP技术
436
0
0
2022-10-23

背景

我之前在做聊天系统时,采用的是ajax异步不断的请求后台服务.这样做的好处时简单,快速.但是有个巨大的缺点就是对服务端的请求压力巨大,容易崩溃.如下图就是一个利用Ajax不断请求的后台服务.

workerman介绍

workerman是一款开源高性能PHP应用容器,它大大突破了传统PHP应用范围,被广泛的用于互联网、即时通讯、APP开发、硬件通讯、智能家居、物联网等领域的开发。

官网地址:www.workerman.net

手册地址:www.workerman.net/doc

workman的特点

1.性能提升10-100倍:基于常驻内存、epoll高性能事件循环库、高性能协议解析,workerman可将基于php-fpm的架构应用性能提升十倍甚至近百倍

2.稳定性:经过多年的不断打磨及完善,workerman早已具备企业级的稳定性,已经被众多公司用在生产环境上

3.兼容性:兼容现有composer生态。即将推出的workerman v5版本将支持PHP自带的Fiber协程以及Swoole、ReactPHP、AmPHP等协程库

4.易用性:少既是多,workerman只提供必要的功能接口,在保证workerman简约的同时,你会发现它使用真的很简单

应用场景

workerman初体验

项目搭建

开发环境:win10+phpstudy集成环境+php7.4+gatewayworker扩展包+tp5.1框架

gatewayworker介绍

文档地址: www.workerman.net/doc/gateway-work...

GatewayWorker基于Workerman开发的一个项目框架,用于快速开发TCP长连接应用,例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等

GatewayWorker使用经典的Gateway和Worker进程模型。Gateway进程负责维持客户端连接,并转发客户端的数据给BusinessWorker进程处理,BusinessWorker进程负责处理实际的业务逻辑(默认调用Events.php处理业务),并将结果推送给对应的客户端。Gateway服务和BusinessWorker服务可以分开部署在不同的服务器上,实现分布式集群。

GatewayWorker提供非常方便的API,可以全局广播数据、可以向某个群体广播数据、也可以向某个特定客户端推送数据。配合Workerman的定时器,也可以定时推送数据。

搭建

1.利用composer create-project topthink/think=5.1.* tp5,下载tp5框架包

2.下载gatewayworker的window-demo

3.将下载好的gatewayworker压缩包解压至tp5下的vendor文件夹中

4.通过phpstudy创建一个网站

5.验证tp5是否能正常加载

6.修改gatewayworker相关配置,主要是修改start_gateway.php中的红框中的协议部分,将TCP协议改为Websocket

7.启动gateway,win下点击红框中的start_for_win.bat

启动结果,如下图

至此,项目已基本搭建完成

workerman整合入项目及长连接实现群发功能初体验

1.首先将聊天室的静态资源配置到public/static目录下

2.在config/template.php配置中追加

'tpl_replace_string' => [ '__STATIC__' => $_SERVER["REQUEST_SCHEME"] . '://' . $_SERVER["SERVER_NAME"] . '/static', ]

3.创建模板

index.html

<!doctype html>
<html>
<head> 
    <meta charset="utf-8"> 
    <meta name="format-detection" content="telephone=no"/> 
    <title>沟通中</title> 
    <link rel="stylesheet" type="text/css" href="__STATIC__/css/themes.css?v=2017129"> 
    <link rel="stylesheet" type="text/css" href="__STATIC__/css/h5app.css"> 
    <link rel="stylesheet" type="text/css" href="__STATIC__/fonts/iconfont.css?v=2016070717"> 
    <script src="__STATIC__/js/jquery.min.js"></script> 
    <script src="__STATIC__/js/dist/flexible/flexible_css.debug.js"></script> 
    <script src="__STATIC__/js/dist/flexible/flexible.debug.js"></script>
</head>
<body ontouchstart>
<div class='fui-page-group'> 
    <div class='fui-page chatDetail-page'> 
        <div class="chat-header flex"> 
            <i class="icon icon-toleft t-48"></i> 
            <span class="shop-titlte t-30">商店</span> 
            <span class="shop-online t-26"></span> 
            <span class="into-shop">进店</span> 
        </div> 
        <div class="fui-content navbar" style="padding:1.2rem 0 1.35rem 0;"> 
            <div class="chat-content"> 
                <p style="display: none;text-align: center;padding-top: 0.5rem" id="more"><a>加载更多</a></p> 
                <p class="chat-time"><span class="time">2017-11-12</span></p>

                <div class="chat-text section-left flex"> 
                    <span class="char-img" style="background-image: url(http://chat.test/static/img/123.jpg)"></span> 
                    <span class="text"><i class="icon icon-sanjiao4 t-32"></i>你好</span> 
               </div>

                <div class="chat-text section-right flex"> 
                    <span class="text"><i class="icon icon-sanjiao3 t-32"></i>你好</span>-->
                    <span class="char-img" style="background-image: url(http://chat.test/static/img/132.jpg)"></span> 
                </div>

            </div> 
        </div> 
        <div class="fix-send flex footer-bar"> 
            <i class="icon icon-emoji1 t-50"></i> 
            <input class="send-input t-28" maxlength="200"> 
            <i class="icon icon-add t-50" style="color: #888;"></i> 
            <span class="send-btn">发送</span> 
        </div> 
    </div>
</div>

</body>
</html>

4.将控制器中Index方法的返回值重定向到index.html模板中

5.验证一下

6.在index.html模板中创建websocket链接

<script type="text/javascript">
var ws = new WebSocket('ws://127.0.0.1:8282');
ws.onmessage = function (e) {
         console.log(e);
    }
</script>

7.验证一下,是否能成功链接websocket

在开一个窗口看下

原窗口的控制台会有一条新窗口登录的信息

8.从上面的7步,我们已经成功的连接了websocket.接下来我们尝试发送一个消息

//当我们点击发送按钮时,会产生什么效果
$(".send-btn").click(function () {
  ws.send("hello websocket!")
})

点击发送按钮后,运行结果

我们发现后台会给前台返回消息,接下来我们发送一下自定义消息

//当我们点击发送按钮时,发送自定义内容是会出现什么现象
$(".send-btn").click(function () {
  var content = $(".send-input").val();
  ws.send(content);
  $(".send-input").val(null);
})

要发送的内容

运行结果

聊天页面展示时出现的问题

1.页面静态资源的字体图标不显示的问题

详细错误:

解决方法:

nginx配置文件添加如下配置:

location ~* \.(eot|ttf|woff|json)$ {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET,POST';
    }

workerman群发与客户端和服务端保持双向消息推送

理解gatewayworker的执行过程

在上一小节中我们已经初体验websocket中gatewayworker框架的魅力了,但它是怎么执行的,实现的.我们继续往下看

官网是怎么说:www.workerman.net/doc/gateway-work...

所以我们只需要关注到Event这个类即可,下面的代码就是Event类

<?php
/**
 * This file is part of workerman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link http://www.workerman.net/
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 */

/**
 * 用于检测业务代码死循环或者长时间阻塞等问题
 * 如果发现业务卡死,可以将下面declare打开(去掉//注释),并执行php start.php reload
 * 然后观察一段时间workerman.log看是否有process_timeout异常
 */
//declare(ticks=1);

use \GatewayWorker\Lib\Gateway;

/**
 * 主逻辑
 * 主要是处理 onConnect onMessage onClose 三个方法
 * onConnect 和 onClose 如果不需要可以不用实现并删除
 */
class Events
{
    /**
     * 当客户端连接时触发
     * 如果业务不需此回调可以删除onConnect
     * 
     * @param int $client_id 连接id
     */ 
    public static function onConnect($client_id)
    {
        // 向当前client_id发送数据  
        Gateway::sendToClient($client_id, "Hello $client_id\r\n");
        // 向所有人发送 
        Gateway::sendToAll("$client_id login\r\n");
    }

   /**
    * 当客户端发来消息时触发
    * @param int $client_id 连接id
    * @param mixed $message 具体消息
    */ 
   public static function onMessage($client_id, $message)
   {
        // 向所有人发送  
        Gateway::sendToAll("$client_id said $message\r\n");
   }

   /**
    * 当用户断开连接时触发
    * @param int $client_id 连接id
    */ 
   public static function onClose($client_id)
   {
       // 向所有人发送  
       GateWay::sendToAll("$client_id logout\r\n");
   }
}

它主要分为三个部分,分别是:onConnect($client_id),onMessage($client_id, $message),onClose($client_id)

在本小节中我们主要注意onConnect($client_id),onMessage($client_id, $message),这两个方法

onConnect($client_id)连接过程

当前端执行实例化(即new Websocket(‘Websocket://127.0.0.1:8282’)),后台通过8282端口监听到前端的请求,就会创建一个ws连接,并自动分配一个客户端ID,如下图

然后会向指定人发送一条登录成功信息,执行代码Gateway::sendToClient($client_id, "Hello $client_id\r\n");,如下图

最后会通过类似广播的方式去通知全场人员,有个新用户登录啦,执行代码Gateway::sendToAll("$client_id login\r\n"),如下图

onMessage($client_id, $message)执行过程

连接成功后,前端通过ws.onmessage = function (e) {consle.log(e)}这行代码执行监听相关方法返回的数据信息,就比如通知[指定人/全场用户]登录成功的消息提示,就是通过该闭包方法执行的.

案例

如何在聊天窗口展示不同类型聊天内容

前台-发送端:

 $(".send-btn").click(function () {
        var content = $(".send-input").val();
        var data = '{"data":"' + content + '","type":"say"}';
        $(".chat-content").append('<div class="chat-text section-right flex"><span class="text"><i class="icon icon-sanjiao3 t-32"></i>'+content+'</span><span class="char-img" style="background-image: url(http://chat.test/static/img/132.jpg)"></span></div>')
        ws.send(data)
        $(".send-input").val(null)
    });

后台-服务接收端:

<?php 
 /**
     * 当客户端连接时触发
     * 如果业务不需此回调可以删除onConnect
     * 
     * @param int $client_id 连接id
     */ 
    public static function onConnect($client_id)
    {
        global $num;
        // 向当前client_id发送数据  
        //Gateway::sendToClient($client_id, "Hello $client_id\r\n"); 
        // 向所有人发送 
        //Gateway::sendToAll("$client_id login\r\n");

        echo "connect : ".$num."<----->client_id : ".$client_id."\r\n";
    }
 /**
    * 当客户端发来消息时触发
    * @param int $client_id 连接id
    * @param mixed $message 具体消息
    */ 
   public static function onMessage($client_id, $message)
   {
       if (empty($message)){
           return;
       }
       $data = [];
       if (!empty($message)){
           $data = json_decode($message,true);
       }
       if (isset($data['type']) && !empty($data['type'])){
           switch ($data['type']){
               case "say":
                   $newDate = [
                       'id'=>$client_id,
                       'date' => date('Y-m-d'),
                       'data' => $data['data'],
                       'type' => 'text',
                   ];
                   // 向所有人发送 
                   Gateway::sendToAll(json_encode($newDate,JSON_UNESCAPED_UNICODE));
                   return;
           }
           return;
       }
       //Gateway::sendToAll("$client_id said ".$message."\r\n");
   }

   /**
    * 当用户断开连接时触发
    * @param int $client_id 连接id
    */ 
   public static function onClose($client_id)
   {
       // 向所有人发送  
       //GateWay::sendToAll("$client_id logout\r\n");
   }

前台-消息接收端

ws.onmessage = function (e) {
        var message = eval("(" + e.data + ")")
         console.log(message);
         console.log(e);
         switch (message.type) {
             case 'text':
                 $(".chat-content").append('<div class="chat-text section-left flex"><span class="char-img" style="background-image: url(http://chat.test/static/img/123.jpg)"></span><span class="text"><i class="icon icon-sanjiao4 t-32"></i>'+message.data+'</span></div>')
                 return;
         }
    }

运行结果:

在第一个窗口中要发送的内容,如下图:

执行结果,如下图:

新窗口发送的内容,如下图:

原窗口也接收到新窗口的内容,如下图

注意:修改gatewayworker中的服务端任何代码时,都要重启服务.

到这里,我们已经体验完了workerman,是不是感觉很简单呢! ^V^.

参考资料