swoole4.0之打造自己的web开发框架(3)

原创
2018/12/20 13:03
阅读数 186

上一篇:swoole4.0之打造自己的web开发框架(2) 我们实现了异常处理和日志输出,能极大的提升框架的健壮性和为后面的debug也打下了基础,本篇,我们将开始探讨在swoole4.0 协程下的框架需要注意的问题

1、全局变量

包括以下几类

  1. 超全局变量, 如:

    1. $_GET/$_POST/$_GLOBAL 等等

  2. 类的静态的数组,如static $array=array

 

在fpm下,全局变量带来了很多便利,可以让我们随时随地可以存取相关的信息,但在协程模式下,这样就会出现很大的数据错误乱的

 

2、数据错乱

在fpm下,由于我每个进程同时只处理一个请求,所以全局变量怎么读取都没有问题,但在协程下,同一进程可以运行很多的协程,只要当某一个协程修改了全局变量,那所有依赖这个全局变量的协程都数据都变化了,看个简单的例子

 

<?php
class Test
{
    static $key = [];
}

$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->set([
    //"daemonize" => true,
    "worker_num" => 1,
]);
$http->on('request', function ($request, $response) {
    if ($request->server['path_info'] == '/favicon.ico') {
        $response->end('');
        return;
    }
    $key = $request->get['key'];
    Test::$key = $key;
    if ($key == 'sleep') {
        //模拟耗时操作
        Co::sleep(10);
    }
    $response->end(Test::$key);
});
$http->start();

先打开浏览器输入:http://127.0.0.1:9501/?key=sleep  (我们期望输出sleep)

再输入:http://127.0.0.1:9501/?key=abcd

最终第一个输出的是abcd, 这是因为,在第一个请求处理过程中,第二个请求的时候,已以把Test::$key改变了,当第一个请求处理完,读到的数据已经变了
 

所以全局变量在swoole协程下应该非常谨慎的使用

 

3、什么情况下可以用全局变量

我认为满足下面两个条件,可以使用全局变量,否则避要完全避免掉全局变量

  1. 有跨请求的需要

  2. 只读

所以像Family框架里的Config,就属于这种情况, 还有后续会提到的各种资源池

 

4、上下文

一个请求之后的逻辑处理,会经过很多的步骤或流程,那么我们需要有个机制来传递上下文,最典型的做法是,直接通过参数一层层的传递,比如golang的大部分方法第一个参数就是Context, 但在swoole里,我们可以通过更友好的方法来传递

 

5、请求的初始协程

onRequest回调,swoole会自动新建一个协程来处理,通过Coroutine::getuid方法,我们可以拿到当前协程的id,在此基础上,我们就可以打造一个Context池,把每个Context跟据所属的协程ID进行隔离了

 

6、定义Context

我先简单定义一下我们的context(后续根据需要,可扩充),如下:

<?php
//file Family/Coroutine/Context.php
namespace Family\Coroutine;


class Context
{
    /**
     * @var \swoole_http_request
     */
    private $request;
    /**
     * @var \swoole_http_response
     */
    private $response;
    /**
     * @var array 一个array,可以存取想要的任何东西
     */
    private $map = [];

    public function __construct(
    \swoole_http_request $request, 
    \swoole_http_response $response)
    {
        $this->request = $request;
        $this->response = $response;
    }

    /**
     * @return \swoole_http_request
     */
    public function getRequest()
    {
        return $this->request;
    }

    /**
     * @return \swoole_http_response
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * @param $key
     * @param $val
     */
    public function set($key, $val)
    {
        $this->map[$key] = $val;
    }

    /**
     * @param $key
     * @return mixed|null
     */
    public function get($key)
    {
        if (isset($this->map[$key])) {
            return $this->map[$key];
        }

        return null;
    }
}

7、Context Pool

 

我们再定义一个Context Pool, 目标是可以在一个请求里任何地方都可以读到Context

<?php
//file Family/Pool/Context.php
namespace Family\Pool;

use Family\Coroutine\Coroutine;


/**
 * Class Context
 * @package Family\Coroutine
 * @desc context pool,请求之间隔离,请求之内任何地方可以存取
 */
class Context
{
    /**
     * @var array context pool
     */
    public static $pool = [];

    /**
     * @return \Family\Coroutine\Context
     * @desc 可以任意协程获取到context
     */
    public static function getContext()
    {
        $id = Coroutine::getPid();
        if (isset(self::$pool[$id])) {
            return self::$pool[$id];
        }

        return null;
    }

    /**
     * @desc 清除context
     */
    public static function clear()
    {
        $id = Coroutine::getPid();
        if (isset(self::$pool[$id])) {
            unset(self::$pool[$id]);
        }
    }

    /**
     * @param $context
     * @desc 设置context
     */
    public static function set($context)
    {
        $id = Coroutine::getPid();
        self::$pool[$id] = $context;
    }
}

 

8、协程关系

细心的同学会发现在Context pool里有个方法 Coroutine::getPid,貌似不是自带的方法,那这是什么呢?

这是因为,swoole的协程是可以嵌套的,如:

go(functon() {
    //todo
    go(function() {
        //todo
    })
});

Coroutine::getUid是取得当前协程的id,那如果有这样的情况,如果我在一个请求内有协程嵌套,通过当前协程Id是拿不到Context的,所以必需维护一个协程的关系,就是必需要知道当前协程的根协程ID(onRequest回调时创建的Id)

