利用workerman构建一个客服系统(1)
背景
我之前在做聊天系统时,采用的是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.