• <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>
            Fork me on GitHub
            隨筆 - 215  文章 - 13  trackbacks - 0
            <2017年1月>
            25262728293031
            1234567
            891011121314
            15161718192021
            22232425262728
            2930311234


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

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            相冊(cè)

            Awesome

            Blog

            Book

            GitHub

            Link

            搜索

            •  

            積分與排名

            • 積分 - 219196
            • 排名 - 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ù)器的性能問題面臨越來(lái)越大的挑戰(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 庫(kù)就是其中一個(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)來(lái)說(shuō)的,阻塞 IO 是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起;相反,非阻塞指在不能立刻得到結(jié)果之前,該函數(shù)不會(huì)阻塞當(dāng)前線程,而會(huì)立刻返回。

            2、同步/異步:這兩個(gè)概念是針對(duì)調(diào)用如果返回結(jié)果來(lái)說(shuō)的,所謂同步,就是在發(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)來(lái)通知調(diào)用者。

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

            感覺概念很抽象對(duì)吧,“一切答案在于現(xiàn)場(chǎng)”,下面讓我們從三種經(jīng)典的 PHP Socket IO 模型實(shí)例來(lái)對(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ù))。雖然邏輯很簡(jiǎn)單,但是其中有幾種情況很值得分析一下:

            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 缺省是阻塞的,我們?cè)诰幊痰臅r(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é)果的情況下,可以使用非阻塞模式來(lái)獲得最大效率。

            D> 運(yùn)行 php socket_client.php 是連續(xù)運(yùn)行 10 次上面的邏輯,這個(gè)沒什么問題;但是很奇怪的是如果你使用 35 - 41 行的代碼,用 popen 同時(shí)開啟 10 個(gè)進(jìn)程來(lái)運(yùn)行,就會(huì)造成服務(wù)器端的死循環(huán),十分怪異!后來(lái)經(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),但是這樣一來(lái)就會(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'");
            //    }

            }
            以上代碼的邏輯也很簡(jiǎn)單,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è)一般來(lái)說(shuō)不要太短,否則會(huì)導(dǎo)致 CPU 負(fù)載過高。
            $maxconns :表示的是最大連接數(shù),客戶端超過這個(gè)數(shù)的話,服務(wù)器會(huì)拒絕接收。這里要提到的一點(diǎn)是,由于 select 是通過句柄來(lái)讀寫的,所以會(huì)受到系統(tǒng)默認(rèn)參數(shù) __FD_SETSIZE 的限制,一般默認(rèn)值為 1024,修改的話需要重新編譯內(nèi)核;另外通過測(cè)試發(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'");
            //    }

            }
            先說(shuō)一下,以上的例子是基于 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ù)端打印出來(lái)的結(jié)果和 accept 阻塞模型就大不一樣了,當(dāng)然運(yùn)行效率也有極大的提升,這是為什么呢?接下來(lái)就介紹一下 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ù)的增大而變差,測(cè)試發(fā)現(xiàn)性能還是很穩(wěn)定的(下篇會(huì)有詳細(xì)介紹)。

            epoll 工作有兩種模式 LT(level triggered) 和 ET(edge-triggered),前者是缺省模式,同時(shí)支持阻塞和非阻塞 IO 模式,雖然性能比后者差點(diǎn),但是比較穩(wěn)定,一般來(lái)說(shuō)在實(shí)際運(yùn)用中,我們都是用這種模式(ET 模式和 WinSock 都是純異步非阻塞模型)。而另外一點(diǎn)要說(shuō)的是 libevent 是在編譯階段選擇系統(tǒng)的 I/O demultiplex 機(jī)制的,不支持在運(yùn)行階段根據(jù)配置再次選擇,所以我們?cè)谶@里也就不細(xì)討論 libevent 的實(shí)現(xiàn)的細(xì)節(jié)了,如果朋友有興趣進(jìn)一步了解的話,請(qǐng)參考: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ì)比;另外,大家可能也同說(shuō)過 AIO 的概念,這里也會(huì)做一個(gè)簡(jiǎn)單的介紹;最后我們會(huì)對(duì)兩種主流異步模式 Reactor 和 Proactor 模式進(jìn)行對(duì)比和討論。

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

            > POLL

            先說(shuō)說(shuō) poll,poll 和 select 為大部分 Unix/Linux 程序員所熟悉,這倆個(gè)東西原理類似,性能上也不存在明顯差異,但 select 對(duì)所監(jiān)控的文件描述符數(shù)量有限制,所以這里選用 poll 做說(shuō)明。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ù)組長(zhǎng)度相關(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ā)生的事件就可以被檢測(cè)到了。

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

            > EPOLL


            接下來(lái)分析 epoll,與 poll/select 不同,epoll 不再是一個(gè)單獨(dú)的系統(tǒng)調(diào)用,而是由 epoll_create/epoll_ctl/epoll_wait 三個(gè)系統(tǒng)調(diào)用組成,后面將會(huì)看到這樣做的好處。先來(lái)看 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)來(lái)實(shí)現(xiàn)的,這樣做至少有以下兩個(gè)好處:

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

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

            具體 epoll 的虛擬文件系統(tǒng)的實(shí)現(xiàn)和性能分析無(wú)關(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)核做的工作交給用戶來(lá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),從用戶的使用來(lái)說(shuō),使用 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 測(cè)試數(shù)據(jù)對(duì)比:


            測(cè)試的環(huán)境:我寫了三段代碼來(lái)分別模擬服務(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ā)送請(qǐng)求接受回復(fù)。

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

            測(cè)試過程:保持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 次請(qǐng)求回復(fù)所花費(fèi)的時(shí)間,以秒為單位。紅色線條表示 poll 數(shù)據(jù),綠色表示 epoll 數(shù)據(jù)。可以看出,poll 在所監(jiān)控的文件描述符數(shù)量增加時(shí),其耗時(shí)呈線性增長(zhǎng),而 epoll 則維持了一個(gè)平穩(wěn)的狀態(tài),幾乎不受描述符個(gè)數(shù)影響。



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

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

            1、支持一個(gè)進(jìn)程打開大數(shù)目的socket描述符(FD):select 最不能忍受的是一個(gè)進(jìn)程所打開的 FD 是有一定限制的,由 FD_SETSIZE 設(shè)置,在 Linux 中,這個(gè)值是 1024。對(duì)于那些需要支持的上萬(wàn)連接數(shù)目的網(wǎng)絡(luò)服務(wù)器來(lái)說(shuō)顯然太少了。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核,不過資料也同時(shí)指出這樣會(huì)帶來(lái)網(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 萬(wàn)左右,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看,一般來(lái)說(shuō)這個(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)了。無(wú)論是 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),但是你無(wú)法回避 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ì)列長(zhǎng)度),也可以根據(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 無(wú)疑是前行者,當(dāng)然 Boost C++ 庫(kù)已經(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ì)象可將來(lái)自事件源的I/O事件分離出來(lái),并分發(fā)到對(duì)應(yīng)的read/write事件處理器(Event Handler)。開發(fā)人員預(yù)先注冊(cè)需要處理的事件及其事件處理器(或回調(diào)函數(shù));事件分離器負(fù)責(zé)將請(qǐng)求事件傳遞給事件處理器。兩個(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)來(lái)完成。傳遞給操作系統(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)讀:
             - 注冊(cè)讀就緒事件和相應(yīng)的事件處理器
             - 事件分離器等待事件
             - 事件到來(lái),激活分離器,分離器調(diào)用事件對(duì)應(yīng)的處理器。
             - 事件處理器完成實(shí)際的讀操作,處理讀到的數(shù)據(jù),注冊(cè)新的事件,然后返還控制權(quán)。

            與如下Proactor(真異步)中的讀過程比較:
             - 處理器發(fā)起異步讀操作(注意:操作系統(tǒng)必須支持異步IO)。在這種情況下,處理器無(wú)視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)來(lái)說(shuō),這種辦法可以隱藏 Socket API 的交互細(xì)節(jié),從而對(duì)外暴露一個(gè)完整的異步接口。借此,我們就可以進(jìn)一步構(gòu)建完全可移植的,平臺(tái)無(wú)關(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 的條件下,正常使用(至少是看起來(lái)正常)ACE_Proactor。TProactor 有兩個(gè)版本:C++ 和 Java 的。C++ 版本采用 ACE 跨平臺(tái)底層類開發(fā),為所有平臺(tái)提供了通用統(tǒng)一的主動(dòng)式異步接口。Boost.Asio 庫(kù),也是采取了類似的這種方案來(lái)實(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)行一定的分析,然后再簡(jiǎn)單介紹一下目前比較熱門的一些相關(guān)技術(shù),比如 Node.js 等。

            自 PHP4 之后,越來(lái)越多的模塊都被作為擴(kuò)展提取出來(lái)(可單獨(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è)頭文件很簡(jiǎn)單,我們主要看一下下面列出的幾個(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 庫(kù)。然后我們看下面這個(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è)文件比較長(zhǎng),為了直接切入重點(diǎn),我們會(huì)按照《Socket 深度探索 4 PHP (一) 》中 select_server.php 部分代碼來(lái)按順序分析一下在最經(jīng)典的 select 模式中我們用到的主要方法:

            >socket_create_listen

            859 行:PHP_FUNCTION(socket_create_listen)
            這個(gè)函數(shù)很簡(jiǎn)單,初始化 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ù)也很簡(jiǎn)單,從 ZEND_FETCH_RESOURCE 取出 runtime 中的 php_sock 然后調(diào)用 php_set_sock_blocking 函數(shù)來(lái)設(shè)置 sockfd 的阻塞或者非阻塞(此函數(shù)可以參考 main/network.c 第 1069 行,我們可以看到 PHP 是使用 fcntl 函數(shù)來(lái)設(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ù)也非常簡(jiǎn)單,詳見 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ù)來(lái)關(guān)閉 socket。

            分析下來(lái),PHP 的 socket 模塊中絕大部分的代碼還是使用的是系統(tǒng)標(biāo)準(zhǔn)的原生 socket 庫(kù),其中唯一有可能造成性能隱患的就是 select 中 PHP 的 fd 數(shù)組與原生 fd 集合轉(zhuǎn)換,至于其他的一些簡(jiǎn)單的數(shù)據(jù)拷貝基本對(duì)效率不會(huì)有什么影響。總的來(lái)說(shuō),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 的語(yǔ)言引擎,語(yǔ)法非常的簡(jiǎn)潔,對(duì)閉包的完美支持讓它特別適合做異步 IO 的代碼編寫,下面是一個(gè)最簡(jiǎn)單的 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)行起來(lái)感受一下,有沒有驚艷的感覺啊?事實(shí)上用它來(lái)寫一些簡(jiǎn)單的服務(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,代碼簡(jiǎn)潔,使用方便。
            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{...} 來(lái)捕獲錯(cuò)誤;使用 process.on('uncaughtException', function(err){...}); 來(lái)處理未捕獲的錯(cuò)誤,否則出錯(cuò)會(huì)導(dǎo)致整個(gè)服務(wù)退出。

            當(dāng)然,Node.js 還在不斷的更新發(fā)展中,雖然目前我在公司的服務(wù)架構(gòu)中還不敢使用它,我還是很希望它能夠迅速成長(zhǎng)起來(lái),這樣子我們開發(fā)服務(wù)中間件的時(shí)候,就會(huì)多出一個(gè)很棒的選項(xiàng)啦~
            posted on 2016-08-30 15:46 思月行云 閱讀(1031) 評(píng)論(0)  編輯 收藏 引用 所屬分類: PHP
            久久综合噜噜激激的五月天| 久久天天躁狠狠躁夜夜躁2O2O| 久久人人爽爽爽人久久久| 国内精品伊人久久久久妇| 欧美日韩精品久久免费| 久久99精品国产99久久6男男| 97久久香蕉国产线看观看| 久久久久国产一区二区| 久久亚洲AV成人无码国产| 久久久亚洲精品蜜桃臀| 亚洲色婷婷综合久久| 亚洲AV伊人久久青青草原| 久久精品9988| 人妻无码久久一区二区三区免费| 久久av高潮av无码av喷吹| 97热久久免费频精品99| 中文字幕久久久久人妻| 日韩一区二区三区视频久久| 久久久久久综合一区中文字幕| 亚洲国产美女精品久久久久∴| 日韩精品久久久久久久电影| 久久伊人色| 久久高清一级毛片| 999久久久国产精品| 狠狠色婷婷综合天天久久丁香| 欧美一区二区三区久久综| 无码任你躁久久久久久老妇App| 久久婷婷久久一区二区三区| 久久国产精品99久久久久久老狼| 欧美噜噜久久久XXX| 精品国产一区二区三区久久久狼 | 无码八A片人妻少妇久久| 一本色道久久88加勒比—综合| 国产精品一区二区久久国产| 久久免费的精品国产V∧| 久久不见久久见免费视频7| 久久综合给合久久国产免费| 国内精品久久人妻互换| 粉嫩小泬无遮挡久久久久久| WWW婷婷AV久久久影片| 国产91色综合久久免费分享|