记一次结合PHP多进程和socket.io解决问题的经历

  公司是作棋牌游戏的。前段时间接到一个后台人工鉴定并处理通牌做弊玩家的需求,其中须要根据几个玩家的游戏ID查询并计算他们在某段时间内彼此之间玩牌输赢次数和输赢总额。javascript

  牌局数据是存储在日志中心的,他们把牌局数据分红两个表来存储,一个表存储牌局概况数据,例如牌局时间、牌局ID、桌子ID、用户ID等信息,另外一个表则存储每个牌局的详情数据,例如,牌局有多少玩家参与,荷官在哪一轮发了什么牌,玩家每一轮都有什么动做等等。要想计算出几个玩家在某段时间以内玩牌输赢次数和输赢总额,就须要知道每个牌局的详情数据,因此须要针对每个玩家的游戏ID,先查询第一个表,查出全部牌局概况数据列表,而后遍历这个列表,根据每一个牌局的牌局ID、桌子ID,从第二个表中查询每一个牌局的详情数据,全部玩家的全部牌局详情数据都查询完成以后再进行统计。php

  日志中心的同窗给出了查询以上两个表的接口,其中牌局详情的查询接口一次只能查询一个牌局的数据(和他们使用的数据表设计有关)。刚开始个人作法是在js代码中遍历全部给出的玩家ID,先查询出每一个玩家的牌局列表,而后使用第二层循环来调用接口请求每个牌局的详情数据,但这样作的问题是,有些用户在某段时间内的牌局数量是很大的,尽管控制了查询时间段的最大范围,但仍是出现了一个用户几千个牌局的状况,这就意味着浏览器须要几乎在同一时间内对同一个域名的服务器发出几千个请求,而浏览器是基于域名进行并发控制的,超过限制数量的请求会被阻塞,阻塞严重的时候常常致使页面变成空白,好长时间才恢复,获得查询结果。这样的体验显然是不行的。css

  那怎么办呢?在老大的指导下,几经思虑,决定采用PHP多线程结合socket.io来完成这个任务。总体思路是这样的:首先js向PHP发起数据查询请求,PHP收到请求以后不是直接进行数据查询,而是在后台挂载一个进程去处理请求,而后返回一个确认状态值给js,这时js请求暂时结束了。这样作好处有二:其一,js请求的PHP接口是php-fpm运行的,使用php-fpm来fork多进程不太稳定,而使用php比较稳定;其二,能够避免数据查询过程时间太长致使超时。前端

  挂载进程代码示例:java

<?php
$par = ['startTime' => '', 'endTime' => '', 'mids' => $mid, ...];//牌局查询参数
$pKey = 'plog_proccess';//传给命令行的参数,做为进程标识,便于查询统计当前进程数量
$php = '/usr/local/php/bin/php';//php执行文件路径
$file = '/www/query.php';//牌局查询脚本文件
$cmd = $php.' '.$file.' '.$pKey.' '.base64_encode(serialize($par)).' > /dev/null 2>&1 &';//命令
system($cmd);//执行命令,挂载后台进程执行查询

  接下来就要在进程运行的PHP脚本/www/query.php中进行数据查询了。首先遍历每个玩家ID,查出每一个玩家的全部牌局列表,而后遍历每一个玩家的牌局列表,fork多个子进程进行每一个牌局详情数据的查询了,一个子进程负责查询一个牌局的详情数据,并将数据写入文件中,代码示例以下:(注意:如下代码只是基本代码框架,没法直接运行)json

<?php
$pKey = $argv[1];
$par = unserialize(base64_decode($argv[2]));
$mids = $par['mids'];
$max_pnum = 100;//最大子进程数量,避免抢占了过多的资源

