Swoole加速Yii2应用


我们理解和使用yii2和swoole的过程中,总会有一些疑惑、想法。现在记录下来,整理笔记、知识,并将其中的价值传播给他人,分享知识。

本文将重点介绍: (*注: php 7.1 + swoole 1.9)

  • 1.yii2控制台程序如何应用swoole(tcp服务器)
  • 2.yii2restful程序如何应用swoole (http服务器)

1.yii2 控制台程序应用swoole

在yii2控制台程序中,启动一个swoole tcp服务。(将yii2作为容器,其内运行sw服务)

1.1 在Yii2控制器方法中,写一个最简单tcp服务(参照swoole文档api)

<?php
namespace app\commands;
use yii\console\Controller;
class SwTcpController extends Controller
{
    // sw tcp 服务
    private $_tcp;
    
    // 入口函数
    public function actionRun()
    {
        $this->_tcp = new \swoole_server('0.0.0.0', 9503);
        $this->_tcp->on('connect', [$this, 'onConnect']);
        $this->_tcp->on('receive', [$this, 'onReceive']);
        $this->_tcp->on('close', [$this, 'onClose']);
        $this->_tcp->start();
    }
    // sw connect 回调函数
    public function onConnect($server, $fd)
    {
        echo "connection open: {$fd}\n";
    }
    // sw receive 回调函数
    public function onReceive($server, $fd, $reactor_id, $data)
    {
        // 向客户端发送数据
        $server->send($fd, "Swoole: {$data}");
        // 关闭客户端
        $server->close($fd);
    }
    // sw close 回调函数
    public function onClose($server, $fd)
    {
        echo "connection close: {$fd}\n";
    }
}

1.2 写一个tcp客户端测试脚本tcp_client.php(这里采用swoole官网tcp客户端栗子)

<?php
// tcp client
$client = new \swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
$client->on("connect", function ($cli) {
    $cli->send("hello world\n");
});
$client->on("receive", function ($cli, $data) {
    echo "received: {$data}\n";
});
$client->on("error", function ($cli) {
    echo "connect failed\n";
});
$client->on("close", function ($cli) {
    echo "connection close\n";
});
$client->connect("127.0.0.1", 9503, 0.5);

1.3 测试

启动Yii2控制台方法:./yii sw-tcp/run

运行tcp客户端测试脚本:php tcp_client.php

1.4 总结

至此一个基本应用就完成咯,然后我们就可以应用swoole和yii2功能组件的愉快的编写代码了。


2.yii2 restful应用swoole

写一个swoole http 服务脚本,在其onRequest回调函数中,模拟fpm功能,运行yii2应用。(将sw作为容器,其内运行yii2服务)

2.1 这里我们只需要将index.php稍微改造一下即可,在sw中模拟yii2 restful应用请求环境。

<?php
class swHttp {
    // sw http server
    private $_http;
    // yii2 application
    private $_app;
    
    // 程序启动入口
    public function run($conf, $app) {
        $this->_app = $app;
      
        $this->_http = new \swoole_http_server('0.0.0.0', 9501);
        $this->_http->on('start', [$this,'onStart']);
        $this->_http->on('WorkerStart', [$this,'onWorkerStart']);
        $this->_http->on('request', [$this,'onRequest']);
        $this->_http->set($conf);  
        $this->_http->start();
    }

    public function onStart($server) {}
    public function onWorkerStart($server, $worker_id) {}
    
    // sw http onRequest回调函数
    public function onRequest($request, $response) {
        $this->setAppRunEnv($request, $response);
        $this->_app->run();
    }
   // 模拟fpm功能
    public function setAppRunEnv($request, $response) {
        // Yii2 request组件保存$request
        Yii::$app->request->setSwRequest($request);
        // Yii2 response组件保存$response
        Yii::$app->response->setSwResponse($response);

        // 常驻服务需要清除信息
        Yii::$app->request->getHeaders()->removeAll();
        Yii::$app->response->clear();
       
       // 常驻服务需要清除信息
        foreach ($request->server as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
       }
       
       // 设置头信息
        foreach ($request->header as $name => $value) {
            Yii::$app->request->getHeaders()->set($name, $value);
       }
       // 设置请求参数
        Yii::$app->request->setQueryParams($request->get);
        Yii::$app->request->setBodyParams($request->post);
        $rawContent = $request->rawContent() ?: null;
        Yii::$app->request->setRawBody($rawContent);
       // 设置路由
        Yii::$app->request->setPathInfo($request->server['path_info']);
    }
}

