青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

Fork me on GitHub
隨筆 - 215  文章 - 13  trackbacks - 0
<2025年11月>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456


專注即時(shí)通訊及網(wǎng)游服務(wù)端編程
------------------------------------
Openresty 官方模塊
Openresty 標(biāo)準(zhǔn)模塊(Opm)
Openresty 三方模塊
------------------------------------
本博收藏大部分文章為轉(zhuǎn)載,并在文章開頭給出了原文出處,如有再轉(zhuǎn),敬請保留相關(guān)信息,這是大家對(duì)原創(chuàng)作者勞動(dòng)成果的自覺尊重!!如為您帶來不便,請于本博下留言,謝謝配合。

常用鏈接

留言簿(1)

隨筆分類

隨筆檔案

相冊

Awesome

Blog

Book

GitHub

Link

搜索

  •  

積分與排名

  • 積分 - 220999
  • 排名 - 117

最新評(píng)論

閱讀排行榜

http://blog.csdn.net/shagoo/article/details/6396089
Socket(套接字)一直是網(wǎng)絡(luò)層的底層核心內(nèi)容,也是 TCP/IP 以及 UDP 底層協(xié)議的實(shí)現(xiàn)通道。隨著互聯(lián)網(wǎng)信息時(shí)代的爆炸式發(fā)展,當(dāng)代服務(wù)器的性能問題面臨越來越大的挑戰(zhàn),著名的 C10K 問題(http://www.kegel.com/c10k.html)也隨之出現(xiàn)。幸虧通過大牛們的不懈努力,區(qū)別于傳統(tǒng)的 select/poll 的 epoll/kqueue 方式出現(xiàn)了,目前 linux2.6 以上的內(nèi)核都普遍支持,這是 Socket 領(lǐng)域一項(xiàng)巨大的進(jìn)步,不僅解決了 C10K 問題,也漸漸成為了當(dāng)代互聯(lián)網(wǎng)的底層核心技術(shù)。libevent 庫就是其中一個(gè)比較出彩的項(xiàng)目(現(xiàn)在非常多的開源項(xiàng)目都有用到,包括 Memcached),感興趣的朋友可以研究一下。

由于網(wǎng)絡(luò)上系統(tǒng)介紹這個(gè)部分的文章并不多,而涉及 PHP 的就更少了,所以石頭君在這里希望通過《Socket深度探究4PHP》這個(gè)系列給對(duì)這個(gè)領(lǐng)域感興趣的讀者們一定的幫助,也希望大家能和我一起對(duì)這個(gè)問題進(jìn)行更深入的探討。首先,解釋一下目前 Socket 領(lǐng)域比較易于混淆的概念有:阻塞/非阻塞、同步/異步、多路復(fù)用等。

1、阻塞/非阻塞:這兩個(gè)概念是針對(duì) IO 過程中進(jìn)程的狀態(tài)來說的,阻塞 IO 是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起;相反,非阻塞指在不能立刻得到結(jié)果之前,該函數(shù)不會(huì)阻塞當(dāng)前線程,而會(huì)立刻返回。

2、同步/異步:這兩個(gè)概念是針對(duì)調(diào)用如果返回結(jié)果來說的,所謂同步,就是在發(fā)出一個(gè)功能調(diào)用時(shí),在沒有得到結(jié)果之前,該調(diào)用就不返回;相反,當(dāng)一個(gè)異步過程調(diào)用發(fā)出后,調(diào)用者不能立刻得到結(jié)果,實(shí)際處理這個(gè)調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者。

3、多路復(fù)用(IO/Multiplexing):為了提高數(shù)據(jù)信息在網(wǎng)絡(luò)通信線路中傳輸?shù)男剩谝粭l物理通信線路上建立多條邏輯通信信道,同時(shí)傳輸若干路信號(hào)的技術(shù)就叫做多路復(fù)用技術(shù)。對(duì)于 Socket 來說,應(yīng)該說能同時(shí)處理多個(gè)連接的模型都應(yīng)該被稱為多路復(fù)用,目前比較常用的有 select/poll/epoll/kqueue 這些 IO 模型(目前也有像 Apache 這種每個(gè)連接用單獨(dú)的進(jìn)程/線程來處理的 IO 模型,但是效率相對(duì)比較差,也很容易出問題,所以暫時(shí)不做介紹了)。在這些多路復(fù)用的模式中,異步阻塞/非阻塞模式的擴(kuò)展性和性能最好。

感覺概念很抽象對(duì)吧,“一切答案在于現(xiàn)場”,下面讓我們從三種經(jīng)典的 PHP Socket IO 模型實(shí)例來對(duì)以上的概念再做一次分析:

1、使用 accept 阻塞的古老模型:屬于同步阻塞 IO 模型,代碼如下:

socket_server.php
<?php
/**
 * SocketServer Class
 * By James.Huang <shagoo#gmail.com>
*
*/
set_time_limit(0);
class SocketServer 
{
    private static $socket;
    function SocketServer($port
    {
        global $errno$errstr;
        if ($port < 1024) {
            die("Port must be a number which bigger than 1024/n");
        }
        
        $socket = stream_socket_server("tcp://0.0.0.0:{$port}", $errno$errstr);
        if (!$socketdie("$errstr ($errno)");
        
//        stream_set_timeout($socket, -1); // 保證服務(wù)端 socket 不會(huì)超時(shí),似乎沒用:)
        
        while ($conn = stream_socket_accept($socket, -1)) { // 這樣設(shè)置不超時(shí)才油用
            static $id = 0;
            static $ct = 0;
            $ct_last = $ct;
            $ct_data = '';
            $buffer = '';
            $id++; // increase on each accept
            echo "Client $id come./n";
            while (!preg_match('//r?/n/', $buffer)) { // 沒有讀到結(jié)束符,繼續(xù)讀
//                if (feof($conn)) break; // 防止 popen 和 fread 的 bug 導(dǎo)致的死循環(huán)

                $buffer = fread($conn, 1024);
                echo 'R'; // 打印讀的次數(shù)
                $ct += strlen($buffer);
                $ct_data .= preg_replace('//r?/n/', '', $buffer);
            }
            $ct_size = ($ct - $ct_last) * 8;
            echo "[$id] " . __METHOD__ . " > " . $ct_data . "/n";
            fwrite($conn, "Received $ct_size byte data./r/n");
            fclose($conn);
        }
        
        fclose($socket);
    }
}
new SocketServer(2000);
socket_client.php
<?php
/**
 * Socket Test Client
 * By James.Huang <shagoo#gmail.com>
*
*/
function debug ($msg)
{
//    echo $msg;
    error_log($msg, 3, '/tmp/socket.log');
}
if ($argv[1]) {
    
    $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno$errstr, 30);
    
//    stream_set_blocking($socket_client, 0);
//    stream_set_timeout($socket_client, 0, 100000);

    
    if (!$socket_client) {
        die("$errstr ($errno)");
    } else {
        $msg = trim($argv[1]);
        for ($i = 0; $i < 10; $i++) {
            $res = fwrite($socket_client, "$msg($i)");
            usleep(100000);
            echo 'W'; // 打印寫的次數(shù)
//            debug(fread($socket_client, 1024)); // 將產(chǎn)生死鎖,因?yàn)?nbsp;fread 在阻塞模式下未讀到數(shù)據(jù)時(shí)將等待

        }
        fwrite($socket_client, "/r/n"); // 傳輸結(jié)束符
        debug(fread($socket_client, 1024));
        fclose($socket_client);
    }
}
else {
    
//    $phArr = array();
//    for ($i = 0; $i < 10; $i++) {
//        $phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');
//    }
//    foreach ($phArr as $ph) {
//        pclose($ph);
//    }

    
    for ($i = 0; $i < 10; $i++) {
        system("php ".__FILE__." '{$i}:test'");
    }
}
首先,解釋一下以上的代碼邏輯:客戶端 socket_client.php 循環(huán)發(fā)送數(shù)據(jù),最后發(fā)送結(jié)束符;服務(wù)端 socket_server.php 使用 accept 阻塞方式接收 socket 連接,然后循環(huán)接收數(shù)據(jù),直到收到結(jié)束符,返回結(jié)果數(shù)據(jù)(接收到的字節(jié)數(shù))。雖然邏輯很簡單,但是其中有幾種情況很值得分析一下:

A> 默認(rèn)情況下,運(yùn)行 php socket_client.php test,客戶端打出 10 個(gè) W,服務(wù)端打出若干個(gè) R 后面是接收到的數(shù)據(jù),/tmp/socket.log 記錄下服務(wù)端返回的接收結(jié)果數(shù)據(jù)。這種情況很容易理解,不再贅述。然后,使用 telnet 命令同時(shí)打開多個(gè)客戶端,你會(huì)發(fā)現(xiàn)服務(wù)器一個(gè)時(shí)間只處理一個(gè)客戶端,其他需要在后面“排隊(duì)”;這就是阻塞 IO 的特點(diǎn),這種模式的弱點(diǎn)很明顯,效率極低。

B> 只打開 socket_client.php 第 26 行的注釋代碼,再次運(yùn)行 php socket_client.php test 客戶端打出一個(gè) W,服務(wù)端也打出一個(gè) R,之后兩個(gè)程序都卡住了。這是為什么呢,分析邏輯后你會(huì)發(fā)現(xiàn),這是由于客戶端在未發(fā)送結(jié)束符之前就向服務(wù)端要返回?cái)?shù)據(jù);而服務(wù)端由于未收到結(jié)束符,也在向客戶端要結(jié)束符,造成死鎖。而之所以只打出一個(gè) W 和 R,是因?yàn)?fread 默認(rèn)是阻塞的。要解決這個(gè)死鎖,必須打開 socket_client.php 第 16 行的注釋代碼,給 socket 設(shè)置一個(gè) 0.1 秒的超時(shí),再次運(yùn)行你會(huì)發(fā)現(xiàn)隔 0.1 秒出現(xiàn)一個(gè) W 和 R 之后正常結(jié)束,服務(wù)端返回的接收結(jié)果數(shù)據(jù)也正常記錄了。可見 fread 缺省是阻塞的,我們在編程的時(shí)候要特別注意,如果沒有設(shè)置超時(shí),就很容易會(huì)出現(xiàn)死鎖。

