项目简介:
PHP项目后台常有下载导出数据的功能,当数据量比较小的时候,查询也比较快, 这时候使用PHP同步代码下载都可以实现,不会出现超时的情况(502/504错误), 当数据量大了之后,查询会很慢,生成的excel也很慢,比如可能需要3分钟, 这个时候同步代码就有问题了。解决方法就是使用异步代码,不超时下载。采用swoole扩展, 集成websocket客户端、websocket服务端、TCP客户端、TCP服务端、swoole_table缓存、 协程mysql、异步task任务。下载生成.xlsx文件采用PhpSpreadsheet类库。
流程说明:
WebSocket客户端->WebSocket服务端->onMessage函数中创建TCP客户端->转发数据至TCP服务端
->异步Task任务处理数据生成excel->Task进程异步处理完成后推送数据至TCP客户端->TCP客户端
接收数据推送至WebSocket客户端。
目录结构:
app --- 服务目录
lib --- PhpSpreadsheet目录
upload --- 生成的xlsx文件目录
demo --- websocket客户端调用代码目录
vendor --- composer目录
启动说明:
php /app/empdown.php 为启动TCP服务端
php /app/empdown_ws.php 为启动WebSocket服务端,并在接收到WebSocket客户端的数据时创建TCP客户端,并转发数据到TCP服务端
备注:
基于swoole扩展开发,需自行安装swoole4.3+扩展
WebSocket服务端代码:
<?php
/**
* Created by PhpStorm.
* User: tanlex
* Date: 2019/5/28
* Time: 15:26
*/
//websocket服务
$ws_server = new Swoole\WebSocket\Server("0.0.0.0", 9602);
//在https站点访问的时候需要设置SSL证书
$ws_server->set(
[
'worker_num' => 4, //worker process num
'max_request' => 3, //每个worker进程任务数
'ssl_cert_file' => '/etc/data/async_excel.cer', //证书
'ssl_key_file' => '/etc/data/async_excel.key', //key
]
);
$ws_server->on('open', function (Swoole\WebSocket\Server $ws_server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
$ws_server->on('message', function (Swoole\WebSocket\Server $ws_server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$data = $frame->data; //接收websocket客户端发送的数据
$fd = $frame->fd; //websocket客户端ID
/********************创建TCP异步客户端********************/
$client = new Swoole\Client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
$client->on("connect", function(swoole_client $cli) use ($data){
//给TCP服务端发送数据(也就是转发websocket客户端发送的数据)
$cli->send($data."\n");
});
//接受TCP服务端的回调函数
$client->on("receive", function(swoole_client $cli, $data) use ($ws_server,$fd){
//调用websocket服务向websocket客户端返回数据
$ws_server->push($fd, $data);
//关闭TCP客户端
$cli->close();
//关闭websocket客户端
$ws_server->close($fd);
});
$client->on("error", function(swoole_client $cli){
echo "error\n";
});
$client->on("close", function(swoole_client $cli){
echo "TCP Connection close\n";
});
$client->connect('127.0.0.1', 9601);
/********************创建TCP异步客户端********************/
});
$ws_server->on('close', function ($ws_server, $fd) {
echo "websocket client: {$fd} closed".PHP_EOL;
});
$ws_server->start();
TCP服务端代码:
<?php
/**
* Created by PhpStorm.
* User: tanlex
* Date: 2019/5/28
* Time: 15:26
*/
//引入MyPhpSpreadsheet类生成excel
require_once __DIR__ . '/../lib/MyPhpSpreadsheet.php';
//创建TCP服务
$serv = new swoole_server("0.0.0.0", 9601);
//设置异步任务的工作进程数量
$serv->set([
'worker_num' => 4, //worker process num
'max_request' => 3, //每个worker进程任务数
'task_worker_num' => 4, //task worker process num
'task_enable_coroutine' => true, //异步任务支持协程
'task_max_request' => 3, //每个task进程任务数
]);
//创建共享内存
$table = new swoole_table(1024);
$table->column('fd', swoole_table::TYPE_INT);
$table->create();
//将table保存在serv对象上
$serv->table = $table;
$serv->on('receive', function($serv, $fd, $from_id, $data) {
//接受TCP客户端数据并投递异步任务
$task_id = $serv->task($data);
//TCP客户端关联任务ID存储到swoole_table
$serv->table->set('empdown_'.$task_id,['fd'=>$fd]);
});
//处理异步任务
$serv->on('Task', function ($serv, Swoole\Server\Task $task) {
/*
//来自哪个`Worker`进程
$task->worker_id;
//任务的编号
$task->id;
//任务的类型,taskwait, task, taskCo, taskWaitMulti 可能使用不同的 flags
$task->flags;
//任务的数据
$task->data;
//协程 API
co::sleep(0.2);
//完成任务,结束并返回数据
$task->finish([123, 'hello']);
*/
//异步任务处理数据
$data = json_decode($task->data);
$keyword = $data->keyword;
$status = $data->status;
$start = $data->start;
$end = $data->end;
$where = 'where 1 ';
if(!empty($keyword)){
$where .= 'and c.name like "%'.$keyword.'%" ';
}
if(!empty($status)){
$where .= 'and c.status='.$status.' ';
}
if(!empty($start)){
$where .= 'and c.create_time>="'.$start.'" ';
}
if(!empty($end)){
$where .= 'and c.create_time<="'.$end.'" ';
}
//协程mysql
$swoole_mysql = new Swoole\Coroutine\MySQL();
$swoole_mysql->connect([
'host' => '127.0.0.1',
'port' => 3306,
'user' => 'root',
'password' => 'root3306',
'database' => 'tang',
]);
// var_dump($swoole_mysql);
$data_sql = "select c.name,c.status,c.create_time
from tang.user c
$where order by c.id desc";
$oneResult = $swoole_mysql->query($data_sql);
// var_dump($oneResult);
//实例化excel生成类
$MyPhpSpreadsheet = new MyPhpSpreadsheet();
//xlsx表格第一行
$title = ['姓名','状态','创建时间'];
$files = $MyPhpSpreadsheet->arrayToXlsx($oneResult,$title);
//返回任务执行的结果
$task->finish(json_encode($files));
});
//处理异步任务的结果
$serv->on('finish', function ($serv, $task_id, $data) {
//获取异步任务结果
echo "AsyncTask[$task_id] Finish: $data".PHP_EOL;
//获取TCP客户端
$fd = $serv->table->get('empdown_'.$task_id,'fd');
if(!empty($fd)){
$serv->send($fd, $data);
$serv->table->del('empdown_'.$task_id);
}
});
//TCP客户端关闭回调
$serv->on('close',function(swoole_server $server, int $fd, int $reactorId){
echo "TCP Client :".$fd." closed ".PHP_EOL;
});
$serv->start();
WebSocket客户端代码:
<script>
var wsServer = 'ws://127.0.0.1:9602';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("onopen");
var message = {
keyword: '唐',
status: '',
start: '',
end: ''
};
websocket.send(JSON.stringify(message)); //给websocket服务端发送数据
};
websocket.onclose = function (evt) {
console.log("onclose");
};
websocket.onmessage = function (evt) {
console.log('onmessage: ' + evt.data);
};
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
</script>
完整代码请移步至:https://github.com/tanlex/async_excel