Tp5.1使用swoole实现异步任务

1,947 阅读9分钟

 一、背景 

         有一个项目是用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多线程