//----------------- yii2 web application--------
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../web/Application.php');
$config = include(__DIR__ . '/../config/web.php');
$app = new \app\web\Application($config);
//----------------- sw config -------------------
$swConf = [
  'pid_file'      => __DIR__ . '/server.pid',
  'worker_num'    => 4,
  'max_request'   => 1,
  'daemonize'     => 0,
];
(new swHttp())->run($swConf, $app);

2.2 可能的问题:
q1:模拟fpm功能是什么意思?

a1:每一次请求会在fpm中存储此次请求的相关信息(路由,get,post,server参数),而yii2 restful应用的运行正是依赖这些参数信息。

q2:为什么要在yii2 request、response组件中保存sw request、response对象?

a2:其一需要扩展组件的方法,其二yii2组件需要与sw兼容。

举个栗子,yii2 的 respone组件,其底层最终通过echo 输出数据到客户端,而在swoole中,echo是输出到控制台,需要使用其$respone->end()方法才可以输出数据到客户端。所以需要改写yii2 的response组件底层的输出方法。

q3:如何重新yii2组件?

a3: 通过配置组件类即可,这里以响应组件为栗子,参考下列代码:

1.修改配置文件
'components' => [
        ...
        'response'   => [
            // 这里配置为重写后的response组件类
            'class'  => \app\components\Response::class,
            'format' => \yii\web\Response::FORMAT_JSON,
        ],
        ...

2.具体的重写后的response组件代码:
<?php
namespace app\components;
// 继承底层 web response
class Response extends \yii\web\Response
{
    // 存储sw response对象
    private $_swResponse;
    
    public function setSwResponse($response)
    {
        $this->_swResponse = $response;
    }
    public function getSwResponse()
    {
        return $this->_swResponse;
    }
    
    // 改写底层发送头信息
    public function sendHeaders()
    {
        $headers = $this->getHeaders();
        if ($headers->count > 0) {
            foreach ($headers as $name => $values) {
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
                foreach ($values as $value) {   
                    // 这里需要使用sw response来发送头信息
                    $this->_swResponse->header($name, $value);
                }
            }
        }
        // 这里需要使用sw response来发送状态码
        $this->_swResponse->status($this->getStatusCode());
    }
    
    // 改写底层发送内容
    public function sendContent()
    {
        if ($this->stream === null) {
            if ($this->content) {
                    //echo $this->content;
                    // 这里需要使用sw response来输出
                $this->_swResponse->end($this->content);
            } else {
                $this->_swResponse->end();
            }
            return;
        }
        $chunkSize = 2 * 1024 * 1024; // 2MB per chunk swoole limit
        if (is_array($this->stream)) {
            list ($handle, $begin, $end) = $this->stream;
            fseek($handle, $begin);
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
                if ($pos + $chunkSize > $end) {
                    $chunkSize = $end - $pos + 1;
                }
                // 使用sw response对象来输出
                $this->_swResponse->write(fread($handle, $chunkSize));
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
            }
            fclose($handle);
        } else {
            while (!feof($this->stream)) {
                $this->_swResponse->write(fread($this->stream, $chunkSize));
                flush();
            }
            fclose($this->stream);
        }
        // 使用sw response对象来输出
        $this->_swResponse->end();
    }
}

然后还有我们常用的request组件:

<?php
namespace app\components;
class Request extends \yii\web\Request {
   // 存储 sw request对象
    private $_swRequest;
    
    public function setSwRequest($request) {
        $this->_swRequest = $request;
    }
    public function getSwRequest() {
        return $this->_swRequest;
    }
    /***
    // 重写底层方法的栗子
    public function getQueryParams()
    {
        if ($this->_queryParams === null) {
            //return $_GET;
            // 重写其参数获取方式
            return $this->_swRequest->get;
        }
        return $this->_queryParams;
    }
    ***/
}

这里提一点,在yii2 request组件中我们是有两种思路可以实现参数获取:

​ 1:将组件的getQueryParams方法进行重写。(栗子在上面)

​ 2:不进行过多的底层重写,设置app运行环境变量。(即模拟fmp环境)

这里推荐第二种,环境信息统一管理,做最小的改动,便于维护。

2.3 总结

原来nginx -> fpm -> yii2 restful

现在nginx -> sw -> yii2 restful

整个思路关键在于sw onRequest事件中模拟yii2 restful应用需要的请求信息即可。


3 写在最后

tcp服务比较简单不在赘述,http服务相对来说更复杂写,但是明白其中解析流程,了解其中的生命周期,在结合一些简单的想法,也就不那么难了。

Yii2中的其他组件:
errorHandler组件(必须改写):
sw禁止使用exit/die,所以需改写其中的异常退出方法。

Log组件:
​sw提供异步写文件,写一个异步写日志的组件,提高性能。

async组件:
​sw中的异步赋予yii2组件应用异步能力。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容