Libevent 是一個用C語言編寫的、輕量級的開源高性能I/O框架,支持多種 I/O 多路複用技術: epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定時器和信號等事件;註冊事件優先級。PHP提供了對應的擴展 libevent、 Event 。php
libevent擴展好久沒有更新了,僅支持PHP5系列,PHP7雖然有網友fork了 libevent 擴展的源碼進行更新兼容,可是穩定性很差,可能會出現段錯誤,因此PHP7最好使用 Event 擴展。html
與libevent擴展不一樣的是,Event 擴展提供了面向對象的接口,且支持更多特性。java
libevent地址: http://pecl.php.net/package/libevent
libevent文檔: http://docs.php.net/libeventc++
系統須要先安裝 Libevent 庫:git
yum install libevent-dev
而後安裝PHP擴展。github
PHP5安裝:express
pecl install libevent-0.1.0
PHP7安裝(不穩定):segmentfault
git clone https://github.com/expressif/pecl-event-libevent.git cd pecl-event-libevent phpize ./configure make && sudo make install
注:後面的代碼示例均使用的php5.6
+ libevent-0.1.0
環境。api
下面的例子實現了一個單進程的TCP server,基於libevent實現I/O複用,達到高性能。數組
libevent_tcp_server.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $receive = []; $master = []; $buffers = []; $socket = stream_socket_server ("tcp://0.0.0.0:9201", $errno, $errstr); if (false === $socket ) { echo "$errstr($errno)\n"; exit(); } if (!$socket) die($errstr."--".$errno); stream_set_blocking($socket,0); $id = (int)$socket; $master[$id] = $socket; echo "waiting client...\n"; //accept事件回調函數,參數分別是$fd, $events, $arg。 //也就是 event_set 函數的$fd, $events, $arg參數。 function ev_accept($socket, $flag, $base){ global $receive; global $master; global $buffers; $connection = stream_socket_accept($socket); stream_set_blocking($connection, 0); $id = (int)$connection; echo "new Client $id\n"; $event = event_new(); event_set($event, $connection, EV_READ | EV_PERSIST, 'ev_read', $id); event_base_set($event, $base); event_add($event); $master[$id] = $connection; $receive[$id] = ''; $buffers[$id] = $event; // event實例必定要存放在一個全局數組裏面。若是去掉該行,客戶端強制斷開再鏈接,服務端沒法正常收到消息 } //read事件回調函數 function ev_read($buffer, $flag, $id) { global $receive; global $master; global $buffers; //該方法裏的$buffer和$master[$id]指向相同的內容 // var_dump(func_get_args(), $master[$id] ); //循環讀取並解析客戶端消息 while( 1 ) { $read = @fread($buffer, 1024); //客戶端異常斷開 if($read === '' || $read === false){ break; } $pos = strpos($read, "\n"); if($pos === false) { $receive[$id] .= $read; // echo "received:".$read.";not all package,continue recdiveing\n"; }else{ $receive[$id] .= trim(substr ($read,0,$pos+1)); $read = substr($read,$pos+1); switch ( $receive[$id] ){ case "quit": echo "client close conn\n"; // fclose($master[$id]); //斷開客戶端鏈接 // event_del($buffers[$id]); //刪除事件 //下面的寫法與上面調用函數效果同樣,都是關閉客戶端鏈接 unset($master[$id]); unset($buffers[$id]); break; default: // echo "all package:\n"; echo $receive[$id]."\n"; break; } $receive[$id]=''; } } } //建立全局event base $base = event_base_new(); //建立 event $event = event_new(); //設置 event:其中$events設置爲EV_READ | EV_PERSIST ;回調事件爲ev_accept,參數 $base //EV_PERSIST可讓註冊的事件在執行完後不被刪除,直到調用event_del()刪除. event_set($event, $socket, EV_READ | EV_PERSIST, 'ev_accept', $base); // 全局event base添加 當前event event_base_set($event, $base); event_add($event); echo "start run...\n"; //進入事件循環 event_base_loop($base); //下面這句不會被執行 echo "This code will not be executed.\n";
咱們先運行代碼:
$ php libevent_tcp_server.php waiting client... start run...
客戶端使用telnet:
$ telnet 127.0.0.1 9201 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. hello server!
代碼裏面我加了不少註釋,基本上能看明白。須要注意的是:
一、event_base
是全局的,只須要建立一次,後續都是event的設置和添加。
二、event_set
的回調函數有三個參數,分別是$fd
, $events
, $arg
。也就是 event_set 函數的$fd
, $events
, $arg
參數。arg 若是須要多個,能夠爲數組。fd參數實際是保存的客戶端鏈接,是個resource。events參數支持下列這些常量:
EV_TIMEOUT
: 超時。利用事件能夠實現定時器EV_READ
: 只要網絡緩衝中還有數據,回調函數就會被觸發EV_WRITE
: 只要塞給網絡緩衝的數據被寫完,回調函數就會被觸發EV_SIGNAL
: POSIX信號量EV_PERSIST
: 不指定這個屬性的話,回調函數被觸發後事件會被刪除EV_ET
: Edge-Trigger邊緣觸發libevent還提供了event_buffer_
系列函數。手冊裏的解釋是:Libevent在基礎的API裏提供了一層抽象層,使用 buffered event ,咱們無序手動處理I/O。估計是對性能的提高。
示例:
libevent_buffer_tcp_server.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $receive = []; $master = []; $buffers = []; $socket = stream_socket_server ("tcp://0.0.0.0:9201", $errno, $errstr); if (false === $socket ) { echo "$errstr($errno)\n"; exit(); } if (!$socket) die($errstr."--".$errno); stream_set_blocking($socket,0); $id = (int)$socket; $master[$id] = $socket; echo "waiting client...\n"; function ev_accept($socket, $flag, $base){ global $receive; global $master; global $buffers; $connection = stream_socket_accept($socket); stream_set_blocking($connection, 0); $id = (int)$connection; echo "new Client $id\n"; //#1 下面改爲了event_buffer事件,與event事件有些不一樣 //event_buffer_new額外支持寫、錯誤事件 $buffer = event_buffer_new($connection, 'ev_read', 'ev_write', 'ev_error', $id); event_buffer_base_set($buffer, $base); //指定超時時間,單位秒 event_buffer_timeout_set($buffer, 30, 30); //設置水位,參考:https://www.cnblogs.com/nengm1988/p/8203784.html event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff); //設置優先級 event_buffer_priority_set($buffer, 10); //開啓event_buffer event_buffer_enable($buffer, EV_READ | EV_PERSIST); $master[$id] = $connection; $receive[$id] = ''; $buffers[$id] = $buffer; } //#2 read回調,因爲使用了event_buffer,這裏僅接受2個參數,分別是fd和arg function ev_read($buffer, $id) { // var_dump(func_get_args()); global $receive; global $master; global $buffers; while( 1 ) { //#3 使用event_buffer_read,而不是fread $read = @event_buffer_read($buffer, 1024); if($read === '' || $read === false) { break; } $pos = strpos($read, "\n"); if($pos === false) { $receive[$id] .= $read; echo "received:".$read.";not all package,continue recdiveing\n"; }else{ $receive[$id] .= trim(substr ($read,0,$pos+1)); $read = substr($read,$pos+1); switch ( $receive[$id] ){ case "quit": echo "client close conn\n"; unset($master[$id]); unset($buffers[$id]); // fclose($master[$id]); // event_buffer_free($buffers[$id]); break; default: echo "all package:\n"; echo $receive[$id]."\n"; break; } $receive[$id]=''; } } } function ev_write($buffer, $id) { echo "$id -- " ."\n"; } function ev_error($buffer, $error, $id) { echo "ev_error - ".$error."\n"; } $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); echo "start run...\n"; event_base_loop($base);
註釋我都寫了,相比前一個例字,主要有3個地方不一樣:
一、ev_accept
裏設置read事件全換成了待buffer的函數;
二、ev_read
回調接收參數爲2個;
三、ev_read
回調裏讀取消息使用 event_buffer_read
,而不是fread。另外增長了ev_write
,ev_error
回調。
libevent提供了event_timer_*
系列函數,實現一次性定時器,精度微秒。
libevent_timer.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $TIME_INTVAL = 1000000; //單位微秒 //回調函數 function ev_timer($fd, $events, $args){ // var_dump(func_get_args()); //打印結果:參數fd爲NULL,參數events固定爲EV_TIMEOUT常量 static $c; $c++; echo time()." hello\n"; event_timer_add($args[1], $args[0]);//再次添加定時器 if($c > 5){ event_timer_del($args[1]); //刪除定時器 } } $base = event_base_new(); $ev_timer = event_timer_new(); event_timer_set($ev_timer, 'ev_timer', [$TIME_INTVAL, $ev_timer]); event_base_set($ev_timer, $base); event_timer_add($ev_timer, $TIME_INTVAL);//單位微秒 event_base_loop($base);
上面的例子實現了每1秒執行一次回調函數。
使用event_*
系列函數也能夠實現:
libevent_timer2.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $TIME_INTVAL = 1000000; function ev_timer($fd, $events, $args){ static $c; $c++; echo time()." hello\n"; event_timer_add($args[1], $args[0]); if($c > 5){ event_timer_del($args[1]); } } $base = event_base_new(); $event = event_new(); event_set($event, 0, EV_TIMEOUT, 'ev_timer', [$TIME_INTVAL, $event]); event_base_set($event, $base); event_add($event, $TIME_INTVAL); event_base_loop($base);
能夠看出,event_timer_*
系列函數是對event_*
系列函數EV_TIMEOUT
事件的包裝。
event_*
系列函數基本上能夠分爲上面三大類。還有幾個函數沒有提到,你們看手冊就能瞭解。
(未完待續)
推薦
內容概要:從億級 PV 項目的架構梳理,到性能提高實戰,而後在更大致系的系統下,構造並使用服務治理框架。最後不要拘泥於一門語言,使用 java 快速構建一套 api 服務。包含內容:
純乾貨!講師是阿里巴巴資深研發工程師周夢康,《深刻 PHP 內核》做者之一。感興趣的朋友能夠點擊試看!