本文参考自 https://wiki.swoole.com/#/start/start_task, 内容有调整
执行异步任务 (Task)
在 Server 程序中如果需要执行很耗时的操作,比如一个聊天服务器发送广播,Web 服务器中发送邮件/短信。如果直接去执行这些函数就会阻塞当前进程,导致服务器响应变慢。
Swoole 提供了异步任务处理的功能,可以投递一个异步任务到 TaskWorker
进程池中执行,不影响当前请求的处理速度。
程序代码
基于我们前面的那个 TCP 服务器,只需要增加 onTask
和 onFinish
2 个事件回调函数即可。另外需要设置 task 进程数量,可以根据任务的耗时和任务量配置适量的 task 进程。
async_task.php
<?php
$server = new Swoole\Server('10.0.2.7', 81);//虚拟机外部访问地址: 127.0.0.1:40181
//设置异步任务的工作进程数量
$server->set([
'task_worker_num' => 1 //这里为了测试多任务的处理, 特地只设置一个, 实际上我们要根据任务数量设置多个进程
]);
$server->on('Start', function($server){
echo 'Swoole AsyncTask TCP server started at'.date('Y-m-d H:i:s').PHP_EOL;
});
//监听连接进入事件
$server->on('Connect', function($server, $fd){
//$fd: 客户端连接的唯一标识, int
echo date('Y-m-d H:i:s').' Client connected fd='.$fd.PHP_EOL;//在服务器命令行端显示文字
$server->send($fd, 'Welcome, your ID is: '.$fd.PHP_EOL);
});
//收到任务, 并投递异步任务 (此回调函数在worker进程中执行)
$server->on('Receive', function($server, $fd, $reactor_id, $data){
//$reactor_id: TCP连接所在的Reactor线程ID, int
//$data: 收到的数据内容,可能是文本或者二进制内容
$data = trim($data);
echo date('Y-m-d H:i:s').' Client '.$fd.' send message: '.$data.PHP_EOL;
//投递异步任务
$task_id = $server->task($data);
$server->send($fd, 'Your task id is '.$task_id.' for : '.$data.PHP_EOL);
echo 'Dispatch AsyncTask from client '.$fd.': id='.$task_id.PHP_EOL;
});
//处理异步任务 (此函数在task进程中执行)
$server->on('Task', function($server, $task_id, $reactor_id, $data){
echo 'New AsyncTask to handle: id='.$task_id.PHP_EOL;
sleep(5);//休眠5秒钟
$strResult = rand() > 0.2 ? 'ok' : 'fail';//我们随机一个结果
//返回执行任务的结果
$server->finish($data.' -> '.$strResult);
});
//处理异步任务的结果,可选(忽略结果) (此回调函数在worker进程中执行)
$server->on('Finish', function($server, $task_id, $data){
echo 'AsyncTask[id='.$task_id.'] ('.$data.') Finished at '.date('Y-m-d H:i:s').PHP_EOL;
});
//监听连接关闭事件
$server->on('Close', function($server, $fd){
echo date('Y-m-d H:i:s').' Client closed: fd='.$fd.PHP_EOL;
});
$server->start();
调用 $serv->task()
后,程序立即返回,继续向下执行代码。onTask 回调函数 Task 进程池内被异步执行。执行完成后调用 $serv->finish()
返回结果。
如果当前待处理任务数量 大于 当前idle的线程, 系统会提示, 但是会在有空闲进程时处理. so, 我们上面的demo代码中设置了 sleep 5秒来测试多个任务的接收和处理.
运行
使用方式与TCP服务一致, 我们这里使用telnet
, 也可以使用netcat
.
php async_task.php
tcp服务开启后, 我们开4个telnet, 连接:
telnet 10.0.2.7 81
执行后我们可以在服务端看到客户端的连接:
客户端显示类似如下:
然后在每个连接中都发送文字add
(只是演示):
因为我们只开启了一个线程, 所以这个线程也会被阻塞. 现实的使用中是要根据实际业务量来设置的.
如果把线程数该为4, 并在 onReceive 函数中给客户端输出信息中增加处理线程的id, 修改后如下:
$server = new Swoole\Server('10.0.2.7', 81);//虚拟机外部访问地址: 127.0.0.1:40181
//设置异步任务的工作进程数量
$server->set([
'task_worker_num' => 4
]);
$server->on('Start', function($server){
echo 'Swoole AsyncTask TCP server started at'.date('Y-m-d H:i:s').PHP_EOL;
});
//监听连接进入事件
$server->on('Connect', function($server, $fd){
//$fd: 客户端连接的唯一标识, int
echo date('Y-m-d H:i:s').' Client connected fd='.$fd.PHP_EOL;//在服务器命令行端显示文字
$server->send($fd, 'Welcome, your ID is: '.$fd.PHP_EOL);
});
//收到任务, 并投递异步任务 (此回调函数在worker进程中执行)
$server->on('Receive', function($server, $fd, $reactor_id, $data){
//$reactor_id: TCP连接所在的Reactor线程ID, int
//$data: 收到的数据内容,可能是文本或者二进制内容
$data = trim($data);
echo date('Y-m-d H:i:s').' Client '.$fd.' send message: '.$data.PHP_EOL;
//投递异步任务
$task_id = $server->task($data);
echo 'Dispatch AsyncTask from client '.$fd.' to reactor_id='.$reactor_id.', task_id='.$task_id.PHP_EOL.PHP_EOL;
$server->send($fd, 'Your task reactor_id='.$reactor_id.', task_id='.$task_id.' for : '.$data.PHP_EOL);
});
//处理异步任务 (此函数在task进程中执行)
$server->on('Task', function($server, $task_id, $reactor_id, $data){
echo 'New AsyncTask to handle at reactor_id='.$reactor_id.', task_id='.$task_id.PHP_EOL;
sleep(5);//休眠
$strResult = rand() > 0.2 ? 'ok' : 'fail';//我们随机一个结果
//返回执行任务的结果
$server->finish($data.' -> '.$strResult);
});
//处理异步任务的结果,可选(忽略结果) (此回调函数在worker进程中执行)
$server->on('Finish', function($server, $task_id, $data){
echo 'AsyncTask[task_id='.$task_id.'] ('.$data.') Finished at '.date('Y-m-d H:i:s').PHP_EOL;
});
//监听连接关闭事件
$server->on('Close', function($server, $fd){
echo date('Y-m-d H:i:s').' Client closed: fd='.$fd.PHP_EOL;
});
$server->start();
启动服务, 并开启5个telnet客户端, 然后随便输入一个命令, 且几乎同一时间操作
奇怪的是, (多次重启服务测试)结果显示只使用了0,1,2这三个线程 那么线程3干嘛去了呢???
参数说明
回调函数中的 reactor_id
和fd
服务器的onConnect
、onReceive
、onClose
回调函数中会携带reactor_id
和fd
两个参数。
$reactor_id
是来自于哪个reactor线程$fd
是TCP
客户端连接的标识符(TCP连接的文件描述符,file description
),在Server
实例中是唯一的,在多个进程内不会重复fd
是一个自增数字,范围是1 ~ 1600万
,fd超过1600万
后会自动从1
开始进行复用
1600w, 这个数字好熟悉啊, 2^24
$fd
是复用的,当连接关闭后fd
会被新进入的连接复用 (不会立即复用, 而是到达1600万以后开始寻找空闲fd复用)- 正在维持的TCP连接
fd
不会被复用
调用Server->send
/Server->close
函数需要传入$fd
参数才能被正确的处理。如果业务中需要发送广播,需要用apc
、redis
、MySQL
、memcache
、Swoole\Table
将fd
的值保存起来。
function my_onReceive($serv, $fd, $reactor_id, $data) {
//向Connection发送数据
$serv->send($fd, 'Swoole: '.$data);
//关闭Connection
$serv->close($fd);
}
fd为什么使用整型
$fd
使用整型而不是使用对象,主要原因是Swoole
是多进程的模型,在Worker
进程/Task
进程中随时可能要访问某一个客户端连接,如果使用对象,那就需要进行Serialize
/Unserialize
, 增加了额外的性能开销。$fd
如果是整数那就可以直接存储传输被使用。
在PHP层可以自行将客户端连接封装成对象。面向对象的好处是可读性更好,对连接的操作可以封装到方法中。如
$connection->send($data);
$connection->close();