PHP之高性能I/O框架:Libevent(一)

Libevent 是一個用C語言編寫的、輕量級的開源高性能I/O框架,支持多種 I/O 多路複用技術: epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定時器和信號等事件;註冊事件優先級。PHP提供了對應的擴展 libeventEventphp

libevent擴展好久沒有更新了,僅支持PHP5系列,PHP7雖然有網友fork了 libevent 擴展的源碼進行更新兼容,可是穩定性很差,可能會出現段錯誤,因此PHP7最好使用 Event 擴展。html

與libevent擴展不一樣的是,Event 擴展提供了面向對象的接口,且支持更多特性。java

libevent擴展

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邊緣觸發

使用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個地方不一樣:
一、ev_accept 裏設置read事件全換成了待buffer的函數;
二、ev_read 回調接收參數爲2個;
三、ev_read 回調裏讀取消息使用 event_buffer_read,而不是fread。另外增長了ev_writeev_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_*系列函數基本上能夠分爲上面三大類。還有幾個函數沒有提到,你們看手冊就能瞭解。

(未完待續)


推薦

PHP進階之路

內容概要:從億級 PV 項目的架構梳理,到性能提高實戰,而後在更大致系的系統下,構造並使用服務治理框架。最後不要拘泥於一門語言,使用 java 快速構建一套 api 服務。包含內容:

純乾貨!講師是阿里巴巴資深研發工程師周夢康,《深刻 PHP 內核》做者之一。感興趣的朋友能夠點擊試看!

相關文章
相關標籤/搜索