Lumen + Swoole 探坑小记

注意:本文以 Lumen 为对象进行写作,部分内容适用于 Laravel 但考虑到 Lumen 和 Laravel 在部分实现上有所差异,所以不保证本文提到的内容都也适用于 Laravel。本文写作时间点 Lumen 的版本是 v5.7.4 ,laravel-swoole 的版本是 v2.5.0。
考虑到随着未来版本的更新,部分特性会发生变化,所述内容仅供参考。

Swoole 是一个已经被广泛验证过的生产级别的PHP高性能异步网络框架,其近期逐渐完善的协程特性,更是极大提高了编码的体验。

目前查到这方面的文章大多数 Swoole 非常好 , Swoole 的运行机制能够大幅改善 Laravel、Lumen 的性能,让我们一起装一下 Swoole ,引入一个库,Webbench 对比看一下吧。

但实际上手完成应用,而不仅仅是一个 echo server ,就不是这么美好的事情,将 Swoole 集成到先有的应用系统中,也不仅仅是单靠引入一个库就能完成的。因此作为探坑学习,相对来说轻量级,并不包含过多组件的 Lumen 就是一个不错的选择。

要玩 Swoole 首先还是需要为 PHP 安装 Swoole 扩展,Linux环境下,可以直接使用PECL进行安装。当然,考虑到环境的复杂性,最省心的做法还是使用 Docker。

目前为 Lumen 增加 Swoole 支持的库有 laravel-s 和 laravel-swoole 两个,由于laravel-s 方案默认关闭并且不推荐开启协程,因此这次探坑选择了 laravel-swoole 方案。

在开始探坑前,首先还是需要了解一下 Swoole 的运行机制,以及 laravel-swoole 这个库到底做了什么。这边借用 laravel-swoole 的 Wiki 上的图。

Swoole 服务 通过自行PHP脚本启动之后,Swoole 会创建主要的反射器和管理器,进行管理调度,其中 主反射器 的工作模式和 Nginx 类似。而管理器则用于调度 Woker 和 Task Worker。而实际请求处理逻辑则是在 Worker 里面完成的。

对于 Lumen 来说,它的 Application 会在 Worker 启动时被创建,并且常驻内存,而不会每次接收请求都重新创建,这会有很好的性能,但问题也显而易见,原先我们并不管新的 全局属性、类的静态属性、单例 都会成为问题。

对于 Lumen 来说,其核心的特性,则在于 Service Container,Container 中的单例不会重新创建,每次请求是复用之前的,那之前的请求过程势必会引入很多污染,这些污染如果不清理的话,被带入到之后的请求,就可能引起后面请求的结果和预期不一致。但作为最大单例的 Application 如果清理掉,重新创建的话,那就失去了其加载常驻内存的意义了,因此 laravel-swoole 库采用的方式是引入了 沙盒 Sandbox。

Sandbox 机制 与 静态访问安全

在前面提到 常驻内存 需要格外注意的有 类的静态属性 ,那是不是我们自己写的类注意了就好呢,并不全是,因为在 Lumen 实现本身,也再使用静态属性。最典型的地方便是 Container 和 Facade 。Lumen 借助 Container 类的 静态属性 $instance 实现单例。而 Facade 则使用 静态属性 $app 保存 Application 实例,使用 $resolvedInstance 保存已经通过 Container 解析好的 Facade 实例。

那么 laravel-swoole 的 Sandbox 机制是怎么运作的呢,首先 Swoole 实际请求执行都是在 Worker 中执行的,laravel-swoole 在 Worker 被创建时,创建了 Sandbox 并且对 Lumen 的 Application 进行了初始化,同时预解析了一些常用的,同时不会因为不同请求处理之间会产生污染的 单例,这个预解析部分在其配置文件中是可以配置的。

