/首页
/开源
/关于
PHP网络编程-IO复用之epoll基础(十四)
发表@2020-02-06 19:18:00
更新@2023-03-27 22:19: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 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 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 add(); $o_event_base->loop(); ``` 众所周知,Libevent官网声称TA是完美支持[ Epoll、Select、Kqueue、Poll 】的,那么Event扩展里有方法使我们可以查看这些吗?没有,全剧终...怎么可能会没有?都说是全家桶了,这种基础支持一定是有的,而且TA不仅支持查看支持的IO复用方法,还能配置不使用某种方法,多TM地幸福啊!幸福如我们,就像幸福的猫咪一样~~~  ```php 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 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复用可以做到同时支持这三个选项。