草庐IT

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

一条理想的咸鱼 2023-03-28 原文

背景

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

workerman介绍

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

官网地址:https://www.workerman.net

手册地址:https://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介绍

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

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框架的魅力了,但它是怎么执行的,实现的.我们继续往下看

官网是怎么说:https://www.workerman.net/doc/gateway-worker/getting-started.html

所以我们只需要关注到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.

参考资料

有关利用workerman构建一个客服系统(1)的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  5. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  6. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  7. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  8. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  9. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

  10. ruby - 在 Ruby 中构建长字符串的简洁方法 - 2

    在编写Ruby(客户端脚本)时,我看到了三种构建更长字符串的方法,包括行尾,所有这些对我来说“闻起来”有点难看。有没有更干净、更好的方法?变量递增。ifrender_quote?quote="NowthatthereistheTec-9,acrappyspraygunfromSouthMiami."quote+="ThisgunisadvertisedasthemostpopularguninAmericancrime.Doyoubelievethatshit?"quote+="Itactuallysaysthatinthelittlebookthatcomeswithit:themo

随机推荐