C> 只打開 15 行注釋,運(yùn)行 php socket_client.php test,結(jié)果基本和情況 A 相同,唯一不同的是 /tmp/socket.log 沒有記錄下返回?cái)?shù)據(jù)。這里可以看出客戶端運(yùn)行在阻塞和非阻塞模式的區(qū)別,當(dāng)然在客戶端不在乎接受結(jié)果的情況下,可以使用非阻塞模式來獲得最大效率。

D> 運(yùn)行 php socket_client.php 是連續(xù)運(yùn)行 10 次上面的邏輯,這個(gè)沒什么問題;但是很奇怪的是如果你使用 35 - 41 行的代碼,用 popen 同時(shí)開啟 10 個(gè)進(jìn)程來運(yùn)行,就會(huì)造成服務(wù)器端的死循環(huán),十分怪異!后來經(jīng)調(diào)查發(fā)現(xiàn)只要是用 popen 打開的進(jìn)程創(chuàng)建的連接會(huì)導(dǎo)致 fread 或者 socket_read 出錯(cuò)直接返回空字串,從而導(dǎo)致死循環(huán),查閱 PHP 源代碼后發(fā)現(xiàn) PHP 的 popen 和 fread 函數(shù)已經(jīng)完全不是 C 原生的了,里面都插入了大量的 php_stream_* 實(shí)現(xiàn)邏輯,初步估計(jì)是其中的某個(gè) bug 導(dǎo)致的 Socket 連接中斷所導(dǎo)致的,解決方法就是打開 socket_server.php 中 31 行的代碼,如果連接中斷則跳出循環(huán),但是這樣一來就會(huì)有很多數(shù)據(jù)丟失了,這個(gè)問題需要特別注意!

2、使用 select/poll 的同步模型:屬于同步非阻塞 IO 模型,代碼如下:

select_server.php
<?php
/**
 * SelectSocketServer Class
 * By James.Huang <shagoo#gmail.com>
*
*/
set_time_limit(0);
class SelectSocketServer 
{
    private static $socket;
    private static $timeout = 60;
    private static $maxconns = 1024;
    private static $connections = array();
    function SelectSocketServer($port
    {
        global $errno$errstr;
        if ($port < 1024) {
            die("Port must be a number which bigger than 1024/n");
        }
        
        $socket = socket_create_listen($port);
        if (!$socketdie("Listen $port failed");
        
        socket_set_nonblock($socket); // 非阻塞
        
        while (true
        {
            $readfds = array_merge(self::$connectionsarray($socket));
            $writefds = array();
            
            // 選擇一個(gè)連接,獲取讀、寫連接通道
            if (socket_select($readfds$writefds$e = null$t = self::$timeout)) 
            {
                // 如果是當(dāng)前服務(wù)端的監(jiān)聽連接
                if (in_array($socket$readfds)) {
                    // 接受客戶端連接
                    $newconn = socket_accept($socket);
                    $i = (int) $newconn;
                    $reject = '';
                    if (count(self::$connections) >= self::$maxconns) {
                        $reject = "Server full, Try again later./n";
                    }
                    // 將當(dāng)前客戶端連接放入 socket_select 選擇
                    self::$connections[$i] = $newconn;
                    // 輸入的連接資源緩存容器
                    $writefds[$i] = $newconn;
                    // 連接不正常
                    if ($reject) {
                        socket_write($writefds[$i], $reject);
                        unset($writefds[$i]);
                        self::close($i);
                    } else {
                        echo "Client $i come./n";
                    }
                    // remove the listening socket from the clients-with-data array
                    $key = array_search($socket$readfds);
                    unset($readfds[$key]);
                }
                
                // 輪循讀通道
                foreach ($readfds as $rfd) {
                    // 客戶端連接
                    $i = (int) $rfd;
                    // 從通道讀取
                    $line = @socket_read($rfd, 2048, PHP_NORMAL_READ);
                    if ($line === false) {
                        // 讀取不到內(nèi)容,結(jié)束連接          
                        echo "Connection closed on socket $i./n";
                        self::close($i);
                        continue;
                    }
                    $tmp = substr($line, -1);
                    if ($tmp != "/r" && $tmp != "/n") {
                        // 等待更多數(shù)據(jù)
                        continue;
                    }
                    // 處理邏輯
                    $line = trim($line);
                    if ($line == "quit") {
                        echo "Client $i quit./n";
                        self::close($i);
                        break;
                    }
                    if ($line) {
                        echo "Client $i >>" . $line . "/n";
                    }
                }
                
                // 輪循寫通道
                foreach ($writefds as $wfd) {
                    $i = (int) $wfd;
                    $w = socket_write($wfd, "Welcome Client $i!/n");
                }
            }
        }
    }
    
    function close ($i
    {
        socket_shutdown(self::$connections[$i]);
        socket_close(self::$connections[$i]);
        unset(self::$connections[$i]);
    }
}
new SelectSocketServer(2000);
select_client.php
<?php
/**
 * SelectSocket Test Client
 * By James.Huang <shagoo#gmail.com>
*
*/
function debug ($msg)
{
//    echo $msg;
    error_log($msg, 3, '/tmp/socket.log');
}
if ($argv[1]) {
    
    $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno$errstr, 30);
    
//    stream_set_timeout($socket_client, 0, 100000);
    
    if (!$socket_client) {
        die("$errstr ($errno)");
    } else {
        $msg = trim($argv[1]);
        for ($i = 0; $i < 10; $i++) {
            $res = fwrite($socket_client, "$msg($i)/n");
            usleep(100000);
//            debug(fread($socket_client, 1024)); // 將產(chǎn)生死鎖,因?yàn)?nbsp;fread 在阻塞模式下未讀到數(shù)據(jù)時(shí)將等待
        }
        fwrite($socket_client, "quit/n"); // add end token
        debug(fread($socket_client, 1024));
        fclose($socket_client);
    }
}
else {
    
    $phArr = array();
    for ($i = 0; $i < 10; $i++) {
        $phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');
    }
    foreach ($phArr as $ph) {
        pclose($ph);
    }
    
//    for ($i = 0; $i < 10; $i++) {
//        system("php ".__FILE__." '{$i}:test'");
//    }

}
以上代碼的邏輯也很簡單,select_server.php 實(shí)現(xiàn)了一個(gè)類似聊天室的功能,你可以使用 telnet 工具登錄上去,和其他用戶文字聊天,也可以鍵入“quit”命令離開;而 select_client.php 則模擬了一個(gè)登錄用戶連續(xù)發(fā) 10 條信息,然后退出。這里也分析兩個(gè)問題:

A> 這里如果我們執(zhí)行 php select_client.php 程序?qū)?huì)同時(shí)打開 10 個(gè)連接,同時(shí)進(jìn)行模擬登錄用戶操作;觀察服務(wù)端打印的數(shù)據(jù)你會(huì)發(fā)現(xiàn)服務(wù)端確實(shí)是在同時(shí)處理這些連接,這就是多路復(fù)用實(shí)現(xiàn)的非阻塞 IO 模型,當(dāng)然這個(gè)模型并沒有真正的實(shí)現(xiàn)異步,因?yàn)樽罱K服務(wù)端程序還是要去通道里面讀取數(shù)據(jù),得到結(jié)果后同步返回給客戶端。如果這次你也使用 telnet 命令同時(shí)打開多個(gè)客戶端,你會(huì)發(fā)現(xiàn)服務(wù)端可以同時(shí)處理這些連接,這就是非阻塞 IO,當(dāng)然比古老的阻塞 IO 效率要高多了,但是這種模式還是有局限的,繼續(xù)看下去你就會(huì)發(fā)現(xiàn)了~