接下来对于每一次请求来说,Sandbox 都会克隆一个已经创建好的 Application 的副本,并且通过预设的 重置器(Resetter) 对确保克隆的 Application 内已经创建的单例如果有已经绑定 Application 的,重新都绑定到当前克隆后的 Application 上,接着由这个克隆后的副本来处理本次请求的内容。这样看着很完美,但还有什么需要注意的呢,那便是前面说的 Lumen 的静态属性。Container 和 Facede。静态属性在同一个 Worker 中是公共的,不会因为不同请求有 独立的 Application ,静态属性就能被隔离。这个问题无解的话,laravel-swoole 目前的做法是在每一次创建时把当前的 Application 设置到 Container 的 $instance 上,同时清空 Facade 的 $resolvedInstance 。这对于同一时间一个Worker只处理一个请求来说是没问题的,但是 Swoole 的协程特性,使得在协程切换时,Worker 是可以接受并处理下一个分配的请求的。这就使得但凡涉及到 访问 Container 单例 和 Facade 进行服务解析都需要被格外注意。而最典型的就是 服务扩展,以及最为敏感的每次服务的鉴权问题。

对于 Lumen来说,Lumen 提供 app 帮助函数进行的服务解析依赖于 Container 单例,而不管使用哪个 Facade 都依赖于 Facade 的静态属性,因此这两种使用方式是需要被注意的。

比较多见的一种情况是在 Service Provider 中对 某个 Application 的 AuthManager 扩展了 guard 或者 guard driver。但如果使用时,通过Container得到的 AuthManager是新创建的,没有被 Service Provider扩展,则可能会遇到 guard 或者 drive 找不到的错误。

Auth guard [{$name}] is not defined.
Auth driver [{$config[‘driver’]}] for guard [{$name}] is not defined.

laravel-swoole 的文档的 Debug Guideline 会建议,如果遇到授权问题时,需要检查每一次请求的 AuthManager 是不是会被重新解析。但是不是重新解析的关键并不是解析本身,而是何时使用何种方式触发解析,怎么样才是安全的。

前面提到 Sandbox 对于 每一个请求 都会创建一个对应的 Application 的副本,Lumen 的 Application 也充当 Service Container 的功能,因此对于每一个请求都需要单独解析的服务类来说,使用 请求本身的 Application 副本 而不是 公共的 Container 单例、或者 Facade ,是一个相对来说安全的做法。

对于 Service Provider 来说,如果需要获取请求本身的 Application 副本,做法是将 Service Provider 添加到 laravel-swoole 配置文件中的 providers 中,从而laravel-swoole在每次重置 Application 的时候,会重新把新的Application关联到 Service Provider 中,并重新执行 Service Provider 。而在 Service Provider 中,则需要使用 $this->app 来访问被重新关联上的当前请求的  Application 副本。

对于其他类来说,如果需要获取请求本身的 Application 副本,那么最常见的做法就是在构造函数接受参数中接受 Application,并且使用 Application 副本的 Service Container 进行解析,通过依赖注入的方式进行获取。

当然,对于全局公共的,并不涉及到限定于本次请求的类来说,保持原来的方式,不管是用 Sandbox 一开始初始化的 Application 中获取,还是从 某一次请求的 Application 中获取,都是没有问题的。

MySQL 中 ONLY_FULL_GROUP_BY 模式

ONLY_FULL_GROUP_BY 对 GROUP BY 查询的限制

在 SQL 中 利用 GROUP BY 聚合 可以起到将字段相同的记录合并的目的。但也常常被滥用,以及使用不当出现结果与预期不一致的情况。

在 sql_mode 中提供了 ONLY_FULL_GROUP_BY 用以限制在 select 字段中 使用 group by 之外的字段 但没有使用聚合函数的情况。

例如:

select type, price from products group by type 

结果根据 type 聚合,但 price 未使用聚合函数,在未开启 ONLY_FULL_GROUP_BY 的情况下 mysql 会依照聚合顺序 返回默认的结果,而开启之后,则会返回错误:

ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘db.tbs.price’ which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

