最近在学习PHP的系统事件驱动(event-base)开发,发现PHP有好几个event扩展,根据底层库依赖分为两类:libevent和libev。libevent可以为文件描述符、信号、超时设定等事件提供了监听回调,支持poll/kqueue/event port/select/epoll。libevent 库的其他组件提供其他功能,包括缓冲的事件系统(用于缓冲发送到客户端/从客户端接收的数据)以及 HTTP、DNS 和 RPC 系统的核心实现。libev提供了各种监听器,包括子进程监听,超时设定,定时器,IO监听,信号监听,文件监视等,支持epoll/kqueue/event ports/inotify/eventfd/signalfd,更快的时钟管理,时间变化检测和修正。PHP依赖libevent扩展有libevent,event,PHP依赖libev扩展则有Ev,libev。
libevent在PHP事件驱动开发上应用广泛,比如workerman,phpDaemon,ReactPHP,Kellner。CentOS上PHP 5.4安装libevent扩展
sudo yum install libevent-devel wget https://pecl.php.net/get/libevent-0.1.0.tgz tar -zxvf libevent-0.1.0.tgz cd libevent-0.1.0 phpize ./configure sudo make sudo make install #增加libevent.so sudo vim /etc/php.ini #是否安装成功 php -m | grep libevent
前面介绍过使用ticks和pcntl_signal来做定时器,然而tick运行机制是PHP解释器每执行 N 条可计时的低级语句就会发生的事件,如果tick值设置小了,会产生频繁的系统调用,设置大了又不能保证及时。使用libevent来设置一个定时器
<?php function print_dot(){ echo "."; } class Timer{ protected $pEventBase; protected $pEvent; public $nInterval = 1; public function __construct(){ $this->pEventBase = event_base_new(); } public function addEvent($p_pFunc, $p_mxArgs = null){ $this->pEvent = event_new(); event_set($this->pEvent, 0, EV_TIMEOUT, $p_pFunc, $p_mxArgs); event_base_set($this->pEvent, $this->pEventBase); } public function loop(){ event_add($this->pEvent, $this->nInterval*1000000); event_base_loop($this->pEventBase); } } $pTimer = new Timer(); $pTimer->addEvent("print_dot"); while(1){ $pTimer->loop(); }
libevent使用也很简单:
- 使用event_base_new和event_new分别创建event_base和event
- 使用event_set为event设置要监听文件描述符fd,比如文件、socke、信号,超时则fd为0,事件类型和回调函数
- 使用event_base_set关联event_base和event
- 使用event_add将设置好的event加入事件监听器
- 调用event_base_loop开始处理事件
官网上有个例子用来做socket监听处理
<?php $socket = stream_socket_server ('tcp://0.0.0.0:2000', $errno, $errstr); stream_set_blocking($socket, 0); $base = event_base_new(); $event = event_new(); event_set($event, $socket, EV_READ | EV_PERSIST, 'ev_accept', $base); event_base_set($event, $base); event_add($event); event_base_loop($base); $GLOBALS['connections'] = array(); $GLOBALS['buffers'] = array(); function ev_accept($socket, $flag, $base) { static $id = 0; $connection = stream_socket_accept($socket); stream_set_blocking($connection, 0); $id += 1; $buffer = event_buffer_new($connection, 'ev_read', NULL, 'ev_error', $id); event_buffer_base_set($buffer, $base); event_buffer_timeout_set($buffer, 30, 30); event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff); event_buffer_priority_set($buffer, 10); event_buffer_enable($buffer, EV_READ | EV_PERSIST); // we need to save both buffer and connection outside $GLOBALS['connections'][$id] = $connection; $GLOBALS['buffers'][$id] = $buffer; } function ev_error($buffer, $error, $id) { event_buffer_disable($GLOBALS['buffers'][$id], EV_READ | EV_WRITE); event_buffer_free($GLOBALS['buffers'][$id]); fclose($GLOBALS['connections'][$id]); unset($GLOBALS['buffers'][$id], $GLOBALS['connections'][$id]); } function ev_read($buffer, $id) { while ($read = event_buffer_read($buffer, 256)) { var_dump($read); } }
相比libevent,event扩展提供了面向对象的方法,支持libevent 2+ 的特性,对HTTP,DNS,OpenSSL等协议操作进行封装。Kellner框架比较有意思,在PHP的libevent扩展基础上将http请求处理封装成了扩展,使用cli模式处理http请求,并给出了基于Zend Framework 2的示例。
libev自称libevent的替代者,克服了libevent的一些不利影响,开销更小,Node JS便是利用它来做事件驱动。相比基于libeventd的扩展,基于libev的ev扩展更新比较积极,支持设置各种的监听器,为感兴趣的事件注册回调,比如文件变化,超时。CentOS上PHP 5.4安装ev扩展
wget https://pecl.php.net/get/ev-0.2.15.tgz tar -zxvf ev-0.2.15 cd ev-0.2.15 phpize ./configure sudo make sudo make install #增加ev.so sudo vim /etc/php.ini #是否安装成功 php -m | grep ev
libev封装了各种监视器,操作也比较简单。
<?php /** * 延迟1秒后执行,不重复 */ $pDelay = new EvTimer(1, 0, function () { echo "1 delay \n"; }); /** * 每隔一秒执行一次的定时器,0秒后执行 */ $pTimer = new EvTimer(0, 1, function () { echo "1 seconds \n"; }); /** * 如果没有其他更高等级的监视器,那么就执行EvIdle,处于低优先级则不执行 */ $pIdle = new EvIdle(function(){ sleep(1); echo "idle timer \n"; },0,2); /** * 每一次loop开始都会执行 */ $pPrepare = new EvPrepare(function(){ echo "before timer \n"; },0); /** * 每一次loop都会执行,可以通过优先级调整执行顺序 */ $c = new EvCheck(function(){ echo "after timer \n"; },0,-1); /** * 定时器,每隔1.5秒后执行一次,0秒后开始 */ $pPeriod = new EvPeriodic(0., 1.5, NULL, function ($w, $revents) { echo time(), PHP_EOL; }); /** * IO输入事件监听,可以拿去监听socket的Ev::WRITE和Ev::READ事件 */ $pReadWatcher = new EvIo(STDIN, Ev::READ, function ($watcher, $revents) { echo "STDIN is readable\n"; }); /** * 注册监听感兴趣的信号 */ $pSignal = new EvSignal(SIGTERM, function ($watcher) { echo "SIGTERM received\n"; $watcher->stop(); }); /** * 文件变化监听器,10秒监测一次 */ $pStatWatcher = new EvStat("/var/log/messages", 10, function ($w) { echo "/var/log/messages changed\n"; $attr = $pStatWatcher->attr(); if ($attr['nlink']) { printf("Current size: %ld\n", $attr['size']); printf("Current atime: %ld\n", $attr['atime']); printf("Current mtime: %ld\n", $attr['mtime']); } else { fprintf(STDERR, "`messages` file is not there!"); $pStatWatcher->stop(); } }); /** * 开始执行Ev::RUN_ONCE则立即执行Ev::RUN_NOWAIT则非阻塞执行 */ Ev::run();
也可以监听子进程
$pid = pcntl_fork(); if ($pid == -1) { fprintf(STDERR, "pcntl_fork failed\n"); } elseif ($pid) { $w = new EvChild($pid, FALSE, function ($w, $revents) { $w->stop(); printf("Process %d exited with status %d\n", $w->rpid, $w->rstatus); }); Ev::run(); // Protect against Zombies pcntl_wait($status); } else { //Forked child exit(2); }
php的libev扩展也实现了libev的所有监视器,提供类似的用法,但比较久没更新了。
在网络编程中,使用事件驱动模型监听感兴趣的事件,结合异步处理,能够大大提高服务器性能。传统服务器模型如Apache为每一个请求生成一个子进程。当用户连接到服务器的一个子进程就产生,并处理连接。每个连接获得一个单独的线程和子进程。当用户请求数据返回时,子进程开始等待数据库操作返回。如果此时另一个用户也请求返回数据,这时就产生了阻塞。以下引用自《使用事件驱动模型实现高效稳定的网络服务器程序》
简单网络编程模型里面,服务器与客户端都是一应一答,大部分的 socket 接口都是阻塞型的。在面对多个客户端的请求时候,最简单的解决方式是在服务器端使用多线程(或多进程)。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。
于是便有了“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。
但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。对付可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。
于是便有了基于事件驱动的非阻塞型服务器,比如Nginx,Node.js。Nginx采用事件驱动,使用epoll事件模型,充分使用异步逻辑,削减了上下文调度开销,并发服务能力更强。Node.js 的异步机制是基于事件的,所有的磁盘 I/O、网络通信、数据库查询都以非阻塞的方式请求,返回的结果由事件循环来处理。Node.js 在执行的过程中会维护一个事件队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式 I/O 请求完成后会被推送到事件队列,等待程序进程进行处理。
参考链接:
libev – a high performance full-featured event loop written in C
Working with events
使用 libevent 和 libev 提高网络应用性能
为什么事件驱动服务器这么火
Asynchronous PHP and Real-time Messaging
react.php 中的异步实现