Hello av8d!I'm Li.Old。
事情是这样的,昨天我在家里找HDMI线,从柜子里翻出来了一个陈酿了十年的iPhone 3G(也就是第二代iPhone),这个3G还是我从老赵那里买的,注意是保定那个搞射影的老赵,不是养猪放牛搬砖搞物流的那个老赵。
十年前,iPhone绝对是个稀罕物件。有一次我去老赵家玩PSP死神嘉年华,正在关头上猛烈操作,突然一个被子从天而降就给我罩住了,然后就看到老赵在黑暗中看着一脸惊恐的我说:“来,宝贝儿,给你看个高科技玩意,纯进口的!”然后就见他在一个亮晶晶的屏幕上一滑就听到“嚓”的一声:“看到没?iPhone!新时代智能手机!苹果的!这家伙!”然后他就是开始展示如何操作图片,然后给我打开一个打火机游戏,那个打火机的火焰始终朝上。这会儿他停下来神秘地对我说:“看好了,给你整个更牛逼的!”,然后他就开始冲着iPhone上那个打火机吹气,吹了一口没啥反应,老赵就在小慌乱紧张以及尴尬中又用力使劲吹了一口,这次连唾沫口水都吹出来了,那坨火焰竟然真的随风而倒。然后突然来了一条明华电脑城催店铺租金的短信,然后老赵开始回短信,因为没有官方的中文输入法,所以老赵开始用拼音回复人家,半天蹦不出个屁来,好不容易拼到一半老赵突然一拍脑袋:WC,MD我可以打电话啊!...我是第一次见到这么沙雕的手机,然后我继续玩嘉年华去了。
时代在发展,科技在进步,就像手机从“诺基亚们”变成了“苹果们”,就像select到epoll。不一样的是epoll不是“街机”,TA的文档躲在那个角落里只有小部分人才会时不时查阅复读。
你以为你躲在角落里我就找不到你了吗?
好了废话不多说,让我们开始进入正题!前篇说到PHP不能直接操作epoll的,必须要靠Libevent等事件库的支持才可以,我推荐大家安装的是event扩展,理由是作者在持续更新、支持PHP7、文档完善,而且我还假装大家都知道如何安装该扩展。
在event的文档里,所有的类如下图所示:
看到这么多类,先不用慌。我先介绍下对我们来说最重要的是Event、EventB ase、EventConfig三个类,这三个类的是我们使用Libevent最基础的三个类;其次是EventBuffer和EventBufferEvent两个类,这两个类是Libevent自己发明了一波儿缓冲类工具,非常好用;EventDnsBase、EventHttp*这些类是Libvent封装好的可以直接利用的DNS工具、HTTP工具;EventSslContext类在我们使用SSL的时候起到配置SSL上下文的作用;EventUtil类中提供了几个小方法可以获取socket信息。
Event、EventBase、EventConfig三个类是最基础最重要的,极端地说就算只有这三个类就可以做很多事情了:
- Event,具体的事件。我举个例子昂,比如说来了一个连接,那么就得给这个连接初始化一个Event并标记上可读
- EventBase,我称之为事件基础。所有的Event都需要在EventBase之上运行
- EventConfig,配置类,这个类可以通过参数来操控EventBase
EventBase有点儿像航空母舰,Event像各种各样的舰载机,EventConfig则有点儿类似于航空母舰的舰岛。所以如果我们要在PHP中使用Libevent的话,就要首先准备好航空母舰(初始化EventBase),然后准备各种舰载机(初始化各种Event),然后将舰载机拖到弹射位置(add),
让我们先从一个定时器开始!众所周知,作为PHP版泥腿子一说定时器,绝BI想到的是crontab,难道没了crontab就没法混了么?不,一些人还知道swoole和Workerman。难道生产环境里不安装swoole或者Workerman就没法混了么?不,一些人还知道在Jenkins点两下鼠标就能创建一个定时器。难道没了Jenkins就没法混了么?这个没准可能真就没法混了...
来,老李手把手教你用纯PHP实现一个定时器!
代码语言:javascript复制<?php
while ( true ) {
sleep( 1 );
// 业务逻辑...
}
???这TM也能用???还真能用,你再配合实现一下守护进程,妥妥的。只是这坨代码还是有些问题的:
- 一是太短了,短到没有自信抬起头来,短到无法相信TA能用
- 二是秒级的,时间粒度太大了
来吧,用定时器来初步接触Libevent吧。这种定时器实际上反馈到Libevent里就是一种[ 时间事件 ],来一把代码,你们感受下(一定要看注释!):
代码语言:javascript复制<?php
// 初始化一个空的EventConfig
// 用这个空的EventConfig初始化一个EventBase
// 空的虽然没用,但是可以演示基础用法
$o_event_config = new EventConfig();
$o_event_base = new EventBase( $o_event_config );
// 初始化一个 timer类型的Event
// 注意Event的参数:EventBase $base,mixed $fd,int $what,callable $cb [, mixed $arg = NULL ] )
// 第一个参数:就是EventBase
// 第二个参数:PHP中的stream、socket fd等,如果为event为信号类型那么
// 就是信号名比如SIGHUP、SIGSTOP,如果为时间类型为则为-1
// 第三个参数:event-flag,读就是Event::READ写为Event::WRITE
// Event::SIGNAL表示为信号类型事件,Event::TIMEOUT则表示时间事件
// 值得注意的是,这个地方有一个叫做 Event::PERSIST 的参数,这个参数表示持续
// 如果不加这个参数,这个定时器不会持久,只会执行一次
// 第四个参数:回调函数
// 第五个参数:传递给回调函数的参数
$o_timer_event = new Event( $o_event_base, -1, Event::TIMEOUT | Event::PERSIST, function() {
echo "bingo".PHP_EOL;
} );
// add函数的参数是可选的,参数是一个超时时间
// add函数最大的作用就是将事件挂起准备执行
// 就类似于飞机到航母的弹射器上,准备起飞
// 官方说法叫做:使事件pending起来
$o_timer_event->add( 0.7 );
// 让event_base loop起来~~~我跟你说,你就当是while(true)就行
$o_event_base->loop();
这段代码,就是基于Libevent实现的一个毫秒级的定时器。这坨代码给你自信,因为TA是基于Libevent实现的,一来是说出去的时候听着比较唬人,二来是如果出问题了可以先甩锅给Libevent...
上面的demo用时间事件来说明EventConfig、EventBase、Event三个类大概是怎么使用的。这里我需要强调的Event::PERSIST这个参数选项,这个选项是需要和Event::READ、Event::WRITE等进行[ 或运算 ]产生组合作用,这个参数的意思就是[ 使本事件成为持久事件,而不是一次性事件 ],举个例子上面的定时器代码如果去掉这个配置项,那么这个定时器就仅仅会执行一次。这里我们大胆猜测一下:Workerman或者Swoole里的一次性定时器和持久定时器,大概原理就是这样实现的。
如果我们需要在程序里动态控制事件,比如我们期望在达到某个条件后使得这个事件停止(也就是说使事件不处于pending状态)。demo里已经说明了使得事件pending的方法是add()方法,那么还有一个del()方法可以实现相反的功能,下面这个demo不仅说明了del()用法也说明一下new Event()时候第四个参数(回调函数)与第五个参数(给回调函数的参数)的用法:
代码语言:javascript复制<?php
// 先看下第四个参数回调函数的原型
// callback([ mixed $fd [, int $what [, mixed $arg = NULL ]]] )
// 原型中的fd就是new Event()时候的第二个参数
// 原型中的what就是new Event()时候的第三个参数
// 原型中的$arg才是真正的回调参数~~~
$o_event_config = new EventConfig();
$o_event_base = new EventBase( $o_event_config );
// 注意Event的参数:EventBase $base,mixed $fd,int $what,callable $cb [, mixed $arg = NULL ] )
// 第四个参数:回调函数
// 第五个参数:传递给回调函数的参数
$i_diy = time(); // 这就是我们自定义参数
// 注意回调函数里,我还额外用了use()~~~
// use也是可以用的,你们看下
$o_timer_event = new Event( $o_event_base, -1, Event::TIMEOUT | Event::PERSIST, function( $i_fd, $m_what, $i_diy ) use( &$o_timer_event ) {
// 这句代码可以使用自定义参数
echo "自定义参数:".$i_diy.PHP_EOL;
// 下面代码当触发随机数字2的时候,删除事件
$i_counter = mt_rand( 1, 3 );
if ( 2 == $i_counter ) {
var_dump( $o_timer_event->del() );
}
}, $i_diy );
$o_timer_event->add( 0.5 );
$o_event_base->loop();
其实对于定时器的实现,Event类中还提供了两个更为快捷的方法可以实现,Event::timer()方法相当于实例化一个时间事件,addTimer()方法相当于add(),delTimer相当于del(),实际上底层上应该是一回事,此处就不再赘述了。
除了定时器事件,Libevent还能快速实现信号事件:大概意思就是当进程收到某个进程时候就作出相关相应。前面进程那里我们说通过pcntl_signal()和pcntl_async_signals或pcntl_signal_dispatch()也能实现,而Libevent也能轻松包办这些事儿,你可以理解为“ Libevent全家桶 ”。来个demo你们快速看下:
代码语言:javascript复制<?php
$o_event_config = new EventConfig();
$o_event_base = new EventBase( $o_event_config );
$o_timer_event = new Event( $o_event_base, SIGTERM, Event::SIGNAL | Event::PERSIST, function() {
echo "sigterm".PHP_EOL;
} );
$o_timer_event->add();
$o_event_base->loop();
众所周知,Libevent官网声称TA是完美支持[ Epoll、Select、Kqueue、Poll 】的,那么Event扩展里有方法使我们可以查看这些吗?没有,全剧终...怎么可能会没有?都说是全家桶了,这种基础支持一定是有的,而且TA不仅支持查看支持的IO复用方法,还能配置不使用某种方法,多TM地幸福啊!幸福如我们,就像幸福的猫咪一样~~~
代码语言:javascript复制<?php
// 查看当前支持的IO复用方法
print_r( Event::getSupportedMethods() );
// 查看默认情况下Libevent使用哪个IO复用
$o_event_base = new EventBase();
echo $o_event_base->getMethod().PHP_EOL;
// 某些情况下我们就只需要指定使用poll,比如某些银行软件只认IE8一样...
$o_event_config = new EventConfig();
$o_event_config->avoidMethod( "select" );
$o_event_config->avoidMethod( "epoll" );
$o_event_base = new EventBase( $o_event_config );
echo $o_event_base->getMethod().PHP_EOL;
$o_event_base->loop();
棒不棒?真TM棒!
下面我们聊一个关于epoll的基础点,然后再配合Event表演一波儿。众所周知,epoll有两个很重要的特性:LT与ET:
- LT,全称叫做Level Trigger。这种方式下,如果监听到了有X个事件发生,那么内核态会将这些事件拷贝到用户态,但是可惜的是,如果用户只处理了其中一件,剩余的X-1件出于某种原因并没有理会,那么下次的时候,这些未处理完的X-1个事件依然会从内核态拷贝到用户态。这样做是有阴阳两面的,阳面是事件不会发生丢失,阴面是对于性能来说是一种浪费
- ET,全称叫做Edge Trigger。这种方式下,是鸡血版本的epoll、是释放自我的epoll。这种情况下,如果发生了X个事件,然而你只处理了其中1个事件,那么剩余的X-1个事件就算“丢失”了。性能是上去了,与之俱来的就是可能的事件丢失
这两种模式,我们今天也就初步提一下,具体选择哪个并没有[ 正确与错误 ]之说(这里主要是为了纠正我在Advance-PHP中的错误说法),而是需要结合具体场景和实际情况的。在后面深入的文章里,会详细说这两种情况。今天主要是说明下EventConfig如何控制选择这些Feature。EventConfig有个方法叫做requireFeatures,这个方法接受下列这三个参数之一:
- EventConfig::FEATURE_ET,如果要开启这个选项,那么选用的IO复用方式一定要支持
- EventConfig::FEATURE_O1,选用的IO复用方法必须支持O(1)级别的发现可读/可写的事件
- EventConfig::FEATURE_FDS,选用的IO复用发放不光能支持socket,还能支持其他文件类型的文件描述符
<?php
$o_event_config = new EventConfig();
// 通过requireFeatures方法来配置控制
$o_event_config->requireFeatures( EventConfig::FEATURE_ET );
//$o_event_config->requireFeatures( EventConfig::FEATURE_O1 );
//$o_event_config->requireFeatures( EventConfig::FEATURE_FDS );
$o_event_base = new EventBase( $o_event_config );
// 通过getFeatures获取当前事件base的具体特性
$i_features = $o_event_base->getFeatures();
// 通过&方法,也就是与方法来判断选项是否开启
( $i_features & EventConfig::FEATURE_ET ) and print("ET - edge-triggered IOn");
( $i_features & EventConfig::FEATURE_O1 ) and print("O1 - O(1) operation for adding/deletting eventsn");
( $i_features & EventConfig::FEATURE_FDS ) and print("FDS - arbitrary file descriptor types, and not just socketsn");
$o_event_base->loop();
如果大家有心的话,可以观察到一个现象:在开启EventConfig::FEATURE_ET时候,EventConfig::FEATURE_ET和EventConfig::FEATURE_O1将会同时被开启;而如果最后(也就是第6行)开启EventConfig::FEATURE_FDS,那么EventConfig::FEATURE_ET和EventConfig::FEATURE_O1将会被关闭。
为啥呢?简单说下。当我们在Linux系统下的时候,EventConfig::FEATURE_ET和EventConfig::FEATURE_O1如果被打开,那么IO复用将会采用epoll;然而epoll不支持普通文件,所以当EventConfig::FEATURE_FDS被开启后,O1和ET特性将会被关闭,此时在Linux下poll IO复用是支持普通文件的。那么有同时支持这三个选项的吗?有...你把上述代码弄到Mac下,不出意外的话Kqueue IO复用可以做到同时支持这三个选项。