B> 我在 select_server.php 中設(shè)置了幾個(gè)參數(shù),大家可以調(diào)整試試:
$timeout :表示的是 select 的超時(shí)時(shí)間,這個(gè)一般來說不要太短,否則會(huì)導(dǎo)致 CPU 負(fù)載過高。
$maxconns :表示的是最大連接數(shù),客戶端超過這個(gè)數(shù)的話,服務(wù)器會(huì)拒絕接收。這里要提到的一點(diǎn)是,由于 select 是通過句柄來讀寫的,所以會(huì)受到系統(tǒng)默認(rèn)參數(shù) __FD_SETSIZE 的限制,一般默認(rèn)值為 1024,修改的話需要重新編譯內(nèi)核;另外通過測試發(fā)現(xiàn) select 模式的性能會(huì)隨著連接數(shù)的增大而線性便差(詳情見《Socket深度探究4PHP(二)》),這也就是 select 模式最大的問題所在,所以如果是超高并發(fā)服務(wù)器建議使用下一種模式。

3、使用 epoll/kqueue 的異步模型:屬于異步阻塞/非阻塞 IO 模型,代碼如下:

epoll_server.php
<?php
/**
 * EpollSocketServer Class (use libevent)
 * By James.Huang <shagoo#gmail.com>
 * 
 * Defined constants:
 * 
 * EV_TIMEOUT (integer)
 * EV_READ (integer)
 * EV_WRITE (integer)
 * EV_SIGNAL (integer)
 * EV_PERSIST (integer)
 * EVLOOP_NONBLOCK (integer)
 * EVLOOP_ONCE (integer)
*
*/
set_time_limit(0);
class EpollSocketServer
{
    private static $socket;
    private static $connections;
    private static $buffers;
    
