PHP IO程式設計epoll實現方案

語言: CN / TW / HK

點選進入“PHP開源社群”    

免費獲取進階面試、文件、影片資源

什麼是 EPOll

epoll是Linux核心為處理大批量檔案描述符而作了改進的poll,是Linux下多路複用IO介面select/poll的增強版本,它能顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被核心IO事件非同步喚醒而加入Ready佇列的描述符集合就行了。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得使用者空間程式有可能快取IO狀態,減少epoll_wait/epoll_pwait的呼叫,提高應用程式效率。

一、PHP 原生socket 實現IO(select模式)

現階段php原生方法是沒有辦法使用epoll模型的,原生實現只能使用select模型,主要步驟如下:

//1:建立一個socket監聽,引數ip:埠

$socket = stream_socket_server($socket_address);

//2:設定為非阻塞

stream_set_blocking($socket , 0);s



//3:監聽socket 整個程序阻塞在這裡,持續監聽可讀事件

//此處引數均為引用傳遞,在函式中會改變傳值,第一個為要監聽可讀的socket陣列,第二為可寫的sockets,介面詳情參考https://www.php.net/manual/zh/function.stream-select.php

while(true) {

stream_select($sockets, [],[], 60);

foreach ($sockets as $index => $socket) {

//TODO 處理有資料的socket



}

}


優點 : 方便快捷,輕量化,不用引用依賴庫

缺點 : 只能使用select IO 模型,單執行緒最大隻能開啟1024 個檔案,Select 模型效能比較差,對併發比較高的場景不適用

完整demo

<?php



class SocketServer{

//監聽socket

protected $socket = NULL;



//所有的socket連線

protected $sockets = array();



//連線事件回撥

public $onConnect = NULL;



//斷線事件回撥

public $onClose = NULL;



//接收訊息事件回撥

public $onMessage = NULL;



public function __construct($socket_address) {

//建立一個socket監聽

$this->socket = stream_socket_server($socket_address);



//設定為非阻塞

stream_set_blocking($this->socket, 0);



//將socket監聽加入allSockets

$this->sockets[(int)$this->socket] = $this->socket;

}



public function run() {

while(true) {

//不監聽可寫事件與帶外資料事件

$write = $except = array();

//監聽所有的socket事件

$read = $this->sockets;

//整個程序阻塞在這裡,持續監聽可讀事件

//此處引數均為引用傳遞,在函式中會改變傳值

stream_select($read, $write, $except, 60);



//處理所有可讀事件

foreach ($read as $index => $socket) {

//如果是監聽socket,此處表示有新的連線

if ($socket === $this->socket) {

//通過stream_socket_accept獲取新的連線

$new_conn_socket = stream_socket_accept($socket);



if ($this->onConnect) {

//觸發連線事件的回撥,並將當前連線傳遞給回掉函式

call_user_func($this->onConnect, $socket);

}

//記錄此socket連線,以便於sream_select監聽可讀事件

$this->sockets[(int)$new_conn_socket] = $new_conn_socket;

} else

//如果可讀事件不為監聽socket,則表示對應客戶端有資料發過來

{

//從連線中讀取資料

$buffer = fread($socket, 65535);

//如果資料為空,表示客戶端已經斷開連線

if ('' === $buffer || false === $buffer) {

//嘗試觸發onClose回撥

if ($this->onClose) {

call_user_func($this->onClose, $socket);

}

fclose($socket);

//關閉socket連線並從allSockets中刪除

unset($this->sockets[(int)$socket]);

continue;

}

//表示一個正常的連線,已經讀取到訊息,交給回掉函式處理

if ($this->onMessage) {

call_user_func($this->onMessage, $socket, $buffer);

}

}

}

}

}

}



$server = new SocketServer('tcp://0.0.0.0:9501');



$server->onConnect = function ($conn) {

echo 'connect';

};

$server->onClose = function ($conn) {

echo 'close';

};

$server->onMessage = function ($conn, $message) {

$http_resonse = "HTTP/1.1 200 OK\r\n";

$http_resonse .= "Connection: keep-alive\r\n";

$http_resonse .= "Server: php socket server\r\n";

$http_resonse .= "Content-length: 11\r\n\r\n";

$http_resonse .= "hello world";

fwrite($conn, $http_resonse);

};



$server->run();

二、使用event擴充套件實現epoll(或者Libeven 拓展,兩者選一個)

php event就是一個事件庫,對市面上各種常用的IO複用技術的統一封裝,通過它可以實現epoll io模型通訊

拓展地址:https://pecl.php.net/package/event

官網地址:https://bitbucket.org/osmanov/pecl-event/src/master/examples

拓展安裝

# 下載event

wget https://pecl.php.net/get/event-3.0.3.tgz


# 解壓檔案

tar -xf event-3.0.3.tgz


# 進入目錄

cd event-3.0.3


