一、背景
有一个项目是用thinkPHP5.1框架搭建的,然后是前后端分离的,其中有需要进行广播的操作,后端推送消息给前端的用户,目前暂时要推送5万左右的用户,如果放在后台管理页面进行同步操作的话,太慢了,十分影响使用体验,于是决定采用异步的方式,将这些广播的操作分离出去,减少接口的执行时间,提高后台管理页面的使用体验。 网上搜索了一下,PHP里比较好的或者说比较大众的实现异步的方式,推荐使用swoole的还是比较多的,毕竟在GitHub上面star也有15k多了,所以想要尝试使用swoole实现异步操作。 先来当波自来水,慕课网有个叫singwa的老师,有一门关于swoole的课程,是付费的实战课,我买了看,感觉不错,看swoole文档觉得看不进去或者看不懂的同学,我推荐看下视频,能更快一点的入门。真的自来水哈,不是打广告,只是我在学习使用swoole的过程中,确实遇到了一些坑,也有看文档和别人的博客文章,也有在QQ群里面问过别人,也有看慕课网这个视频,问题最终也得到了解决。都是学习的途径,自己选择合适的就好哈。
二、使用笔记
1、swoole安装首先是安装swoole,这个网上的教程还是比较多的,GitHub上面swoole的readme里面都有安装教程,也可以看swoole官方文档。 swoole目前是有两种安装的方式,一种是通过源码的方式安装swoole,这是官方推荐的方法,不过对小白不是很友好,因为其中有一步是需要执行configure文件的命令,是./configure,这样的命令,然后后面需要带参数的,因为需要将编译好的swoole文件夹中的swoole.so放到PHP的扩展目录里面,所以完整的命令是:./configure --with-php-config=你要安装的PHP版本的php-config文件完整路径。这样子的话,安装完了,然后在php.ini里面添加上extension=swoole.so就可以啦,打印phpinfo或者命令行用php -m能看到swoole的话就没有问题了。多个PHP版本的用户一定要注意当前用的PHP版本,然后安装swoole的时候是否是对应的PHP版本的phpize命令和php-config文件,如果不对应的话应该是不能够成功安装swoole的。我自己通过源码安装的时候,总是失败,后来看多了教程和视频之后,发现是./configure的时候,没有配置上对应的php-config文件,导致我的swoole.so文件总是找不到,即便是我自己手动放到php扩展文件夹里面也不行,很是失败。 另外一种安装方式就是pecl自动安装,这个对新手比较友好。先确定下pecl和phpize是否安装,apt install php-pear,apt install php7.3-dev(确定好自己的PHP版本号),都没问题的话执行pecl install swoole,然后在php.ini文件中添加上extension=swoole.so就可以啦,该方式省去了自己配置路径。 swoole安装的相关知识大概就这些,网上的教程都比较多,文章最后我也会把我学习swoole过程中看过的,学习过的文章放上来。 tp5里面有swoole的扩展,叫think-swoole,但是我看GitHub上面star比较少,而且我用了一下,感觉不太顺手,主要是连demo我都跑不起来,就放弃了,想要自己动手直接用swoole。
2、swoole入门
swoole用处很多,像官方文档里面介绍的那样,是PHP的协程高性能网络通信引擎,使用C/C++语言编写,提供了多种通信协议的网络服务器和客户端模块。可以方便快捷的实现TCP/UDP服务、高性能Web、WebSocket服务、物联网、实时通讯、游戏、微服务等。
我用swoole,暂时是用来实现异步任务的,参考官网文档里关于执行异步任务(Task)的内容,异步任务可以基于TCP服务器,只需要增加上onTask和onFinish两个事件回调函数即可。 先看下官方的示例,我们随便新建一个PHP文件,然后在命令行中执行php 文件名就可以。
<?php
$serv = new Swoole\Server("127.0.0.1", 9501);
//设置异步任务的工作进程数量
$serv->set(array('task_worker_num' => 4));
//此回调函数在worker进程中执行
$serv->on('receive', function($serv, $fd, $from_id, $data) {
//投递异步任务
$task_id = $serv->task($data);
echo "Dispatch AsyncTask: id=$task_id\n";
});
//处理异步任务(此回调函数在task进程中执行)
$serv->on('task', function ($serv, $task_id, $from_id, $data) {
echo "New AsyncTask[id=$task_id]".PHP_EOL;
//返回任务执行的结果
$serv->finish("$data -> OK");
});
//处理异步任务的结果(此回调函数在worker进程中执行)
$serv->on('finish', function ($serv, $task_id, $data) {
echo "AsyncTask[$task_id] Finish: $data".PHP_EOL;
});
$serv->start(); 关于回调函数的问题,可能有的人有点懵,官方这里也有介绍,有好几种方法实现回调函数,看完这个的话应该就能弄懂回调函数怎么实现的问题了。当时我看不同的文章,也是一脸懵逼,怎么好几种方法呀,后来看到这个文档才知道原来都是可以的。
刚刚执行完了之后,结果应该如图所示:

