Libevent 是一個用C語言編寫的、輕量級的開源高性能I/O框架,支持多種 I/O 多路復用技術: epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定時器和信號等事件;注冊事件優先級。PHP提供了對應的擴展 libevent、 Event 。
libevent擴展很久沒有更新了,僅支持PHP5系列,PHP7雖然有網友fork了 libevent 擴展的源碼進行更新兼容,但是穩定性不好,可能會出現段錯誤,所以PHP7最好使用 Event 擴展。
與libevent擴展不同的是,Event 擴展提供了面向對象的接口,且支持更多特性。
libevent擴展
libevent地址: http://pecl.php.net/package/libevent
libevent文檔: http://docs.php.net/libevent
系統需要先安裝 Libevent 庫:
yum install libevent-dev
然后安裝PHP擴展。
PHP5安裝:
pecl install libevent-0.1.0
PHP7安裝(不穩定):
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
環境。
基本使用
下面的例子實現了一個單進程的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!
代碼里面我加了很多注釋,基本上能看明白。需要注意的是:
1、event_base
是全局的,只需要創建一次,后續都是event的設置和添加。
2、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邊緣觸發
使用event_buffer
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個地方不同:
1、ev_accept
里設置read事件全換成了待buffer的函數;
2、ev_read
回調接收參數為2個;
3、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 內核》作者之一。感興趣的朋友可以點擊試看!