degree 2018-03-24 21:21:20 13808次浏览 6条评论 20 7 0

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

本文将重点介绍: (*注: 系统环境:php 5.6 + swoole 1.9)

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

1.yii2 控制台程序应用swoole

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

HOW:1.1 在控制器方法中,参照swoole文档回调函数配置,启动一个最简单tcp服务,然后运行./yii swt-tcp/run 即可。

<?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.php 运行,就可以愉快的进行数据交互了。

<?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);

WHAT:至此一个tcp服务器就完成了,然后我们就可以应用yii2强大的功能组件快乐的编写代码了。

服务端通过解析json中的cmd(命令字)进行分发处理并响应。

2.yii2 restful应用swoole

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

HOW:2.1 这里我们只需要将index.php稍微改造一下即可,把实例化应用主体放到sw workstart进程中,并模拟请求环境。

<?php
class swHttp {
    private $_http;
  	// 程序启动入口
    public function run($conf) {
        $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) {
        // 加载配置文件,启动一个web应用
        $config = include(__DIR__ . '/../config/web.php');
        new \app\web\Application($config);
    }
   
    public function onRequest($request, $response) {
        // 由于sw 接管所有环境变量,这里我们设置模拟yii2应用请求所需要的环境即可
        $this->setYii2Env($request, $response);
        Yii::$app->run();
    }
    
    public function setYii2Env($request, $response) {
        // 在yii2 request组件中保存sw request对象,参考重写后的request组件
        Yii::$app->request->setSwRequest($request);
        // 在yii2 response组件中保存sw response对象,参考重写后的request组件
        Yii::$app->response->setSwResponse($response);
        // 设置yii2请求环境,参考重写后的request组件
        Yii::$app->request->setRequestEnv();
      	// 由于是常驻服务,需要清除上一次响应信息
        Yii::$app->response->clear();
    }
}
//----------------- 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');
//----------------- sw config -------------------
$swConf = [
  'pid_file'      => __DIR__ . '/server.pid',
  'worker_num'    => 4,
  'max_request'   => 1000,
  'daemonize'     => 0,
];
(new swHttp())->run($swConf);

2.2 yii2 request、response组件重写

Q:为什么要重写yii2的组件?

A:其一需要扩展组件的方法,其二需要组件方法需sw兼容。随手一个栗子respone组件来说,其底层最终通过echo 输出数据到客户端,而在swoole中,echo 是输出到控制台,需要使用其$respone->end()方法才可以输出数据到客户端。

Q:如何重写?

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

'components' => [
        ...
        'response'   => [
        	// 重写后的response组件类
            'class'  => \app\components\Response::class,
            'format' => \yii\web\Response::FORMAT_JSON,
        ],

具体的重写后的response组件代码:

<?php
namespace app\components;
// 继承底层 web response
class Response extends \yii\web\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) {     
                    $this->_swResponse->header($name, $value);
                }
            }
        }
        $this->_swResponse->status($this->getStatusCode());
    }
    // 需要重写的方法,这里我们只需要更改echo 为 swoole的response输出即可
    public function sendContent()
    {
        if ($this->stream === null) {
            if ($this->content) {
              	// 输出数据到客户端,这里需要使用sw response对象来输出
                //echo $this->content;
                $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;
                }
                // 同上
                $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);
        }
        // 同上
        $this->_swResponse->end();
    }
}

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

注:如果我们需要通过yii2 request组件获取get参数信息,直接通过getQueryParams方法是取不到参数信息的,因为参数都在sw 的request对象中,所以这里我们有我们有两种方式可以实现参数获取:

​ 1:将其getQueryParams方法进行重写(栗子在下面)。

​ 2:不进行重写,设置环境变量(将sw接收到的get数据放置 $_GET中,栗子在下面)

<?php
namespace app\components;
class Request extends \yii\web\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;
    }
    ***/
    // 模拟fpm请求环境,将sw接收的数据赋值给yii2请求变量中(如果还需要其他信息,自行添加即可)
    public function setRequestEnv() {
        // header 信息设置
        $this->getHeaders()->removeAll();
        foreach ($this->_swRequest->header as $name => $value) {
            $this->getHeaders()->add($name, $value);
        }
        // 参数信息设置
        $_GET                      = isset($this->_swRequest->get) ? $this->_swRequest->get : [];
        $_POST                     = isset($this->_swRequest->post) ? $this->_swRequest->post : [];
        $_SERVER['REQUEST_METHOD'] = $this->_swRequest->server['request_method'];
        
        $this->setBodyParams(null);
        $this->setRawBody($this->_swRequest->rawContent());
        // 路由设置
        $this->setPathInfo($this->_swRequest->server['path_info']);
    }
}

最后提一点Yii2中的其他组件:

errorHandler组件:

​ 由于 sw 程序中禁止使用exit/die 方法,所以 ErrorHandler 需要改写其中的异常退出方法。

Log 组件:

​ sw提供异步写文件,这里是不是可以改进一下,写一个自己的异步写日志的组件,提高性能。

...

3.结尾

tcp服务不在赘述,http服务相对来说更复杂写,但是明白其中解析过程(onRequest事件 -> 设置yii2请求环境-> 运行yii2应用)和运行原理,以及相关组件兼容写法,也就不那么难了。(写了这么多如果还是不明白,扫一扫下面的微信二维码,你就会和我一样,立马解锁更多思路)

最后,如果有不当之处,请以扶正。如果觉得还不错,请点赞、评论、转发。

[]: https://github.com/degree66/swoole-yii2 "github原装代码"

觉得很赞
  • 评论于 2018-03-29 10:44 举报

    666好文章

    1 条回复
    评论于 2018-03-30 09:21 回复

    谢谢,有时间帮忙点github一个小星星

    , 觉得很赞
  • 评论于 2018-04-03 17:30 举报

    总觉得跟swoole整合 怎么弄不不太优雅。按理说 app入口应该类似、\yii\web\Application 一样单起个入口,来跑服务,但是Yii的单例又污染了一些变量和组件。需要大改。。。

    1 条回复
    评论于 2018-04-04 10:00 回复

    这个时候你需要新框架,比如swoft

  • 评论于 2018-06-04 16:48 举报

    写的很好,解惑了一些思路

  • 评论于 2018-07-12 16:37 举报

    用ab压测了下,性能还不如不加swoole,cpu翻了一倍,很疑惑

  • 评论于 2019-04-23 11:10 举报

    控制器写到哪里呢

  • 评论于 2019-05-31 15:40 举报

    [root Ladon2]# ./yii swt-tcp/run
    -bash: ./yii: 权限不够
    [root Ladon2]#

    执行控制台显示 权限不够,怎么解决

您需要登录后才可以评论。登录 | 立即注册