当前所使用的 sql_mode 可以使用 show variables 查看

show variables like 'sql_mode';

MySQL 5.7 新增 ANY_VALUE()

从 MySQL 5.7 版本开始 默认将 ONLY_FULL_GROUP_BY 设置为启用状态。因此 如果依旧按照原先的方式进行查询 将会出现错误,在开启 ONLY_FULL_GROUP_BY 时 如果对非 GROUP BY 字段需要获取同原先一样的结果,5.7 版本开始则提供了一个 ANY_VALUE() 函数:

select type, ANY_VALUE(price) from products group by type 

Laravel 的 strictMode

对于 Laravel 框架 在 config/database.php 文件启用严格模式(strict 为 true )时 ONLY_FULL_GROUP_BY 设为启用状态

strict 为 true
strictMode 下启用 ONLY_FULL_GROUP_BY

如果使用的 MySQL 版本高于 5.7 则 优先考虑使用 ANY_VALUE() 函数
如果还在使用低版本 MySQL 又不方便升级,可以考虑将 strict 设为 false 关闭 strictMode

PHP中使用curl取得HTTP状态码

今天做了个程序,需要取得HTTP状态码,于是继续搜索无力,果然中文资源非常少,于是去查了PHP的手册。
找到了curl这个小工具。
使用curl需要在php.ini中设置启用才行 >< Windows的服务器中,打开php.ini,找到:
extension=php_curl.dll
去掉前面的注释既可 。

实现代码如下:

$curl = curl_init();
$url=’http://www.smdcn.net’;
curl_setopt($curl, CURLOPT_URL, $url); //设置URL
curl_setopt($curl, CURLOPT_HEADER, 1); //获取Header
curl_setopt($curl,CURLOPT_NOBODY,true); //Body就不要了吧,我们只是需要Head
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); //数据存到成字符串吧,别给我直接输出到屏幕了
$data = curl_exec($curl); //开始执行啦~
echo curl_getinfo($curl,CURLINFO_HTTP_CODE); //我知道HTTPSTAT码哦~
curl_close($curl); //用完记得关掉他

完成~~

QeePHP 使用缓存QCache_File储存单条查询结果

QeePHP 是一个很简单易用的PHP框架,以至于我就看了一晚,就开始用它做应用了(你去死)

好吧,我比较笨,PHP不是很熟,于是之前一直没有搞定如果把查询结果的数据保存起来,最后自制一个超土曲线救国的方式。于是记录下来:

$cache =new QCache_File; //新建一个缓存类,这里用QCache_File,当然QeePHP还支持Memcache和XCache

if ($mycache = $cache->get(“data_”.$dataid)) //尝试取Cache,文档告诉我,如果无法取得会返回false
{
$data= new DataBook($mycache);  //DataBook是一个QeePHP的模型类 QDB_ActiveRecord_Abstract,如果取到数据的话,就用数据创建一个DataBook
}else{
$data = DataBook::find(‘id = ?’,$dataid)->getOne(); //如果没有找到数据,然后就查询数据库
$cache->set(“data_”.$dataid,$data->toArray()); //然后把查到的数据转成Array存到缓存
}

在PHP中使用Google Translate

今天需要在做一个内置的翻译功能,然后找了半天,一直没看到合适的方法,
网络上基本上都是同样一个有问题的代码,于是下面找到一个可以用的代码。
直接file_get_contents取API,然后正则表达式取需要的那部分。

