<?php
/**
* ***************************************
* 单进程保护 *
* ***************************************
*/
$phpSelf = realpath($_SERVER['PHP_SELF']);
$lockFile = $phpSelf.'.lock';
$lockFileHandle = fopen($lockFile, "w");
if ($lockFileHandle == false) {
exit("Can not create lock file $lockFile\n");
}
if (!flock($lockFileHandle, LOCK_EX + LOCK_NB)) {
exit(date("Y-m-d H:i:s")."Process already exist.\n");
} /**
* ***************************************
* 进入程序,定义相关配置 *
* ***************************************
*/
set_time_limit(0);
//socket会话的超时时间,根据业务场景设置,这里设置为永不超时
//如果设置了时间,则从socket建立=>传输=>关闭整个过程必须在定义的时间内完成,否则自动close该socket并抛出warning
ini_set('default_socket_timeout', -1);
$conf = array(
'listen' => array('host' => '0.0.0.0','port' => '8008'),
'setting' => array(
//程序允许的最大连接数,用以设置server最大允许维持多少个TCP连接,超过该数量后,新连接将被拒绝,默认为ulimit -n的值,如果设置大于ulimit -n则强制重置为ulimit- n,如果确实需要设置超过ulimit -n的值,请修改系统值 vim /etc/security/limits.conf 修改nofile的值
"max_conn" => 1024,
//启用CPU亲和设置(在全异步非阻塞是可启用),在多核的服务器中,启用此特性会将swoole的reactor线程/worker进程绑定到固定的一个核上。可以避免进程/线程的运行时在多个核之间互相切换,提高CPU Cache的命中率,如何确定绑定在了哪个核上,请参考文档, 查看命令: taskset -p 进程id
'open_cpu_affinity' => 0,
//配置task进程数量,配置此参数后将会启用task功能。所以Server务必要注册onTask、onFinish2个事件回调函数。如果没有注册,服务器程序将无法启动.Task进程是同步阻塞的,配置方式与Worker同步模式一致。
'task_worker_num' => 20,
//设置task进程的最大任务数。一个task进程在处理完超过此数值的任务后将自动退出。这个参数是为了防止PHP进程内存溢出。如果不希望进程自动退出可以设置为0, 默认是0
'task_max_request' => 1024,
//设置task的数据临时目录,在swoole_server中,如果投递的数据超过8192字节,将启用临时文件来保存数据。这里的task_tmpdir就是用来设置临时文件保存的位置。
'task_tmpdir' => '/tmp/',
//worker进程数量,根据业务代码的模式作调整,全异步非阻塞可设置为CPU核数的1-4倍;同步阻塞,请参考文档调整
'worker_num' => 8,
//指定swoole错误日志文件
'log_file' => '/tmp/log/log.txt',
//SSL公钥和私钥的位置,启用wss必须在编译swoole时加入--enable-openssl选项
'ssl_cert_file' => '/usr/local/nginx/conf/server.cer',
'ssl_key_file' => '/usr/local/nginx/conf/server.key',
),
); /**
* ***************************************
* 初始化Redis连接 *
* ***************************************
*/
$redis = null;
$redis = new Redis();
$redis->connect(REDIS_HOST, REDIS_PORT);
$redis->auth(REDIS_PWD);
$GLOBALS['redis']=$redis; /**
* ***************************************
* 脚本重启时,清除历史的数据 *
* ***************************************
*/
$sArr = $redis->sMembers(REDIS_S_KEY);
if (!empty($sArr)) {
foreach ((array)$sArr as $key => $sc) {
$fdArr = $redis->sMembers(REDIS_S_FD.$sc);
foreach ((array)$fdArr as $k => $fd) {
$res1 = $redis->del(REDIS_FD_S.$fd);
}
$res2 = $redis->del(REDIS_S_FD.$sc);
}
$redis->del(REDIS_S_KEY);
}
$redis->del(REDIS_ZS_KEY); /**
* ***************************************
* 绑定回调事件 *
* ***************************************
*/
$ws = null;
//wss服务
$ws = new swoole_websocket_server($conf['listen']['host'], $conf['listen']['port'], SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$ws->set($conf['setting']); /**
* Server启动在主进程的主线程回调此函数
* 在此事件之前Swoole Server已进行了如下操作
* 已创建了manager进程
* 已创建了worker子进程
* 已监听所有TCP/UDP端口
* 已监听了定时器
* 在onStart中创建的全局资源对象不能在worker进程中被使用,因为发生onStart调用时,worker进程已经创建好了。新创建的对象在主进程内,worker进程无法访问到此内存区域。因此全局对象创建的代码需要放置在swoole_server_start之前
*/
$ws->on('start', function ($ws) {
swoole_set_process_name(PROCESS_NAME.'_master');
}); /**
* 与onStart回调在不同进程中并行执行的回调函数(不存在先后顺序)
* @param: $ws swoole_websocket_server object
* @param: $wid 创建该进程时swoole分配的id(不是进程id)
* 注意点:
* 1. 此事件在worker进程/task进程启动时发生。onWorkerStart/onStart是并发执行的,没有先后顺序,这里创建的对象可以在进程生命周期内使用
* 2. swoole1.6.11之后task_worker中也会触发onWorkerStart,故而在下面的处理中,加入了判断业务类型$jobType是task还是work,如果是task则命名为****_Tasker_$id,如果是worker则命名为****_Worker_$id
* 3. 发生PHP致命错误或者代码中主动调用exit时,Worker/Task进程会退出,管理进程会重新创建新的进程
* 5. 如果想使用swoole_server_reload实现代码重载入,必须在workerStart中require你的业务文件,而不是在文件头部。在onWorkerStart调用之前已包含的文件,不会重新载入代码。
* 6. 可以将公用的,不易变的php文件放置到onWorkerStart之前(例如上面的redis配置)。这样虽然不能重载入代码,但所有worker是共享的,不需要额外的内存来保存这些数据。
* 7. onWorkerStart之后的代码每个worker都需要在内存中保存一份
*/
$ws->on('workerstart', function ($ws, $wid) {
$jobType = $ws->taskworker ? 'Tasker' : 'Worker';
swoole_set_process_name(PROCESS_NAME.'_'.$jobType.'_'.$wid);
$GLOBALS['ws'] = $ws; //保存server对象到全局中以待使用
if ($jobType == 'Worker') { //在某个worker进程上绑定redis订阅进程
if ($wid === 0) {
$dataRedis = null;
$dataRedis = new Redis();
$dataRedis->connect(REDIS_HOST_DATA, REDIS_PORT_DATA);
$dataRedis->auth(REDIS_PWD_DATA);
//使用psubscribe订阅指定模式的频道,这里*表示所有频道
//请注意,redis订阅不提供区分库(db)的功能,所以多个库都同时在发布同一个名字的频道时,都将被订阅到
$dataRedis->psubscribe(array("*"), "sendTask");
}
}
}); /**
* 管理进程启用时,调用该回调函数
* 注意manager进程中不能添加定时器
* manager进程中可以调用sendMessage接口向其他工作进程发送消息
*/
$ws->on('managerstart', function ($ws) {
swoole_set_process_name(PROCESS_NAME.'_manage');
}); /**
* swoole websocket服务特有的回调函数,此函数在websocket服务器中必须定义实现,否则websocket服务将无法启动
* 当服务器收到来自客户端的数据帧时会回调此函数
* @param: $ws为swoole_websocket_server对象,其结构在调试时可var_dump查看
* @param: $frame为swoole_websocket_frame对象,包含了客户端发来的数据帧信息,包含以下四个属性:
* @param: $frame->fd: 客户端的socket id,每个id对应一个客户端,推送消息的时候需要指定
* @param: $frame->data: 数据内容,可以是文本内容或者是二进制数据(图片等),可以通过opcode的值来判断。$data 如果是文本类型,编码格式必然是UTF-8,这是WebSocket协议规定的
* @param: $frame->opcode: WebSocket的OpCode类型,可以参考WebSocket协议标准文档, WEBSOCKET_OPCODE_TEXT = 0x1 ,文本数据; WEBSOCKET_OPCODE_BINARY = 0x2 ,二进制数据
* @param: $frame->finish: 表示数据帧是否完整,一个WebSocket请求可能会分成多个数据帧进行发送
* 注意点: 客户端发送的ping帧不会触发onMessage,底层会自动回复pong包
*/
$ws->on('message', function ($ws, $frame) {
echo "Server has receive message\n";
//接收到客户端请求,并建立连接之后,进行相应业务的处理
handleClientData($ws, $frame);
}); /**
* 在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务(此处使用的是taskwait)
* 当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将进程状态切换为空闲然后继续接收新的Task。
* @param: $ws swoole_websocket_server object
* @param: $tid task process id
* @param: $wid from id 表示来自哪个Worker进程。$task_id和$wid组合起来才是全局唯一的,不同的worker进程投递的任务ID可能会有相同
* @param: $data 需要执行的任务内容
* 注意点: onTask函数执行时遇到致命错误退出,或者被外部进程强制kill,当前的任务会被丢弃,但不会影响其他正在排队的Task
*/
$ws->on('task', function ($ws, $tid, $wid, $data) {
switch ($data['cmd']) {
case 'pushToClient': $ret = pushToClientTask($ws, $data['key'], $data['val']); break;
}
//1.7.2以上的版本,在onTask函数中 return字符串,表示将此内容返回给worker进程。worker进程中会触发onFinish函数,表示投递的task已完成。return的变量可以是任意非null的PHP变量
return $returnContent;
//1.7.2以前的版本,需要调用swoole_server->finish()函数将结果返回给worker进程
// $ws->finish($data);
}); /**
* 当worker进程投递的任务在task_worker中完成时,task进程会通过$ws->finish()方法将任务处理的结果发送给worker进程。
* @param: $ws swoole_websocket_server object
* @param: $tid task_id
* @param: $data 任务处理后的结果内容
* 注意点: task进程的onTask事件中没有调用finish方法或者return结果,worker进程不会触发onFinish
* 执行onFinish逻辑的worker进程与下发task任务的worker进程是同一个进程
*/
$ws->on('finish', function($ws, $tid, $data) { }); /**
* TCP客户端连接关闭后,在worker进程中回调此函数
* 在函数中可以做一些类似于删除业务中与每个客户端交互时存放的数据的操作
* @param: $ws swoole_websocket_server object
* @param: $fd 已关闭的fd interger
* @param: $rid(可选),来自哪个reactor线程
* 注意点:
* 1. onClose回调函数如果发生了致命错误,会导致连接泄漏。通过netstat命令会看到大量CLOSE_WAIT状态的TCP连接
* 2. 查看命令netstat -anopc | grep 端口号,可以查看到TCP接收和发送队列是否有堆积以及TCP连接的状态
* 3. 无论由客户端发起close还是服务器端主动调用$serv->close()关闭连接,都会触发此事件。因此只要连接关闭,就一定会回调此函数
* 4. 1.7.7+版本以后onClose中依然可以调用connection_info方法获取到连接信息,在onClose回调函数执行完毕后才会调用close关闭TCP连接
* 5. 这里回调onClose时表示客户端连接已经关闭,所以无需执行$server->close($fd)。代码中执行$serv->close($fd)会抛出PHP错误告警。也就是在onclose中不能再$ws->close()了.
* 6. swoole-1.9.7版本修改了$reactorId参数,当服务器主动关闭连接时,底层会设置此参数为-1,可以通过判断$reactorId < 0来分辨关闭是由服务器端还是客户端发起的(debug时可以使用)
*/
$ws->on('close', function ($ws, $fd) {
$redis = new Redis();
$redis->connect(REDIS_HOST, REDIS_PORT);
$redis->auth(REDIS_PWD);
$sArr = $redis->sMembers(REDIS_FD_S.$fd);
if (!empty($sArr)) {
foreach ((array)$sArr as $key => $sc) {
$res = $redis->sRem(REDIS_S_FD.$sc, $fd);
$num = $redis->sCard(REDIS_S_FD.$sc);
if ($num == '0') {
$redis->sRem(REDIS_S_KEY, $sc);
$redis->hDel(REDIS_ZS_KEY, $sc);
}
}
}
$redis->del(REDIS_FD_S.$fd);
$redis->close();
echo "FD $fd has closed.\n";
}); /**
* 开启swoole_websocket_server服务
*/
$ws->start(); /**
* 接受到消息以后进行响应异步任务的执行
* @param: $ws swoole_websocket_sever object
* @param: $frame swoole_websocket_frame obejct
*/
function handleClientData($ws, $frame) {
$data = $frame->data;
$redis = new Redis();
$redis->connect(REDIS_HOST, REDIS_PORT);
$redis->auth(REDIS_PWD);
$isMembers = $redis->sIsmember(REDIS_S_FD.$sc, $frame->fd);
if (!$isMembers) {
$res = $redis->sAdd(REDIS_S_FD.$sc, $frame->fd);
}
$redis->sAdd(REDIS_FD_S.$frame->fd, $sc);
$isMembers = $redis->sIsmember(REDIS_S_KEY, $sc);
if (!$isMembers) {
$redis->sAdd(REDIS_S_KEY, $sc);
}
} /**
* redis订阅后的回调函数
* @param: $ins instance实例
* @param: $pattern 匹配模式
* @param: $channel 频道名
* @param: $data 数据
* 注意点: subscribe和psubscribe两种不同的订阅方式的回调函数的参数个数不一样,后者多了$pattern参数
*/
function sendTask($ins, $pattern, $channel, $data) {
//满足一些条件后,投递到task进程中进行推送
$taskData = array(
'cmd' => 'pushToClient',
'key' => $sc,
'val' => $data,
);
//请注意,taskwait是同步阻塞的,所以改脚本并不是全异步非阻塞的
$GLOBALS['ws']->taskwait($taskData);
} /**
* 推送消息到指定的客户端
* @param: $ws swoole_websocket_server object
* @param: $sc 股票代码
* @param: $data 要推送的数据
*/
function pushToClientTask($ws, $sc, $data) {
$redis = new Redis();
$redis->connect(REDIS_HOST, REDIS_PORT);
$redis->auth(REDIS_PWD);
$fdList = $redis->sMembers(REDIS_S_FD.$sArr[4]);
if (!empty($fdList)) {
foreach ((array)$fdList as $fd) {
$res = $GLOBALS['ws']->push($fd, $data);
echo "FD: $fd push $res.\n";
if (!$res) { //推送失败,即客户端已经断开连接
//从该fd订阅的所有股票中删除该fd
$sArrOfFd = $redis->sMembers(REDIS_FD_S.$fd);
if (!empty($sArrOfFd)) {
foreach ((array)$sArrOfFd as $key => $sc) {
$res = $redis->sRem(REDIS_S_FD.$sc, $fd);
$num = $redis->sCard(REDIS_S_FD.$sc);
if ($num == '0') {
$redis->sRem(REDIS_S_KEY, $sc);
$redis->hDel(REDIS_ZS_KEY, $sc);
}
}
}
$redis->del(REDIS_FD_S.$fd);
}
}
}
$redis->close();
}

swoole+Redis实现实时数据推送的更多相关文章

  1. C# ASP.NET MVC 之 SignalR 学习 实时数据推送显示 配合 Echarts 推送实时图表

    本文主要是我在刚开始学习 SignalR 的技术总结,网上找的学习方法和例子大多只是翻译了官方给的一个例子,并没有给出其他一些经典情况的示例,所以才有了本文总结,我在实现推送简单的数据后,就想到了如何 ...

  2. 实时数据推送webSocket

    实时数据推送 在Web或移动项目中,服务器向客户端实时推送消息是一种常见的业务需求. 实现方式 Polling:轮询(俗称“拉”),即定期重新请求数据. Long-Polling:长轮询,是 Poll ...

  3. C# 数据推送 实时数据推送 轻量级消息订阅发布 多级消息推送 分布式推送

    前言 本文将使用一个NuGet公开的组件技术来实现数据订阅推送功能,由服务器进行推送数据,客户端订阅指定的数据后,即可以接收服务器推送过来的数据,包含了自动重连功能,使用非常方便 nuget地址:ht ...

  4. kafka和websocket实时数据推送

    需求 ​ 已有Kafka服务,通过kafka服务数据(GPS)落地到本地磁盘(以文本文件存储).现要根据echarts实现一个实时车辆的地图. 分析 前端实时展现:使用websocket技术,实现服务 ...

  5. 我是如何用redis做实时订阅推送的

    前阵子开发了公司领劵中心的项目,这个项目是以redis作为关键技术落地的.       先说一下领劵中心的项目吧,这个项目就类似京东app的领劵中心,当然图是截取京东的,公司的就不截了...   其中 ...

  6. 我是如何用redis做实时订阅推送的(转)

    前阵子开发了公司领劵中心的项目,这个项目是以redis作为关键技术落地的.       先说一下领劵中心的项目吧,这个项目就类似京东app的领劵中心,当然图是截取京东的,公司的就不截了...   其中 ...

  7. 使用Node.js实现数据推送

    业务场景:后端更新数据推送到客户端(Java部分使用Tomcat服务器). 后端推送数据的解决方案有很多,比如轮询.Comet.WebSocket. 1. 轮询对于后端来说开发成本最低,就是按照传统的 ...

  8. 基于Web的数据推送技术(转)

    基于Web的数据推送技术 对于实时性数据显示要求比较高的系统,比如竞价,股票行情,实时聊天等,我们的解决方案有以下几种.1. HTTP请求发送模式,一般可以基于ajax的请求,比如每3秒一次访问下服务 ...

  9. javascript之数据推送

    我们使用ajax与后台服务进行交互,常常是通过触发事件来单次交互,但对于有些web应用来说,需要前台与后台保持长连接,前端不定时地接收后台推送的数据信息, 例如:股票行情分析.聊天室和网页在线游戏等. ...

随机推荐

  1. centos7网卡名修改

    centos7网卡名不是以etho的方式命名,有时候在自动化方面不便于管理,在安装的时候输入如下代码即可命名: net.ifnames=0  biosdevname=0

  2. 【AtCoder】CODE FESTIVAL 2017 qual C

    A - Can you get AC? No #include <bits/stdc++.h> #define fi first #define se second #define pii ...

  3. Codeforces 498B Name That Tune 概率dp (看题解)

    Name That Tune 刚开始我用前缀积优化dp, 精度炸炸的. 我们可以用f[ i ][ j ] 来推出f[ i ][ j + 1 ], 记得加加减减仔细一些... #include<b ...

  4. BZOJ1293 [SCOI2009]生日礼物 离散化

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1293 题意概括 彩珠有N个,分为K种.每一个彩珠有一个对应的坐标.坐标上可以没有彩珠,多个彩珠也可 ...

  5. routing路由模式

    一:介绍 1.模式 2.应用场景 如果exchangge与队列中的key相同,消息就发送过去. 这个就是需要将交换机与队列增加key. 3.路由类型 上节课的订阅模式中的路由类型是Fanout. 这篇 ...

  6. UVA - 11149 (矩阵快速幂+倍增法)

    第一道矩阵快速幂的题:模板题: #include<stack> #include<queue> #include<cmath> #include<cstdio ...

  7. hdu 1272 小希的迷宫【并查集】

    <题目链接> 小希的迷宫 Problem Description 上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走.但是她设计迷宫的 ...

  8. UVa140 Bandwidth 【最优性剪枝】

    题目链接:https://vjudge.net/contest/210334#problem/F  转载于:https://www.cnblogs.com/luruiyuan/p/5847706.ht ...

  9. vue 百度地图实现标记多个maker,并点击任意一个maker弹出对应的提示框信息, (附: 通过多个地址,标记多个marker 的 方法思路)

    通过点击不同筛选条件,筛选出不同企业所在的地点, 根据每个企业的经纬度 在地图上标记多个maker,点击任意一个maker,会弹出infoWindow 信息窗口: 说明:  因每个人写法不同.需求不同 ...

  10. 洛谷.2051.[AHOI2009]中国象棋(DP)

    题目链接 /* 每行每列不能超过2个棋子,求方案数 前面行对后面行的影响只有 放了0个.1个.2个 棋子的列数,与排列方式无关 所以设f[i][j][k]表示前i行,放了0个棋子的有j列,放了1个棋子 ...