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

上一篇:

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

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

1、全局变量

包括以下几类

超全局变量, 如:

$_GET/$_POST/$_GLOBAL 等等

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

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

2、数据错乱

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

classTest

{

static$key= [];

}

$http =newSwoole\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、什么情况下可以用全局变量

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

有跨请求的需要

只读

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

4、上下文

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

5、请求的初始协程

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

6、定义Context

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

//file Family/Coroutine/Context.php

namespaceFamily\Coroutine;

classContext

{

/**

*@var\swoole_http_request

*/

private$request;

/**

*@var\swoole_http_response

*/

private$response;

/**

*@vararray 一个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 functiongetRequest()

{

return$this->request;

}

/**

*@return\swoole_http_response

*/

public functiongetResponse()

{

return$this->response;

}

/**

*@param$key

*@param$val

*/

public functionset($key, $val)

{

$this->map[$key] = $val;

}

/**

*@param$key

*@returnmixed|null

*/

public functionget($key)

{

if(isset($this->map[$key])) {

return$this->map[$key];

}

return null;

}

}

7、Context Pool

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

//file Family/Pool/Context.php

namespaceFamily\Pool;

useFamily\Coroutine\Coroutine;

/**

* Class Context

*@packageFamily\Coroutine

*@desc context pool,请求之间隔离,请求之内任何地方可以存取

*/

classContext

{

/**

*@vararray context pool

*/

public static$pool= [];

/**

*@return\Family\Coroutine\Context

*@desc可以任意协程获取到context

*/

public static functiongetContext()

{

$id =Coroutine::getPid();

if(isset(self::$pool[$id])) {

return self::$pool[$id];

}

return null;

}

/**

*@desc清除context

*/

public static functionclear()

{

$id =Coroutine::getPid();

if(isset(self::$pool[$id])) {

unset(self::$pool[$id]);

}

}

/**

*@param$context

*@desc设置context

*/

public static functionset($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)

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

//file Fmaily/Coroutine/Coroutine.php

namespaceFamily\Coroutine;

useSwoole\CoroutineasSwCo;

classCoroutine

{

/**

*@vararray

*@desc保存当前协程根id

* 结构:["当前协程Id"=> "根协程Id"]

*/

public static$idMaps= [];

/**

*@returnmixed

*@desc获取当前协程id

*/

public static functiongetId()

{

returnSwCo::getuid();

}

/**

*@desc父id自设, onRequest回调后的第一个协程,把根协程Id设置为自己

*/

public static functionsetBaseId()

{

$id =self::getId();

self::$idMaps[$id] = $id;

return$id;

}

/**

*@paramnull $id

*@paramint $cur

*@returnint|mixed|null

*@desc获取当前协程根协程id

*/

public static functiongetPid($id =null, $cur =1)

{

if($id ===null) {

$id =self::getId();

}

if(isset(self::$idMaps[$id])) {

return self::$idMaps[$id];

}

return$cur ? $id : -1;

}

/**

*@returnbool

*@throws\Exception

*@desc判断是否是根协程

*/

public static functioncheckBaseCo()

{

$id =SwCo::getuid();

if(!empty(self::$idMaps[$id])) {

return false;

}

if($id !==self::$idMaps[$id]) {

return false;

}

return true;

}

/**

*@param$cb //协程执行方法

*@paramnull $deferCb //defer执行的回调方法

*@returnmixed

* @从协程中创建协程,可保持根协程id的传递

*/

public static functioncreate($cb, $deferCb =null)

{

$nid =self::getId();

returngo(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

*@returnnull

*@desc执行回调函数

*/

public static functioncall($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;

}

/**

*@paramnull $id

*@desc协程退出,清除关系树

*/

public functionclear($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 =newContext($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、使用

//file: application/controller/Index.php

namespacecontroller;

useFamily\Pool\Context;

classIndex

{

public functionindex()

{

//通过context拿到$request, 再也不用担收数据错乱了

$context =Context::getContext();

$request = $context->getRequest();

return'i am family by route!'.json_encode($request->get);

}

public functiontong()

{

return'i am tong ge';

}

}

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

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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181217G12JUA00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券