/**
* Translating language with Google API
* @author gabe@fijiwebdesign.com
* @version $id$
* @license – Share-Alike 3.0 (http://creativecommons.org/licenses/by-sa/3.0/)
*
* Google requires attribution for their Language API, please see: http://code.google.com/apis/ajaxlanguage/documentation/#Branding
*
*/
class Google_Translate_API {

/**
* Translate a piece of text with the Google Translate API
* @return String
* @param $text String
* @param $from String[optional] Original language of $text. An empty String will let google decide the language of origin
* @param $to String[optional] Language to translate $text to
*/
static function translate($text, $from = ”, $to = ‘en’) {
$url = ‘http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=’.rawurlencode($text).’&langpair=’.rawurlencode($from.’|’.$to);
$response = file_get_contents(
$url,
null,
stream_context_create(
array(
‘http’=>array(
‘method’=>”GET”,
‘header’=>”Referer: http://”.$_SERVER[‘HTTP_HOST’].”/\r\n”
)
)
)
);
if (preg_match(“/{\”translatedText\”:\”([^\”]+)\”/i”, $response, $matches)) {
return self::_unescapeUTF8EscapeSeq($matches[1]);
}
return false;
}

/**
* Convert UTF-8 Escape sequences in a string to UTF-8 Bytes
* @return UTF-8 String
* @param $str String
*/
static function _unescapeUTF8EscapeSeq($str) {
return preg_replace_callback(“/\\\u([0-9a-f]{4})/i”, create_function(‘$matches’, ‘return html_entity_decode(\’&#x\’.$matches[1].\’;\’, ENT_NOQUOTES, \’UTF-8\’);’), $str);
}
}

// example usage
$text = ‘570話ベースで鷹鰐・ダズワニネタが1本ずつです。smdcn.net’;
$trans_text = Google_Translate_API::translate($text, ‘ja’, ‘zh-cn’);
if ($trans_text !== false) {
echo $trans_text;

充满灾难的一月…

一月才过去十几天可是就已经酿成不少杯具…于是来盘点下…
PHP全局变量未初始化的灾难
在上个月某杂志留意到在介绍PHP漏洞,一个是全局变量初始化,还有一个是远程执行,要说第一个是编程习惯问题,那么第二个我就想不同真的有程序会那么笨的让你远程引用?可惜我忘记如果把两个加起来就…
没错…很久没出现这么让人泪流满面的漏洞,一个人让工具灰客流氓门又找回他们恶心的工作的漏洞…
Discuz全局变量在某些情况未初始化造成的杯具…这个漏洞危害很大,应该说对DZ团队响应能力有很大的考验,受灾用户也是最多的…恩…虽然他需要一定的达成条件…但是那么多用DZ的论坛还怕达成条件的少?当然…被沦为笑话的就是黑X论坛被黑了?
比起DZ,另外一个我朝论坛程序PW也未能幸免…而且存在漏洞文件最多…并且多处存在之前说的两个失误同时有的情况…PW的团队真是太不严谨了…恩…于是两大论坛程序一起杯具…撒花…
我想会有更多的程序可能存在这两个问题,就和当初魔法字符BUG被发现时各大PHP程序都杯具一样的…
如果这还只是站长圈的事的话,那么今早的百度被黑事件就又为本月增添了一笔杯具色彩
被黑的方式和推特被黑如出一辙,黑客的来路也是一样的…ICA…如果说当初推特被黑我们还能怀疑是不是推特哪个工作人员的电脑被黑被人拿到域名管理帐号的话,那么这次矛头显然直指注册商管理系统的漏洞,是的,无论你如何注意你的机房,你的服务器的安全,这并不代表你就不会被黑,任何一个可能忽视的环节都会变为突破口,而DNS作为互联网最基础的服务,他的安全问题确实值得担忧,这是在暴风门之后我们又一次认识了DNS的重要性…
当然,Asol和我提到话外音就是这不仅仅是简单的百度在国外注册的域名被黑的问题,很有可能被借题发挥(至少目前CNNIC和某大学教授已经又站出来说话了),这是不是又是一个对目前把国际域名往海外迁的站长门放出的一个警告,也或者是对“关门”的一个暗示,于是面对域名被新网门clienthold还是域名被ICA黑你觉得那个更难以接受呢?