    function EpollSocketServer ($port)
    {
        global $errno$errstr;
        
        if (!extension_loaded('libevent')) {
            die("Please install libevent extension firstly/n");
        }
        
        if ($port < 1024) {
            die("Port must be a number which bigger than 1024/n");
        }
        
        $socket_server = stream_socket_server("tcp://0.0.0.0:{$port}", $errno$errstr);
        if (!$socket_serverdie("$errstr ($errno)");
        
        stream_set_blocking($socket_server, 0); // 非阻塞
        
        $base = event_base_new();
        $event = event_new();
        event_set($event$socket_server, EV_READ | EV_PERSIST, array(__CLASS__, 'ev_accept'), $base);
        event_base_set($event$base);
        event_add($event);
        event_base_loop($base);
        
        self::$connections = array();
        self::$buffers = array();
    }
    
    function ev_accept($socket$flag$base
    {
        static $id = 0;
    
        $connection = stream_socket_accept($socket);
        stream_set_blocking($connection, 0);
    
        $id++; // increase on each accept
    
        $buffer = event_buffer_new($connectionarray(__CLASS__, 'ev_read'), array(__CLASS__, 'ev_write'), array(__CLASS__, '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
        self::$connections[$id] = $connection;
        self::$buffers[$id] = $buffer;
    }
    
    function ev_error($buffer$error$id
    {
        event_buffer_disable(self::$buffers[$id], EV_READ | EV_WRITE);
        event_buffer_free(self::$buffers[$id]);
        fclose(self::$connections[$id]);
        unset(self::$buffers[$id], self::$connections[$id]);
    }
    
    function ev_read($buffer$id
    {
        static $ct = 0;
        $ct_last = $ct;
        $ct_data = '';
        while ($read = event_buffer_read($buffer, 1024)) {
            $ct += strlen($read);
            $ct_data .= $read;
        }
        $ct_size = ($ct - $ct_last) * 8;
        echo "[$id] " . __METHOD__ . " > " . $ct_data . "/n";
        event_buffer_write($buffer, "Received $ct_size byte data./r/n");
    }
    
    function ev_write($buffer$id
    {
        echo "[$id] " . __METHOD__ . "/n";
    }
}
new EpollSocketServer(2000);
epoll_client.php
<?php
/**
 * EpollSocket Test Client
 * By James.Huang <shagoo#gmail.com>
*
*/
function debug ($msg)
{
//    echo $msg;
    error_log($msg, 3, '/tmp/socket.log');
}
if ($argv[1]) {
    $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno$errstr, 30);
//    stream_set_blocking($socket_client, 0);
    if (!$socket_client) {
        die("$errstr ($errno)");
    } else {
        $msg = trim($argv[1]);
        for ($i = 0; $i < 10; $i++) {
            $res = fwrite($socket_client, "$msg($i)");
            usleep(100000);
            debug(fread($socket_client, 1024));
        }
        fclose($socket_client);
    }
}
else {
    
    $phArr = array();
    for ($i = 0; $i < 10; $i++) {
        $phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');
    }
    foreach ($phArr as $ph) {
        pclose($ph);
    }
    
//    for ($i = 0; $i < 10; $i++) {
//        system("php ".__FILE__." '{$i}:test'");
//    }

}
先說一下,以上的例子是基于 PHP 的 libevent 擴(kuò)展實(shí)現(xiàn)的,需要運(yùn)行的話要先安裝此擴(kuò)展,參考:http://pecl.php.net/package/libevent。

這個(gè)例子做的事情和前面介紹的第一個(gè)模型一樣,epoll_server.php 實(shí)現(xiàn)的服務(wù)端也是接受客戶端數(shù)據(jù),然后返回結(jié)果(接收到的字節(jié)數(shù))。但是,當(dāng)你運(yùn)行 php epoll_client.php 的時(shí)候你會(huì)發(fā)現(xiàn)服務(wù)端打印出來的結(jié)果和 accept 阻塞模型就大不一樣了,當(dāng)然運(yùn)行效率也有極大的提升,這是為什么呢?接下來就介紹一下 epoll/kqueue 模型:在介紹 select 模式的時(shí)候我們提到了這種模式的局限,而 epoll 就是為了解決 poll 的這兩個(gè)缺陷而生的。首先,epoll 模式基本沒有限制(參考 cat /proc/sys/fs/file-max 默認(rèn)就達(dá)到 300K,很令人興奮吧,其實(shí)這也就是所謂基于 epoll 的 Erlang 服務(wù)端可以同時(shí)處理這么多并發(fā)連接的根本原因,不過現(xiàn)在 PHP 理論上也可以做到了,呵呵);另外,epoll 模式的性能也不會(huì)像 select 模式那樣隨著連接數(shù)的增大而變差,測試發(fā)現(xiàn)性能還是很穩(wěn)定的(下篇會(huì)有詳細(xì)介紹)。

epoll 工作有兩種模式 LT(level triggered) 和 ET(edge-triggered),前者是缺省模式,同時(shí)支持阻塞和非阻塞 IO 模式,雖然性能比后者差點(diǎn),但是比較穩(wěn)定,一般來說在實(shí)際運(yùn)用中,我們都是用這種模式(ET 模式和 WinSock 都是純異步非阻塞模型)。而另外一點(diǎn)要說的是 libevent 是在編譯階段選擇系統(tǒng)的 I/O demultiplex 機(jī)制的,不支持在運(yùn)行階段根據(jù)配置再次選擇,所以我們在這里也就不細(xì)討論 libevent 的實(shí)現(xiàn)的細(xì)節(jié)了,如果朋友有興趣進(jìn)一步了解的話,請參考:http://monkey.org/~provos/libevent/。

到這里,第一部分的內(nèi)容結(jié)束了,相信大家已經(jīng)了解了 Socket 編程的幾個(gè)重點(diǎn)概念和一些實(shí)戰(zhàn)技巧,在下一篇《Socket深度探究4PHP(二) 》我將會(huì)對(duì) select/poll/epoll/kqueue 幾種模式做一下深入的介紹和對(duì)比,另外也會(huì)涉及到兩種重要的 I/O 多路復(fù)用模式:Reactor 和 Proactor 模式。

To be continued ...

http://blog.csdn.net/shagoo/article/details/6531950

上一篇《Socket深度探究4PHP(一)》中,大家應(yīng)該對(duì) poll/select/epoll/kqueue 這幾個(gè) IO 模型有了一定的了解,為了讓大家更深入的理解 Socket 的技術(shù)內(nèi)幕,在這個(gè)篇幅,我會(huì)對(duì)這幾種模式做一個(gè)比較詳細(xì)的分析和對(duì)比;另外,大家可能也同說過 AIO 的概念,這里也會(huì)做一個(gè)簡單的介紹;最后我們會(huì)對(duì)兩種主流異步模式 Reactor 和 Proactor 模式進(jìn)行對(duì)比和討論。

首先,然我們逐個(gè)介紹一下 2.6 內(nèi)核(2.6.21.1)中的 poll/select/epoll/kqueue 這幾個(gè) IO 模型。

> POLL

先說說 poll,poll 和 select 為大部分 Unix/Linux 程序員所熟悉,這倆個(gè)東西原理類似,性能上也不存在明顯差異,但 select 對(duì)所監(jiān)控的文件描述符數(shù)量有限制,所以這里選用 poll 做說明。poll 是一個(gè)系統(tǒng)調(diào)用,其內(nèi)核入口函數(shù)為 sys_poll,sys_poll 幾乎不做任何處理直接調(diào)用 do_sys_poll,do_sys_poll 的執(zhí)行過程可以分為三個(gè)部分:

1、將用戶傳入的 pollfd 數(shù)組拷貝到內(nèi)核空間,因?yàn)榭截惒僮骱蛿?shù)組長度相關(guān),時(shí)間上這是一個(gè) O(n) 操作,這一步的代碼在 do_sys_poll 中包括從函數(shù)開始到調(diào)用 do_poll 前的部分。

2、查詢每個(gè)文件描述符對(duì)應(yīng)設(shè)備的狀態(tài),如果該設(shè)備尚未就緒,則在該設(shè)備的等待隊(duì)列中加入一項(xiàng)并繼續(xù)查詢下一設(shè)備的狀態(tài)。查詢完所有設(shè)備后如果沒有一個(gè)設(shè)備就緒,這時(shí)則需要掛起當(dāng)前進(jìn)程等待,直到設(shè)備就緒或者超時(shí),掛起操作是通過調(diào)用 schedule_timeout 執(zhí)行的。設(shè)備就緒后進(jìn)程被通知繼續(xù)運(yùn)行,這時(shí)再次遍歷所有設(shè)備,以查找就緒設(shè)備。這一步因?yàn)閮纱伪闅v所有設(shè)備,時(shí)間復(fù)雜度也是 O(n),這里面不包括等待時(shí)間。相關(guān)代碼在 do_poll 函數(shù)中。

3、將獲得的數(shù)據(jù)傳送到用戶空間并執(zhí)行釋放內(nèi)存和剝離等待隊(duì)列等善后工作,向用戶空間拷貝數(shù)據(jù)與剝離等待隊(duì)列等操作的的時(shí)間復(fù)雜度同樣是 O(n),具體代碼包括 do_sys_poll 函數(shù)中調(diào)用 do_poll 后到結(jié)束的部分。

但是,即便通過 select() 或者 poll() 函數(shù)復(fù)用事件通知具有突出的優(yōu)點(diǎn),不過其他具有類似功能的函數(shù)實(shí)現(xiàn)也可以達(dá)到同樣的性能。然而,這些實(shí)現(xiàn)在跨平臺(tái)方面沒有實(shí)現(xiàn)標(biāo)準(zhǔn)化。你必須在使用這些特定函數(shù)實(shí)現(xiàn)同喪失可移植性之間進(jìn)行權(quán)衡。我們現(xiàn)在就討論一下兩個(gè)替代方法:Solaris 系統(tǒng)下的 /dev/poll 和 FreeBSD 系統(tǒng)下的 kqueue:

1、Solaris 系統(tǒng)下的 /dev/poll:在Solaris 7系統(tǒng)上,Sun引入了/dev/poll設(shè)備。在使用 /dev/poll的時(shí)候,你首先要打開/dev/poll作為一個(gè)普通文件。然后構(gòu)造pollfd結(jié)構(gòu),方式同普通的poll()函數(shù)調(diào)用一樣。這些 pollfd結(jié)構(gòu)隨后寫入到打開的 /dev/poll 文件描述符。在打開句柄的生存周期內(nèi), /dev/poll會(huì)根據(jù)pollfd結(jié)構(gòu)返回事件(注意,pollfd結(jié)構(gòu)內(nèi)的事件字段中的特定POLLREMOVE將從/dev/poll的列表中刪除對(duì)應(yīng)的fd)。通過調(diào)用特定的ioctl (DP_POLL) 和dvpoll,程序就可以從/dev/poll獲得需要的信息。在使用dvpoll結(jié)構(gòu)的情況下,發(fā)生的事件就可以被檢測到了。

2、FreeBSD 系統(tǒng)下的 kqueue:在FreeBSD 4.1中推出。FreeBSD的kqueue API設(shè)計(jì)為比其他對(duì)應(yīng)函數(shù)提供更為廣泛的事件通知能力。kqueue API提供了一套通用過濾器,可以模仿poll()語法(EVFILT_READ和EVFILT_WRITE)。不過,它還實(shí)現(xiàn)了文件系統(tǒng)變化(EVFILT_VNODE)、進(jìn)程狀態(tài)變更(EVFILT_PROC)和信號(hào)交付(EVFILT_SIGNAL)的有關(guān)通知。

> EPOLL


接下來分析 epoll,與 poll/select 不同,epoll 不再是一個(gè)單獨(dú)的系統(tǒng)調(diào)用,而是由 epoll_create/epoll_ctl/epoll_wait 三個(gè)系統(tǒng)調(diào)用組成,后面將會(huì)看到這樣做的好處。先來看 sys_epoll_create(epoll_create對(duì)應(yīng)的內(nèi)核函數(shù)),這個(gè)函數(shù)主要是做一些準(zhǔn)備工作,比如創(chuàng)建數(shù)據(jù)結(jié)構(gòu),初始化數(shù)據(jù)并最終返回一個(gè)文件描述符(表示新創(chuàng)建的虛擬 epoll 文件),這個(gè)操作可以認(rèn)為是一個(gè)固定時(shí)間的操作。epoll 是做為一個(gè)虛擬文件系統(tǒng)來實(shí)現(xiàn)的,這樣做至少有以下兩個(gè)好處:

1、可以在內(nèi)核里維護(hù)一些信息,這些信息在多次 epoll_wait 間是保持的,比如所有受監(jiān)控的文件描述符。

2、epoll 本身也可以被 poll/epoll。

具體 epoll 的虛擬文件系統(tǒng)的實(shí)現(xiàn)和性能分析無關(guān),不再贅述。

在 sys_epoll_create 中還能看到一個(gè)細(xì)節(jié),就是 epoll_create 的參數(shù) size 在現(xiàn)階段是沒有意義的,只要大于零就行。

接著是 sys_epoll_ctl(epoll_ctl對(duì)應(yīng)的內(nèi)核函數(shù)),需要明確的是每次調(diào)用 sys_epoll_ctl 只處理一個(gè)文件描述符,這里主要描述當(dāng) op 為 EPOLL_CTL_ADD 時(shí)的執(zhí)行過程,sys_epoll_ctl 做一些安全性檢查后進(jìn)入 ep_insert,ep_insert 里將 ep_poll_callback 做為回掉函數(shù)加入設(shè)備的等待隊(duì)列(假定這時(shí)設(shè)備尚未就緒),由于每次 poll_ctl 只操作一個(gè)文件描述符,因此也可以認(rèn)為這是一個(gè) O(1) 操作。ep_poll_callback 函數(shù)很關(guān)鍵,它在所等待的設(shè)備就緒后被系統(tǒng)回掉,執(zhí)行兩個(gè)操作:

1、將就緒設(shè)備加入就緒隊(duì)列,這一步避免了像 poll 那樣在設(shè)備就緒后再次輪詢所有設(shè)備找就緒者,降低了時(shí)間復(fù)雜度,由 O(n) 到 O(1)。
 
2、喚醒虛擬的 epoll 文件。

最后是 sys_epoll_wait,這里實(shí)際執(zhí)行操作的是 ep_poll 函數(shù)。該函數(shù)等待將進(jìn)程自身插入虛擬 epoll 文件的等待隊(duì)列,直到被喚醒(見上面 ep_poll_callback 函數(shù)描述),最后執(zhí)行 ep_events_transfer 將結(jié)果拷貝到用戶空間。由于只拷貝就緒設(shè)備信息,所以這里的拷貝是一個(gè) O(1) 操作。

還有一個(gè)讓人關(guān)心的問題就是 epoll 對(duì) EPOLLET 的處理,即邊沿觸發(fā)的處理,粗略看代碼就是把一部分水平觸發(fā)模式下內(nèi)核做的工作交給用戶來處理,直覺上不會(huì)對(duì)性能有太大影響,感興趣的朋友歡迎討論。

> POLL/EPOLL 對(duì)比:

表面上 poll 的過程可以看作是由一次 epoll_create,若干次 epoll_ctl,一次 epoll_wait,一次 close 等系統(tǒng)調(diào)用構(gòu)成,實(shí)際上 epoll 將 poll 分成若干部分實(shí)現(xiàn)的原因正是因?yàn)榉?wù)器軟件中使用 poll 的特點(diǎn)(比如Web服務(wù)器):

1、需要同時(shí) poll 大量文件描述符;

2、每次 poll 完成后就緒的文件描述符只占所有被 poll 的描述符的很少一部分。

3、前后多次 poll 調(diào)用對(duì)文件描述符數(shù)組(ufds)的修改只是很小;

傳統(tǒng)的 poll 函數(shù)相當(dāng)于每次調(diào)用都重起爐灶,從用戶空間完整讀入 ufds,完成后再次完全拷貝到用戶空間,另外每次 poll 都需要對(duì)所有設(shè)備做至少做一次加入和刪除等待隊(duì)列操作,這些都是低效的原因。

epoll 將以上情況都細(xì)化考慮,不需要每次都完整讀入輸出 ufds,只需使用 epoll_ctl 調(diào)整其中一小部分,不需要每次 epoll_wait 都執(zhí)行一次加入刪除等待隊(duì)列操作,另外改進(jìn)后的機(jī)制使的不必在某個(gè)設(shè)備就緒后搜索整個(gè)設(shè)備數(shù)組進(jìn)行查找,這些都能提高效率。另外最明顯的一點(diǎn),從用戶的使用來說,使用 epoll 不必每次都輪詢所有返回結(jié)果已找出其中的就緒部分,O(n) 變 O(1),對(duì)性能也提高不少。

此外這里還發(fā)現(xiàn)一點(diǎn),是不是將 epoll_ctl 改成一次可以處理多個(gè) fd(像 semctl 那樣)會(huì)提高些許性能呢?特別是在假設(shè)系統(tǒng)調(diào)用比較耗時(shí)的基礎(chǔ)上。不過關(guān)于系統(tǒng)調(diào)用的耗時(shí)問題還會(huì)在以后分析。

> POLL/EPOLL 測試數(shù)據(jù)對(duì)比:


測試的環(huán)境:我寫了三段代碼來分別模擬服務(wù)器,活動(dòng)的客戶端,僵死的客戶端,服務(wù)器運(yùn)行于一個(gè)自編譯的標(biāo)準(zhǔn) 2.6.11 內(nèi)核系統(tǒng)上,硬件為 PIII933,兩個(gè)客戶端各自運(yùn)行在另外的 PC 上,這兩臺(tái)PC比服務(wù)器的硬件性能要好,主要是保證能輕易讓服務(wù)器滿載,三臺(tái)機(jī)器間使用一個(gè)100M交換機(jī)連接。

服務(wù)器接受并poll所有連接,如果有request到達(dá)則回復(fù)一個(gè)response,然后繼續(xù)poll。

活動(dòng)的客戶端(Active Client)模擬若干并發(fā)的活動(dòng)連接,這些連接不間斷的發(fā)送請求接受回復(fù)。

僵死的客戶端(zombie)模擬一些只連接但不發(fā)送請求的客戶端,其目的只是占用服務(wù)器的poll描述符資源。

測試過程:保持10個(gè)并發(fā)活動(dòng)連接,不斷的調(diào)整僵并發(fā)連接數(shù),記錄在不同比例下使用 poll 與 epoll 的性能差別。僵死并發(fā)連接數(shù)根據(jù)比例分別是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。

下圖中橫軸表示僵死并發(fā)連接與活動(dòng)并發(fā)連接之比,縱軸表示完成 40000 次請求回復(fù)所花費(fèi)的時(shí)間,以秒為單位。紅色線條表示 poll 數(shù)據(jù),綠色表示 epoll 數(shù)據(jù)。可以看出,poll 在所監(jiān)控的文件描述符數(shù)量增加時(shí),其耗時(shí)呈線性增長,而 epoll 則維持了一個(gè)平穩(wěn)的狀態(tài),幾乎不受描述符個(gè)數(shù)影響。



但是要注意的是在監(jiān)控的所有客戶端都是活動(dòng)時(shí),poll 的效率會(huì)略高于 epoll(主要在原點(diǎn)附近,即僵死并發(fā)連接為0時(shí),圖上不易看出來),究竟 epoll 實(shí)現(xiàn)比 poll 復(fù)雜,監(jiān)控少量描述符并非它的長處。

> epoll 的優(yōu)點(diǎn)綜述

1、支持一個(gè)進(jìn)程打開大數(shù)目的socket描述符(FD):select 最不能忍受的是一個(gè)進(jìn)程所打開的 FD 是有一定限制的,由 FD_SETSIZE 設(shè)置,在 Linux 中,這個(gè)值是 1024。對(duì)于那些需要支持的上萬連接數(shù)目的網(wǎng)絡(luò)服務(wù)器來說顯然太少了。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核,不過資料也同時(shí)指出這樣會(huì)帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache 方案),不過雖然 linux 上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。不過 epoll 則沒有這個(gè)限制,它所支持的 FD 上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于 1024,舉個(gè)例子,在 1GB 內(nèi)存的機(jī)器上大約是 10 萬左右,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。

