前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初识PHP版的Libevent(十四节)

初识PHP版的Libevent(十四节)

作者头像
老李秀
发布2020-02-19 11:47:26
9120
发布2020-02-19 11:47:26
举报

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实现一个定时器!

<?php
while ( true ) {
  sleep( 1 );
  // 业务逻辑...
}

???这TM也能用???还真能用,你再配合实现一下守护进程,妥妥的。只是这坨代码还是有些问题的:

  • 一是太短了,短到没有自信抬起头来,短到无法相信TA能用
  • 二是秒级的,时间粒度太大了

来吧,用定时器来初步接触Libevent吧。这种定时器实际上反馈到Libevent里就是一种[ 时间事件 ],来一把代码,你们感受下(一定要看注释!):

<?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()时候第四个参数(回调函数)与第五个参数(给回调函数的参数)的用法:

<?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你们快速看下:

<?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地幸福啊!幸福如我们,就像幸福的猫咪一样~~~

<?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 IO\n");
( $i_features & EventConfig::FEATURE_O1 ) and print("O1 - O(1) operation for adding/deletting events\n");
( $i_features & EventConfig::FEATURE_FDS ) and print("FDS - arbitrary file descriptor types, and not just sockets\n");
$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复用可以做到同时支持这三个选项。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
短信
腾讯云短信(Short Message Service,SMS)可为广大企业级用户提供稳定可靠,安全合规的短信触达服务。用户可快速接入,调用 API / SDK 或者通过控制台即可发送,支持发送验证码、通知类短信和营销短信。国内验证短信秒级触达,99%到达率;国际/港澳台短信覆盖全球200+国家/地区,全球多服务站点,稳定可靠。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档