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

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

1.性能提升10-100倍:基于常驻内存、epoll高性能事件循环库、高性能协议解析,workerman可将基于php-fpm的架构应用性能提升十倍甚至近百倍
2.稳定性:经过多年的不断打磨及完善,workerman早已具备企业级的稳定性,已经被众多公司用在生产环境上
3.兼容性:兼容现有composer生态。即将推出的workerman v5版本将支持PHP自带的Fiber协程以及Swoole、ReactPHP、AmPHP等协程库
4.易用性:少既是多,workerman只提供必要的功能接口,在保证workerman简约的同时,你会发现它使用真的很简单

开发环境:win10+phpstudy集成环境+php7.4+gatewayworker扩展包+tp5.1框架
文档地址: 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

启动结果,如下图

至此,项目已基本搭建完成
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';
}
在上一小节中我们已经初体验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),这两个方法
当前端执行实例化(即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"),如下图

连接成功后,前端通过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.
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的rubyyaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir
我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下: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列,在这种情况下
在编写Ruby(客户端脚本)时,我看到了三种构建更长字符串的方法,包括行尾,所有这些对我来说“闻起来”有点难看。有没有更干净、更好的方法?变量递增。ifrender_quote?quote="NowthatthereistheTec-9,acrappyspraygunfromSouthMiami."quote+="ThisgunisadvertisedasthemostpopularguninAmericancrime.Doyoubelievethatshit?"quote+="Itactuallysaysthatinthelittlebookthatcomeswithit:themo