2、IO 效率不隨 FD 數(shù)目增加而線性下降:傳統(tǒng)的 select/poll 另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的 socket 集合,不過由于網(wǎng)絡(luò)延時(shí),任一時(shí)間只有部分的 socket 是"活躍"的,但是 select/poll 每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是 epoll 不存在這個(gè)問題,它只會(huì)對(duì)"活躍"的 socket 進(jìn)行操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中 epoll 是根據(jù)每個(gè) fd 上面的 callback 函數(shù)實(shí)現(xiàn)的。那么,只有"活躍"的 socket 才會(huì)主動(dòng)的去調(diào)用 callback 函數(shù),其他 idle 狀態(tài) socket 則不會(huì),在這點(diǎn)上,epoll 實(shí)現(xiàn)了一個(gè)"偽"AIO,因?yàn)檫@時(shí)候推動(dòng)力在 os 內(nèi)核。在一些 benchmark 中,如果所有的 socket 基本上都是活躍的 --- 比如一個(gè)高速LAN環(huán)境,epoll 并不比 select/poll 有什么效率,相反,如果過多使用 epoll_ctl,效率相比還有稍微的下降。但是一旦使用 idle connections 模擬 WAN 環(huán)境,epoll 的效率就遠(yuǎn)在 select/poll 之上了。