命令行中进入等待状态,如果有什么报错的话,可以搜索一下是哪里的问题。
上面是面向过程的方式,改成面向对象的话会更好一点,我自己的SwooleServer.php文件代码如下:
<?php
/** 根据swoole官方文档执行异步任务(task)
* 在TCP服务器的基础上,增加onTask和onFinish 2个事件回调函数
* 另外需要设置 task 进程数量
*/
namespace server;
use app\admin\service\TaskService;
class SwooleServer
{
private $server;
public function __construct()
{
//创建Server对象,监听 127.0.0.1:9501端口
$this->server = new \Swoole\Server("127.0.0.1", 9501);
//设置异步任务的工作进程数量
$this->server->set([
'worker_num' => 4,
'task_worker_num' => 100,
'open_length_check' => true,
'package_length_type' => 'N', //长度字段的类型 N:无符号、网络字节序、4字节 (常用)
'package_length_offset' => 0, //第几个字节是包长度的值
'package_body_offset' => 4, //包体从第4字节开始计算长度
'package_max_length' => 10 * 1024 * 1024, //协议最大长度 允许包的最大长度10MB
'buffer_output_size' => 20 * 1024 * 1024, //设置输出缓冲区的大小
'socket_buffer_size' => 20 * 1024 * 1024, //配置客户端连接的缓存区长度,20MB
]);
//匿名函数方法实现workerstart方法的内容
$this->server->on('WorkerStart', function ($server, $worker_id) {
define("ROOT", realpath("."));
define('APP_PATH', __DIR__ . '/../application/');
//加载基础文件
require __DIR__ . '/../thinkphp/base.php';
});
//此回调函数在worker进程中执行
$this->server->on('Receive', function ($server, $fd, $from_id, $data) {
$info = unpack('N', $data);
$len = $info[1];
$body = substr($data, -$len);
echo "$body\n";
$taskData = json_decode(trim($body), true);
$taskId = $server->task($taskData);
echo "Dispatch AsyncTask[$taskId]\n";
});
//处理异步任务(此回调函数在task进程中执行)
$this->server->on('Task', function ($server, $task_id, $from_id, $data) {
echo "New AsyncTask[$task_id]" . PHP_EOL;
/*注意:核心点都在这里
* 实现思路:client端通过传递不同的type来区分业务队列
*/
switch ($data['type'])
{
//异步广播功能
case 'broadcast':
{
$service = new TaskService();
$service->sendBroadcast($data['data']);
break;
}
case 'email':
{
call_user_func_array(array(
new TaskService(),
'sendEmail'
), array(
$server,
$data['data']
));
break;
}
default:
return [
'type' => 'undefided',
'status' => false
];
}
//不return的话,finish不会被调用
return "Task Finish\n";
});
//匿名函数方法实现finish方法的内容
//处理异步任务的结果(此回调函数在worker进程中执行)
$this->server->on('finish', function ($server, $task_id, $data) {
echo "AsyncTask[$task_id] Finish" . PHP_EOL;
});
//启动服务器
$this->server->start();
}
}
$server = new SwooleServer();上面的文件我是在tp5项目的根目录中建立了一个server文件夹,然后放进去了。
客户端的代码如下:
<?
phpnamespace app\admin\controller;
class SwooleClient{
public function __construct()
{
$this->client = new \swoole_client(SWOOLE_SOCK_TCP);
$this->client->set([
'open_length_check' => true,
'package_max_length' => 10 * 1024 * 1024, //允许包的最大长度10MB
'package_length_type' => 'N', //N:无符号、网络字节序、4字节 (常用)
'package_length_offset' => 0, //整个包头加包体计算长度
'package_body_offset' => '4', //包体从第4字节开始计算长度
'socket_buffer_size' => 20 * 1024 * 1024, //配置客户端连接的缓存区长度,20MB
]);
$this->client->connect('127.0.0.1', 9501, 1);//与server端对应
}
public function sendData($data) {
$data = pack('N', strlen($data)) . $data;
$this->client->send($data);
}
}然后我的TaskService文件中的具体的task方法是实现具体业务的,跟swoole就没有关系啦,就没有放上来。
那是怎么调用的呢,在想要通过swoole实现异步任务的地方,执行如下的代码:
$client = new SwooleClient();
$broadcast = [
'type' => 'broadcast',
'data' => $cidArray,
];
$client->sendData(\GuzzleHttp\json_encode($broadcast));就是新建一个client对象, 然后将数据整理好,转成json格式发送过去。
3、遇到的问题
① 刚开始的时候,传输数据比较小,可以传递过去,但是太多数据之后,会失败,总是只有第一个用户能够接收到推送消息,但是其他的用户都不行,一开始是怀疑传输的数据太大了,5万的数据最终也就1.6MB,其实还好,不算太大,后面找原因,一开始只会在SwooleServer.php里面的方法像onReceive和onTask里面打印数据,都是完整的,没想过在最终执行异步任务的service方法中打印数据看一下,当时以为不行,但是后面试了一下,居然可以,打印出来之后发现最终传递过去的数组只有索引为0的一个数据,看起来应该是call_user_func_array方法的问题,我心思要不直接生成service类然后调用方法?试了一下这样子:
$service = new TaskService();
$service->sendBroadcast($data['data']);居然可以,果断抛弃call_user_func_array方法。
②还是之前失败的时候,怀疑是TCP包传输的有问题,后来看了一些文章和咨询别人,才知道数据比较大的时候,有可能是粘包了之类的问题。明确了问题之后,寻找解决方案,swoole官方有两种解决方法,一种是自己手动分包,增加EOF标记解决粘包问题,但是这种方法不推荐,因为server需要从左到右对数据进行逐字节对比,性能上有影响。第二种方法是固定包头+包体协议,这个方法只需要我们在发送数据的时候打包上我们的数据长度,然后在收到数据的时候,去掉这个长度字段,然后获取到剩下的数据就可以啦,比较方便,你可以看到我的client类中的sendData方法就进行了打包,然后SwooleServer类中的onReceive进行了解包,去掉包体长度字段,然后得到真正的数据。当然了,在client和server都要进行相关的配置,你可以看到,set方法设置了一些字段的值,每个字段的具体意义和用法官方文档都有的,这个我就不做过多介绍啦。
③swoole如何跟tp5更好的结合呢,毕竟我们异步执行任务的方法都是之前tp5里面已经写好了的,不可能在swoole里面再单独重新写,那样子也太费劲了,而且不高效呀。通过查资料,我知道了tp5.1每次访问都会进到public目录里面的index.php文件,然后加载启动文件base.php,有的版本里也叫start.php,可以将这一步放在swoole server里面的WorkerStart方法中,这个方法是在worker进程启动的时候会发生,这样子每一次启动worker都可以加载一下,就解决了我们无法方便使用tp5中写好的方法的问题了。
三、总结
虽然实现了自己需要的功能,但是自己还是小白,singwa老师的课也还没看完,文章也没有完全复现自己探索过程中遇到的所有情况,所有大家有什么不清楚的可以多评论,我们互相交流,共同进步。
参考过的文章链接如下:
粘包问题 www.cnblogs.com/JsonM/artic…
tp5结合问题 bingxiong.vip/16802/
tp5实现swoole异步 my.oschina.net/u/125977/bl…
Task进程异步任务 xiaoxiami.gitbook.io/swoole/swoo…
四、更新
1、swoole不支持Apache多线程