背景
我之前在做聊天系统时,采用的是ajax异步不断的请求后台服务.这样做的好处时简单,快速.但是有个巨大的缺点就是对服务端的请求压力巨大,容易崩溃.如下图就是一个利用Ajax不断请求的后台服务.
workerman介绍
workerman是一款开源高性能PHP应用容器,它大大突破了传统PHP应用范围,被广泛的用于互联网、即时通讯、APP开发、硬件通讯、智能家居、物联网等领域的开发。
官网地址:www.workerman.net
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
<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类
/** | |
* 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) | |
}); |
后台-服务接收端:
/** | |
* 当客户端连接时触发 | |
* 如果业务不需此回调可以删除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^.