3、使用 mmap 加速內(nèi)核與用戶空間的消息傳遞:這點(diǎn)實(shí)際上涉及到 epoll 的具體實(shí)現(xiàn)了。無論是 select,poll 還是 epoll 都需要內(nèi)核把 FD 消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點(diǎn)上,epoll 是通過內(nèi)核于用戶空間 mmap 同一塊內(nèi)存實(shí)現(xiàn)的。而如果你想我一樣從 2.5 內(nèi)核就關(guān)注 epoll 的話,一定不會(huì)忘記手工 mmap 這一步的。

4、內(nèi)核微調(diào):這一點(diǎn)其實(shí)不算 epoll 的優(yōu)點(diǎn)了,而是整個(gè) linux 平臺(tái)的優(yōu)點(diǎn)。也許你可以懷疑 linux 平臺(tái),但是你無法回避 linux 平臺(tái)賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核 TCP/IP 協(xié)議棧使用內(nèi)存池管理 sk_buff 結(jié)構(gòu),那么可以在運(yùn)行時(shí)期動(dòng)態(tài)調(diào)整這個(gè)內(nèi)存 pool(skb_head_pool) 的大小 --- 通過 echo XXXX > /proc/sys/net/core/hot_list_length 完成。再比如 listen 函數(shù)的第 2 個(gè)參數(shù)(TCP 完成 3 次握手的數(shù)據(jù)包隊(duì)列長度),也可以根據(jù)你平臺(tái)內(nèi)存大小動(dòng)態(tài)調(diào)整。更甚至在一個(gè)數(shù)據(jù)包面數(shù)目巨大但同時(shí)每個(gè)數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的 NAPI 網(wǎng)卡驅(qū)動(dòng)架構(gòu)。

> AIO 和 Epoll


epoll 和 aio(這里的aio是指linux 2.6內(nèi)核后提供的aio api)的區(qū)別:

1、aio 是異步非阻塞的。其實(shí)是aio是用線程池實(shí)現(xiàn)了異步IO。

2、epoll 在這方面的定義上有點(diǎn)復(fù)雜,首先 epoll 的 fd 集里面每一個(gè) fd 都是非阻塞的,但是 epoll(包括 select/poll)在調(diào)用時(shí)阻塞等待 fd 可用,然后 epoll 只是一個(gè)異步通知機(jī)制,只是在 fd 可用時(shí)通知你,并沒有做任何 IO 操作,所以不是傳統(tǒng)的異步。

在這方面,Windows 無疑是前行者,當(dāng)然 Boost C++ 庫已經(jīng)實(shí)現(xiàn)了 linux 下 aio 的機(jī)制,有興趣的朋友可以參考:http://stlchina.huhoo.net/twiki/bin/view.pl/Main/WebHome

> Reactor 和 Proactor

一般地,I/O多路復(fù)用機(jī)制都依賴于一個(gè)事件多路分離器(Event Demultiplexer)。分離器對(duì)象可將來自事件源的I/O事件分離出來,并分發(fā)到對(duì)應(yīng)的read/write事件處理器(Event Handler)。開發(fā)人員預(yù)先注冊需要處理的事件及其事件處理器(或回調(diào)函數(shù));事件分離器負(fù)責(zé)將請求事件傳遞給事件處理器。兩個(gè)與事件分離器有關(guān)的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用異步IO。

在Reactor中,事件分離器負(fù)責(zé)等待文件描述符或socket為讀寫操作準(zhǔn)備就緒,然后將就緒事件傳遞給對(duì)應(yīng)的處理器,最后由處理器負(fù)責(zé)完成實(shí)際的讀寫工作。而在Proactor模式中,處理器--或者兼任處理器的事件分離器,只負(fù)責(zé)發(fā)起異步讀寫操作。IO操作本身由操作系統(tǒng)來完成。傳遞給操作系統(tǒng)的參數(shù)需要包括用戶定義的數(shù)據(jù)緩沖區(qū)地址和數(shù)據(jù)大小,操作系統(tǒng)才能從中得到寫出操作所需數(shù)據(jù),或?qū)懭霃膕ocket讀到的數(shù)據(jù)。事件分離器捕獲IO操作完成事件,然后將事件傳遞給對(duì)應(yīng)處理器。比如,在windows上,處理器發(fā)起一個(gè)異步IO操作,再由事件分離器等待IOCompletion事件。典型的異步模式實(shí)現(xiàn),都建立在操作系統(tǒng)支持異步API的基礎(chǔ)之上,我們將這種實(shí)現(xiàn)稱為“系統(tǒng)級(jí)”異步或“真”異步,因?yàn)閼?yīng)用程序完全依賴操作系統(tǒng)執(zhí)行真正的IO工作。

舉個(gè)例子,將有助于理解Reactor與Proactor二者的差異,以讀操作為例(類操作類似)。

在Reactor中實(shí)現(xiàn)讀:
 - 注冊讀就緒事件和相應(yīng)的事件處理器
 - 事件分離器等待事件
 - 事件到來,激活分離器,分離器調(diào)用事件對(duì)應(yīng)的處理器。
 - 事件處理器完成實(shí)際的讀操作,處理讀到的數(shù)據(jù),注冊新的事件,然后返還控制權(quán)。

與如下Proactor(真異步)中的讀過程比較:
 - 處理器發(fā)起異步讀操作(注意:操作系統(tǒng)必須支持異步IO)。在這種情況下,處理器無視IO就緒事件,它關(guān)注的是完成事件。
 - 事件分離器等待操作完成事件
 - 在分離器等待過程中,操作系統(tǒng)利用并行的內(nèi)核線程執(zhí)行實(shí)際的讀操作,并將結(jié)果數(shù)據(jù)存入用戶自定義緩沖區(qū),最后通知事件分離器讀操作完成。
 - 事件分離器呼喚處理器。
 - 事件處理器處理用戶自定義緩沖區(qū)中的數(shù)據(jù),然后啟動(dòng)一個(gè)新的異步操作,并將控制權(quán)返回事件分離器。

對(duì)于不提供異步 IO API 的操作系統(tǒng)來說,這種辦法可以隱藏 Socket API 的交互細(xì)節(jié),從而對(duì)外暴露一個(gè)完整的異步接口。借此,我們就可以進(jìn)一步構(gòu)建完全可移植的,平臺(tái)無關(guān)的,有通用對(duì)外接口的解決方案。上述方案已經(jīng)由Terabit P/L公司實(shí)現(xiàn)為 TProactor (ACE compatible Proactor) :http://www.terabit.com.au/solutions.php。正是因?yàn)?linux 對(duì) aio 支持的不完整,所以 ACE_Proactor 框架在 linux 上的表現(xiàn)很差,大部分在 windows 上執(zhí)行正常的代碼,在 linux 則運(yùn)行異常,甚至不能編譯通過。這個(gè)問題一直困擾著很大多數(shù) ACE 的用戶,現(xiàn)在好了,有一個(gè) TProactor 幫助解決了在 Linux 不完整支持 AIO 的條件下,正常使用(至少是看起來正常)ACE_Proactor。TProactor 有兩個(gè)版本:C++ 和 Java 的。C++ 版本采用 ACE 跨平臺(tái)底層類開發(fā),為所有平臺(tái)提供了通用統(tǒng)一的主動(dòng)式異步接口。Boost.Asio 庫,也是采取了類似的這種方案來實(shí)現(xiàn)統(tǒng)一的 IO 異步接口。

以下是一張 TProactor 架構(gòu)設(shè)計(jì)圖,有興趣的朋友可以看看:



到這里,第二部分的內(nèi)容結(jié)束了,相信大家對(duì) Socket 的底層技術(shù)原理有了一個(gè)更深層次的理解,在下一篇《Socket深度探究4PHP(三)》我將會(huì)深入 PHP 源代碼,探究一下 PHP 在 Socket 這部分的一些技術(shù)內(nèi)幕,然后介紹一下目前在這個(gè)領(lǐng)域比較活躍的項(xiàng)目(node.js)。

To be continued ...

http://blog.csdn.net/shagoo/article/details/6647961

看過前兩篇文章《Socket深度探究4PHP(一)》和《Socket深度探究4PHP(二)》,大家應(yīng)該對(duì)目前 Socket 技術(shù)的底層有了一定的了解。本文我們會(huì)對(duì) PHP-5.3.6 的源碼中的 Socket 模塊進(jìn)行一定的分析,然后再簡單介紹一下目前比較熱門的一些相關(guān)技術(shù),比如 Node.js 等。