for($mids as $mid) {//遍历查询各个用户的牌局数据
	$list = ...;//这里进行当前用户牌局列表数据查询
	$num = count($list);//牌局总数
	$count = 0;//已有多少个牌局在查询
	
	while(true) {//fork多个子进程查询各个牌局的详情数据
		$s = "ps aux|awk '" . '/query.php/ && /' . $pKey . '/ && !/awk/' . "'|wc -l";
		ob_start();
		system($s);
		$pNum = (int)ob_get_clean();//当前查询进程数量
		
		if($count >= $num) {//当前牌局列表都已经交给各个子进程查询了
			if($pnum > 1) {//有子进程没有完成,稍等
				sleep(3);
				continue;
			} else {//全部子进程都已经完成,退出while循环,回到for循环中查询下一个用户的牌局数据
				break;
			}
		} else if($pNum > $max_pnum) {//子进程数量超出限制,稍等
			sleep(3);
			continue;
		}
		
		$rs = $list[$count];//从牌局列表中取出一个牌局来进行牌局详情数据查询
		pcntl_signal(SIGCHLD, SIG_IGN);
		$pid = pcntl_fork();//fork一个子进程,子进程会今后位置开始执行
		if($pid < 0) {//子进程建立失败
			//这里能够作一些日志记录
			
			exit(0);
		}
		if($pid) {//子进程建立成功(主进程逻辑)
			$count ++;
			
		} else if($pid == 0) {//进行牌局详情数据查询(子进程逻辑)
			$pid = posix_setsid();//子进程ID
			//这里根据$rs中的牌局数据进行牌局详情查询,并将获得的数据写入当前子进程专属文件(文件路径+文件名要惟一,可使用时间戳、桌子ID和牌局ID组合表示)
			
			exit(0);//当前子进程任务完成,退出
		}
	}
}
exit(0);//查询完成,主进程退出

  这个PHP后台挂载进程执行完成以后,全部须要查询的牌局数据就已经所有写入文件中了。如今问题来了,PHP应该怎么把这些数据传给前端页面呢?咱们知道http协议是单向协议,只能由前端向服务器主动发起请求,而服务器是没法主动把数据发送给前端的,那怎么办呢?使用socket.io!能够在全部子进程执行完成以后,经过socket.io使用当前sock链接通知js,js收到消息以后即发送请求给一个PHP接口,这个PHP接口的任务即是读取上述多进程在文件中写下的数据,返回给js进行页面渲染。浏览器

  关于socket.io,没有进行过多研究,使用的是公司框架封装好的,固然也可使用原生的,简单教程地址:http://www.workerman.net/phpsocket_io,这里只是简单介绍一下思路。
  首先须要到上面这个地址中下载phpsocket,而后启动一个服务端,注意,只能在命令行中启动,一样能够做为一个后台挂载进程运行。缓存

<?php
require_once __DIR__ . '/socketio/vendor/autoload.php';
use Workerman\Worker;
use PHPSocketIO\SocketIO;

//建立socket.io服务器,监听2021端口
$io = new SocketIO(2021);

//向客户端发送消息,通知数据已查询完成
$io->emit('hello', json_encode([1 => 'hello', 'aaa' => 'ewfewr']));

Worker::runAll();

  而后在客户端js中监听这个消息:服务器

<script src='https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js'></script>
<script>
var socket = io('http://127.0.0.1:2021');
socket.on('hello', function(par){
	//这里即是发送请求到PHP接口进行数据读取了
});
</script>

  如果以为使用原生socket.io麻烦,也可使用封装好的ElephantIO。多线程

  固然,这里有个问题,就是写数据产生的文件会愈来愈多,能够在每次挂载进程进行写文件以前先把以前写的文件(已经没用了的)进行删除:

function rmDataDir($dir) {
	if(!is_dir($dir)) return;
	
	$handle = opendir($dir);
	while($file = readdir($handle)) {
		if(in_array($file, ['.', '..'])) continue;
		
		$str = $dir . $file;
		if(is_dir($str)) {
			rmDataDir($str . '/');
		} else {
			unlink($str);
		}
	}
	closedir($handle);
	$arr = scandir($dir);//readdir()有时候没有识别完全部文件就返回false了。。。
	if(count($arr) <= 2) {//只有.和..的时候能够删除
		rmdir($dir);
	}
}

  同时,因为在这个功能中,每次发送查询数据请求的代价都是比较昂贵的,能够考虑在js中对查询过的数据进行缓存,例如,相同查询条件下相同用户ID,已经查询过的就不须要查询了,直接从js缓存中读取数据进行页面渲染就能够了。

  然而,尽管使用了PHP多进程,可是进行了不少的文件读写操做,磁盘IO也是很耗时间的,因此速度上并无提高多少,只是不会再出现浏览器页面卡死的状况了。这个功能中关于速度的提高不知还有什么更好的方法呢???各位朋友,走过路过,别忘了给下建议哈~

相关文章
相关标签/搜索