# 執行phpize

/www/server/php/72/bin/phpize


./configure --with-php-config=/www/server/php/72/bin/php-config


# 安裝

make && make install

#修改php.ini配置



extension = /www/server/php/72/lib/php/extensions/no-debug-non-zts-20170718/event.so



Demo 使用:https://cloud.tencent.com/developer/article/1586427

<?php

$s_host = '0.0.0.0';

$i_port = 9501;

$r_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );

socket_set_option( $r_listen_socket, SOL_SOCKET, SO_REUSEADDR, 1 );

socket_bind( $r_listen_socket, $s_host, $i_port );

socket_listen( $r_listen_socket );

// 將$listen_socket設定為非阻塞IO

socket_set_nonblock( $r_listen_socket );



$a_event_array = array();

$a_client_array = array();



// 建立event-base

$o_event_base = new EventBase();

$s_method_name = $o_event_base->getMethod();

if ( 'epoll' != $s_method_name ) {

exit( "not epoll" );

}



function read_callback( $r_connection_socket, $i_event_flag, $o_event_base ) {

$s_content = socket_read( $r_connection_socket, 1024 );

echo "接受到:".$s_content;

// 在這個客戶端連線socket上新增 讀事件

// 當這個客戶端連線socket一旦滿足可寫條件,我們就可以向socket中寫資料了

global $a_event_array;

global $a_client_array;

$o_write_event = new Event( $o_event_base, $r_connection_socket, Event::WRITE | Event::PERSIST, 'write_callback', array(

'content' => $s_content,

) );

$o_write_event->add();

$a_event_array[ intval( $r_connection_socket ) ]['write'] = $o_write_event;

}

function write_callback( $r_connection_socket, $i_event_flag, $a_data ) {

global $a_event_array;

global $a_client_array;

$s_content = $a_data['content'];

foreach( $a_client_array as $r_target_socket ) {

if ( intval( $r_target_socket ) != intval( $r_connection_socket ) ) {

socket_write( $r_target_socket, $s_content, strlen( $s_content ) );

}

}

$o_event = $a_event_array[ intval( $r_connection_socket ) ]['write'];

$o_event->del();

unset( $a_event_array[ intval( $r_connection_socket ) ]['write'] );

}

function accept_callback( $r_listen_socket, $i_event_flag, $o_event_base ) {

global $a_event_array;

global $a_client_array;

// socket_accept接受連線,生成一個新的socket,一個客戶端連線socket

$r_connection_socket = socket_accept( $r_listen_socket );

$a_client_array[] = $r_connection_socket;

// 在這個客戶端連線socket上新增 讀事件

// 也就說 要從客戶端連線上讀取訊息

$o_read_event = new Event( $o_event_base, $r_connection_socket, Event::READ | Event::PERSIST, 'read_callback', $o_event_base );

$o_read_event->add();

$a_event_array[ intval( $r_connection_socket ) ]['read'] = $o_read_event;

}



// 在$listen_socket上新增一個 讀事件

// 為啥是讀事件?

// 因為$listen_socket上發生事件就是:客戶端建立連線

// 所以,應該是讀事件

$o_event = new Event( $o_event_base, $r_listen_socket, Event::READ | Event::PERSIST, 'accept_callback', $o_event_base );

$o_event->add();

//$a_event_array[] = $o_event;

$o_event_base->loop();



優點 : 支援epoll模型,併發效能比較好,足夠靈活,適合寫框架使用

缺點 :文件,教程比較少,很多靠自己摸索,封裝度不高,在專案中使用需要二次封裝

三、Swoole

官網:https://www.swoole.com/

文件:https://wiki.swoole.com/#/

Swoole 高度封裝了各種通訊服務 如:tcp,http,webscoket等,使用簡單方便,內部都是使用epoll呼叫



$server = new Swoole\Server('127.0.0.1', 9503);



$server->on('start', function ($server) {

echo "TCP Server is started at tcp://127.0.0.1:9503\n";

});



$server->on('connect', function ($server, $fd){

echo "connection open: {$fd}\n";

});



$server->on('receive', function ($server, $fd, $reactor_id, $data) {

$server->send($fd, "Swoole: {$data}");

});



$server->on('close', function ($server, $fd) {

echo "connection close: {$fd}\n";

});



$server->start();



優點:可以使用epoll模型,效率高,封裝度高,使用方便,文件完善

缺點:框架龐大,與傳統FPM 程式設計完全不同,很多操作容易引起程式異常,入門門檻比較高

總結

現階段php原生不支援epoll模式的IO呼叫,可以通過三方拓展Event實現epoll呼叫,或者使用swoole 直接建立服務即可

原文連結:http://a.nxw.so/21owAg

END

PHP開源社群

掃描關注  進入”PHP資料“

免費獲取進階

面試、文件、影片資源

點選“檢視原文”獲取更多