自 PHP4 之后,越來越多的模塊都被作為擴(kuò)展提取出來(可單獨(dú)編譯),都在 PHP 源碼的 ext 目錄下面,因此我們我需要先進(jìn)入 ext/sockets/ 目錄,做過 PHP 擴(kuò)展的同學(xué)應(yīng)該都很熟悉下面的一些文件了,這次我們主要分析的是 php_sockets.h 和 sockets.c 這兩個(gè) C 源碼文件。

ext/sockets/php_sockets.h

這個(gè)頭文件很簡單,我們主要看一下下面列出的幾個(gè)重點(diǎn):

32 行:
  1. #ifdef PHP_WIN32  
  2. #include <winsock.h>  
  3. #else  
  4. #if HAVE_SYS_SOCKET_H  
  5. #include <sys/socket.h>  
  6. #endif  
  7. #endif  

以上就是 PHP 對(duì)于不同環(huán)境 Socket 底層調(diào)用的定義了,我們可以看到不管是 Unix 還是 Windows 環(huán)境,PHP均調(diào)用的是系統(tǒng)標(biāo)準(zhǔn)的 BSD Socket 庫。然后我們看下面這個(gè)重要的結(jié)構(gòu)體定義:

82 行:
  1. typedef struct {  
  2.     PHP_SOCKET bsd_socket;  
  3.     int        type;  
  4.     int        error;  
  5.     int        blocking;  
  6. } php_socket;  

這個(gè)就是 php socket 的存儲(chǔ)結(jié)構(gòu)了,此結(jié)構(gòu)體在以下的代碼閱讀中將會(huì)大量出現(xiàn),里面的幾個(gè)字段很容易理解:bsd_socket 就是標(biāo)準(zhǔn)的 socket 類型,type 表示 socket 類型(PF_UNIX/AF_UNIX),error 是錯(cuò)誤代碼,blocking 則表示是否阻塞。

ext/sockets/sockets.c

這個(gè)文件比較長,為了直接切入重點(diǎn),我們會(huì)按照《Socket 深度探索 4 PHP (一) 》中 select_server.php 部分代碼來按順序分析一下在最經(jīng)典的 select 模式中我們用到的主要方法:

>socket_create_listen

859 行:PHP_FUNCTION(socket_create_listen)
這個(gè)函數(shù)很簡單,初始化 php_sock 并獲取 socket 需要監(jiān)聽的端口,然后傳入下面的 php_open_listen_sock 函數(shù)進(jìn)行加工,最后調(diào)用 ZEND_REGISTER_RESOURCE 宏返回 php_sock。

347行:static int php_open_listen_sock(php_socket **php_sock, int port, int backlog TSRMLS_DC)
此函數(shù)基本上就是 socket 的標(biāo)準(zhǔn)初始化過程:socket(...) -> bind(...) -> listen(...)(詳見 368 行至 391 行)。
  1. sock->bsd_socket = socket(PF_INET, SOCK_STREAM, 0);  
  2. sock->blocking = 1;  
  3. ...  
  4. sock->type = PF_INET;  
  5. ...  
  6. if (bind(sock->bsd_socket, (struct sockaddr *)&la, sizeof(la)) != 0) {  
  7. ...  
  8. }  
  9. if (listen(sock->bsd_socket, backlog) != 0) {  
  10. ...  
  11. }  

>socket_set_nonblock

906 行:PHP_FUNCTION(socket_set_nonblock)
這個(gè)函數(shù)也很簡單,從 ZEND_FETCH_RESOURCE 取出 runtime 中的 php_sock 然后調(diào)用 php_set_sock_blocking 函數(shù)來設(shè)置 sockfd 的阻塞或者非阻塞(此函數(shù)可以參考 main/network.c 第 1069 行,我們可以看到 PHP 是使用 fcntl 函數(shù)來設(shè)置的)。

>socket_select

785 行:PHP_FUNCTION(socket_select)
也是標(biāo)準(zhǔn)的 select 函數(shù)調(diào)用,過程如下:FD_ZERO(...) -> php_sock_array_to_fd_set(...) -> select(...) -> php_sock_array_from_fd_set(...),可能比較特殊的就是 php_sock_array_from_fd_set() 和 php_sock_array_from_fd_set() 兩個(gè)函數(shù),這是由于我們要先把 PHP 的 fd 數(shù)組轉(zhuǎn)換成原生 fd 集合,才能調(diào)用原生的 select 函數(shù),而最后系統(tǒng)還把 fd 集合重新轉(zhuǎn)回到 PHP 的 fd 數(shù)組(具體代碼參考 799 行至 851 行)。

>socket_accept

881 行:PHP_FUNCTION(socket_accept)
此函數(shù)基本上也就是 socket 原生 accept 函數(shù)的包裝,具體代碼可參考 397 行:php_accept_connect 函數(shù)中的邏輯,最后調(diào)用 ZEND_REGISTER_RESOURCE 宏返回 new_sock,若失敗程序會(huì)清理使用的 out_socket 資源。

>socket_write

986 行:PHP_FUNCTION(socket_write)
按照以上的思路看這個(gè)函數(shù)也非常簡單,詳見 986 行,唯一值得注意的是對(duì)于不同操作系統(tǒng)調(diào)用的函數(shù)有點(diǎn)不同,代碼(見 1004 行)如下:
  1. #ifndef PHP_WIN32  
  2.     retval = write(php_sock->bsd_socket, str, MIN(length, str_len));  
  3. #else  
  4.     retval = send(php_sock->bsd_socket, str, min(length, str_len), 0);  
  5. #endif  

>socket_read

1021 行:PHP_FUNCTION(socket_read)
此函數(shù)是用于接受 socket 的數(shù)據(jù),調(diào)用的原生函數(shù)是 recv(),不過這里需要注意的是 PHP 為我們提供兩種獲取方式:
1、PHP_NORMAL_READ
按行讀取,具體代碼見 419 行:php_read 函數(shù)的邏輯,我們注意到此函數(shù)在非阻塞模式下會(huì)立即返回,否則將會(huì)讀取直至遇到 \n 或者 \r 字符。
2、PHP_BINARY_READ
代碼見 1045 行:retval = recv(php_sock->bsd_socket, tmpbuf, length, 0); 相當(dāng)原生和“環(huán)保”。
最后,如果返回值為 -1 則會(huì)進(jìn)行一些錯(cuò)誤記錄和系統(tǒng)清理工作。

>socket_close

970 行:PHP_FUNCTION(socket_close)
清理 socket 運(yùn)行時(shí)所用的資源。

>socket_shutdown

1968 行:PHP_FUNCTION(socket_shutdown)
調(diào)用原生 shutdown 函數(shù)來關(guān)閉 socket。

分析下來,PHP 的 socket 模塊中絕大部分的代碼還是使用的是系統(tǒng)標(biāo)準(zhǔn)的原生 socket 庫,其中唯一有可能造成性能隱患的就是 select 中 PHP 的 fd 數(shù)組與原生 fd 集合轉(zhuǎn)換,至于其他的一些簡單的數(shù)據(jù)拷貝基本對(duì)效率不會(huì)有什么影響。總的來說,PHP 的 socket 模塊應(yīng)該效率還是比較高的,但是在使用的時(shí)候還是需要注意到一些資源的及時(shí)釋放,因?yàn)楫吘故?Daemon 程序,需要不斷運(yùn)行的,而且 PHP 的數(shù)據(jù)結(jié)構(gòu)是很占內(nèi)存(是原生 C 的 4 倍左右)的。

node.js