所以我们需要对嵌套协程的创建做一个包装处理

 

<?php
//file Fmaily/Coroutine/Coroutine.php
namespace Family\Coroutine;

use Swoole\Coroutine as SwCo;


class Coroutine
{
    /**
     * @var array
     * @desc 保存当前协程根id
     *      结构:["当前协程Id"=> "根协程Id"]
     */
    public static $idMaps = [];

    /**
     * @return mixed
     * @desc 获取当前协程id
     */
    public static function getId()
    {
        return SwCo::getuid();
    }

    /**
     * @desc 父id自设, onRequest回调后的第一个协程,把根协程Id设置为自己
     */
    public static function setBaseId()
    {
        $id = self::getId();
        self::$idMaps[$id] = $id;
        return $id;
    }

    /**
     * @param null $id
     * @param int $cur
     * @return int|mixed|null
     * @desc 获取当前协程根协程id
     */
    public static function getPid($id = null, $cur = 1)
    {
        if ($id === null) {
            $id = self::getId();
        }
        if (isset(self::$idMaps[$id])) {
            return self::$idMaps[$id];
        }
        return $cur ? $id : -1;
    }

    /**
     * @return bool
     * @throws \Exception
     * @desc 判断是否是根协程
     */
    public static function checkBaseCo()
    {
        $id = SwCo::getuid();
        if (!empty(self::$idMaps[$id])) {
            return false;
        }

        if ($id !== self::$idMaps[$id]) {
            return false;
        }

        return true;
    }

    /**
     * @param $cb //协程执行方法
     * @param null $deferCb //defer执行的回调方法
     * @return mixed
     * @从协程中创建协程,可保持根协程id的传递
     */
    public static function create($cb, $deferCb = null)
    {
        $nid = self::getId();
        return go(function () use ($cb, $deferCb, $nid) {
            $id = SwCo::getuid();
            defer(function () use ($deferCb, $id) {
                self::call($deferCb);
                self::clear($id);
            });

            $pid = self::getPid($nid);
            if ($pid == -1) {
                $pid = $nid;
            }
            self::$idMaps[$id] = $pid;
            self::call($cb);
        });
    }

    /**
     * @param $cb
     * @param $args
     * @return null
     * @desc 执行回调函数
     */
    public static function call($cb, $args)
    {
        if (empty($cb)) {
            return null;
        }
        $ret = null;
        if (\is_object($cb) || (\is_string($cb) && \function_exists($cb))) {
            $ret = $cb(...$args);
        } elseif (\is_array($cb)) {
            list($obj, $mhd) = $cb;
            $ret = \is_object($obj) ? $obj->$mhd(...$args) : $obj::$mhd(...$args);
        }
        return $ret;
    }

    /**
     * @param null $id
     * @desc 协程退出,清除关系树
     */
    public function clear($id = null)
    {
        if (null === $id) {
            $id = self::getId();
        }
        unset(self::$idMaps[$id]);
    }
}

这样,我们在代码中,如果需要用到Context,创建协程则不能直接祼的

go(function() {
    //todo
})

而应该

Coroutine::create(function (){});

这样可以保存协程的关系,在创建的协程内可以正确的拿到Context了, 而且这个还做了自动资源回收的操作

 

9、初始化

最后我们在onRequest入口处,做相关的初始化:

$http->on('request', function (\swoole_http_request $request, \swoole_http_response $response) {
    try {
        //初始化根协程ID
        $coId = Coroutine::setBaseId();
        //初始化上下文
        $context = new Context($request, $response);
        //存放容器pool
        Pool\Context::set($context);
        //协程退出,自动清空
        defer(function () use ($coId) {
            //清空当前pool的上下文,释放资源
            Pool\Context::clear($coId);
        });

        //自动路由
        $result = Route::dispatch($request->server['path_info']);
        $response->end($result);

    } catch (\Exception $e) { //程序异常
        Log::alert($e->getMessage(), $e->getTrace());
        $response->end($e->getMessage());
    } catch (\Error $e) { //程序错误,如fatal error
        Log::emergency($e->getMessage(), $e->getTrace());
        $response->status(500);
    } catch (\Throwable $e) {  //兜底
        Log::emergency($e->getMessage(), $e->getTrace());
        $response->status(500);
    }
});

 

10、使用

<?php
//file: application/controller/Index.php
namespace controller;

use Family\Pool\Context;

class Index
{
    public function index()
    {
        //通过context拿到$request, 再也不用担收数据错乱了
        $context = Context::getContext();
        $request = $context->getRequest();
        return 'i am family by route!' . json_encode($request->get);
    }

    public function tong()
    {
        return 'i am tong ge';
    }

}

 

 

本篇介绍了协程下最大的一个区别:全局变量或数据使用的异同,并给出了一个通用的解决方案,理解了这个,那swoole的开发和正常的web开发没有区别了,下一篇,我们将实现一个完整的CRUD的操作过程,顺带把我们之前用的mysql pool整合进来

 

PS: 不管是在Fpm还是Swoole下,都完全避免用超全局变量

 

github地址: https://github.com/shenzhe/family

 

--------------伟大的分割线----------------

PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!

饭米粒只发原创或授权发表的文章,不转载网上的文章

所发的文章,均可找到原作者进行沟通。

也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。

投稿请联系:

shenzhe163@gmail.com

 

本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部