最后,我們看看現(xiàn)在很流行的 Node.js(http://nodejs.org/),它采用了 JavaScript 的語言引擎,語法非常的簡潔,對(duì)閉包的完美支持讓它特別適合做異步 IO 的代碼編寫,下面是一個(gè)最簡單的 HTTP Server,只用僅僅六行代碼:
[javascript] view plain copy
  1. var http = require('http');  
  2. http.createServer(function (req, res) {  
  3.   res.writeHead(200, {'Content-Type': 'text/plain'});  
  4.   res.end('Hello World\n');  
  5. }).listen(8000, "127.0.0.1");  
  6. console.log('Server running at http://127.0.0.1:8000/');  

運(yùn)行起來感受一下,有沒有驚艷的感覺啊?事實(shí)上用它來寫一些簡單的服務(wù)確實(shí)很不錯(cuò),有興趣的朋友可以多研究研究(中文社區(qū):http://cnodejs.org/),它有 8000 行 C++ 代碼,2000 行 javascript 代碼,使用 Google 的 V8 引擎(和 Mongodb 一樣),相當(dāng)?shù)暮苄∏删贰O旅媸俏以谑褂眠^程總結(jié)出中幾個(gè)要點(diǎn),大家可以參考:

1、使用 V8 引擎(和 Mongodb 一樣),內(nèi)置 JSON,代碼簡潔,使用方便。
2、使用單線程非阻塞 I/O 中的 select 方式,比較穩(wěn)定(但是對(duì)于超高并發(fā)有點(diǎn)力不從心)。
3、一些第三方應(yīng)用接口不是很穩(wěn)定,比如 Mongodb 的接口,并發(fā) 200 出現(xiàn)卡死現(xiàn)象,Mysql 接口也比 fast-cgi 差很多。
4、注意使用 try{...}catch{...} 來捕獲錯(cuò)誤;使用 process.on('uncaughtException', function(err){...}); 來處理未捕獲的錯(cuò)誤,否則出錯(cuò)會(huì)導(dǎo)致整個(gè)服務(wù)退出。

當(dāng)然,Node.js 還在不斷的更新發(fā)展中,雖然目前我在公司的服務(wù)架構(gòu)中還不敢使用它,我還是很希望它能夠迅速成長起來,這樣子我們開發(fā)服務(wù)中間件的時(shí)候,就會(huì)多出一個(gè)很棒的選項(xiàng)啦~
posted on 2016-08-30 15:46 思月行云 閱讀(1046) 評(píng)論(0)  編輯 收藏 引用 所屬分類: PHP
青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            欧美成人精品在线观看| 久久亚洲国产成人| 另类激情亚洲| 国产亚洲综合精品| 久久福利毛片| 久久久久久久久久看片| 亚洲高清免费| 亚洲精品三级| 在线观看亚洲视频| 亚洲精品小视频在线观看| 欧美黄色影院| 欧美午夜精品| 久久久精品一区| 久久中文精品| 亚洲一区二区三区精品动漫| 亚洲欧美一区二区精品久久久| 国产一区二区三区自拍| 欧美黄色小视频| 国产精品国产a| 久久字幕精品一区| 欧美久久久久久久| 久久精品视频网| 欧美极品一区二区三区| 午夜精品一区二区三区四区| 久久青草欧美一区二区三区| 亚洲一区二区日本| 久久久噜噜噜久噜久久| 亚洲一区二区免费看| 久久久精品国产免大香伊| 99精品视频免费观看视频| 欧美一区二区三区免费视频| 日韩一级免费| 久久久国产精彩视频美女艺术照福利| 日韩亚洲在线观看| 久久午夜色播影院免费高清| 亚洲欧美亚洲| 欧美成人一品| 久久亚洲精品网站| 欧美体内谢she精2性欧美| 欧美a级大片| 国产精品一区二区欧美| 亚洲欧洲精品一区二区三区波多野1战4| 欧美激情91| 美日韩精品免费| 国产日韩欧美自拍| 一本大道久久a久久综合婷婷| 日韩一级视频免费观看在线| 亚洲欧美激情视频| 亚洲一级在线观看| 免费欧美视频| 免费看黄裸体一级大秀欧美| 国产日韩亚洲欧美精品| 亚洲手机成人高清视频| 一本色道久久精品| 欧美区在线播放| 欧美成人综合网站| 黄色日韩网站视频| 欧美影院久久久| 久久国产免费| 国产一区二区三区精品欧美日韩一区二区三区 | 久久国产精品一区二区| 欧美一区深夜视频| 国产精品一区二区你懂得| 亚洲视频一区二区在线观看 | 国产精品乱码人人做人人爱| 亚洲精品国产精品国自产在线| 亚洲国产成人久久综合| 久久中文久久字幕| 欧美激情第3页| 亚洲欧洲在线一区| 欧美精品久久久久久久久久| 亚洲国产一区在线| 在线视频亚洲欧美| 国产精品草莓在线免费观看| 亚洲一区二区三区四区中文| 西西裸体人体做爰大胆久久久| 国产精品国产a| 午夜精品一区二区三区在线| 久久久久久久999精品视频| 国模精品一区二区三区| 久久久久久综合| 蜜桃视频一区| 亚洲国产综合91精品麻豆| 欧美成年人在线观看| 亚洲精品免费观看| 午夜精彩视频在线观看不卡| 国产午夜一区二区三区| 久久久久久久精| 亚洲级视频在线观看免费1级| 这里只有精品视频| 国产亚洲激情| 欧美成人精品一区| 亚洲少妇最新在线视频| 久久精品亚洲一区| 亚洲精品看片| 国产精品视频免费一区| 久久都是精品| 亚洲精选在线观看| 久久精品日产第一区二区三区 | 欧美日韩国产精品自在自线| 亚洲特色特黄| 欧美国产日韩精品| 亚洲欧美激情在线视频| 精品69视频一区二区三区| 欧美日韩大片一区二区三区| 欧美怡红院视频一区二区三区| 欧美激情视频一区二区三区不卡| 亚洲欧美日韩国产一区| 亚洲国产裸拍裸体视频在线观看乱了中文 | 日韩性生活视频| 国产精品成av人在线视午夜片| 久久婷婷蜜乳一本欲蜜臀| 中文日韩在线视频| 亚洲黄色一区二区三区| 久久久久久久国产| 午夜精品视频在线观看一区二区| 亚洲精品一级| 国产精品对白刺激久久久| 美女主播一区| 久久精品国产v日韩v亚洲| av成人毛片| 亚洲国产精品v| 久久三级视频| 亚洲欧美在线网| 在线综合亚洲欧美在线视频| 亚洲国产精品久久久| 韩国av一区二区三区在线观看| 国产精品免费小视频| 欧美日韩亚洲一区三区 | 亚洲精品久久久久| 美腿丝袜亚洲色图| 久久国产精品久久久久久久久久| 亚洲一区二区三区高清不卡| 亚洲人成在线播放| 亚洲国产成人精品久久| 国产一区二区无遮挡| 国产精品亚洲综合| 国产精品久久久久久久7电影 | 欧美在线免费播放| 亚洲欧美在线网| 亚洲欧美久久| 亚洲午夜未删减在线观看| 99国产精品久久| 亚洲人成在线观看| 亚洲激情成人网| 亚洲三级网站| 99精品视频一区二区三区| 日韩西西人体444www| 日韩天天综合| 在线亚洲一区| 亚洲综合第一页| 香蕉乱码成人久久天堂爱免费 | 香蕉久久夜色精品国产使用方法| 亚洲欧美日韩另类精品一区二区三区| 在线视频日本亚洲性| 亚洲影院色无极综合| 国产精品一区二区你懂的| 久久久精品2019中文字幕神马| 久久久久亚洲综合| 欧美福利在线观看| 欧美日产国产成人免费图片| 欧美视频亚洲视频| 国产日韩欧美一区二区三区四区 | 欧美久久一级| 欧美性大战久久久久久久蜜臀 | 欧美另类综合| 国产精品videosex极品| 国产一区二区电影在线观看| 在线观看欧美日韩| 日韩亚洲欧美一区| 欧美资源在线| 亚洲福利视频网站| 亚洲另类在线视频| 欧美一乱一性一交一视频| 麻豆成人在线| 国产精品极品美女粉嫩高清在线 | 伊人久久大香线蕉av超碰演员| 亚洲国产天堂久久综合网| 亚洲伦理中文字幕| 欧美专区日韩视频| 久久久噜久噜久久综合| 亚洲欧美日韩国产一区| 久久精品亚洲乱码伦伦中文| 欧美高清视频www夜色资源网| 亚洲欧洲日产国产网站| 亚洲欧美日韩精品在线| 免费成人毛片| 国产精品美女久久久久av超清| 亚洲高清视频一区二区| 小黄鸭精品aⅴ导航网站入口 | 欧美高清影院| 国产亚洲激情| 一道本一区二区| 欧美成人一区二区在线 | 欧美福利精品| 亚洲伊人观看| 欧美日韩国产在线观看| 亚洲电影免费在线| 久久精品欧美| 午夜国产精品视频|