(二)單進程阻塞複用的網絡服務器

基本概念

文章開篇先腦補一些知識,有助於閱讀,本篇文章主要以select爲住,介紹select實現原理,並利用select來實現一個單進程阻塞複用的網絡服務器。

IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取,它就通知該進程,目前支持I/O多路複用有 select,poll,epoll,I/O多路複用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作,IO多路複用適用如下場合:

  1. 當客戶處理多個描述字時(一般是交互式輸入和網絡套接口),必須使用I/O複用。
  2. 當一個客戶同時處理多個套接口時,而這種情況是可能的,但很少出現。
  3. 如果一個TCP服務器既要處理監聽套接口,又要處理已連接套接口,一般也要用到I/O複用。
  4. 如果一個服務器即要處理TCP,又要處理UDP,一般要使用I/O複用。
  5. 如果一個服務器要處理多個服務或多個協議,一般要使用I/O複用。

與多進程和多線程技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減小了系統的開銷。

select

描述

監視並等待多個文件描述符的屬性變化(可讀、可寫或錯誤異常)。
select函數監視的文件描述符分 3 類,分別是writefds、readfds、和 exceptfds。
調用後 select會阻塞,直到有描述符就緒(有數據可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函數才返回。
當 select()函數返回後,可以通過遍歷 fdset,來找到就緒的描述符,並且描述符最大不能超過1024

poll

描述

poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大文件描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增加而線性增大。

select 與 poll

select/poll問題很明顯,它們需要循環檢測連接是否有事件。如果服務器有上百萬個連接,在某一時間只有一個連接向服務器發送了數據,select/poll需要做循環100萬次,其中只有1次是命中的,剩下的99萬9999次都是無效的,白白浪費了CPU資源。

epoll

描述

epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制,無需輪詢。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中。
簡單點來說就是當連接有I/O流事件產生的時候,epoll就會去告訴進程哪個連接有I/O流事件產生,然後進程就去處理這個事件。

網絡服務器

單進程阻塞複用的網絡服務器 ,如下圖所示
圖片描述

描述

服務監聽流程如上
1、保存所有的socket,通過select系統調用,監聽socket描述符的可讀事件
2、select會在內核空間監聽一旦發現socket可讀,會從內核空間傳遞至用戶空間,在用戶空間通過邏輯判斷是服務端socket可讀,還是客戶端的socket可讀
3、如果是服務端的socket可讀,說明有新的客戶端建立,將socket保留到監聽數組當中
4、如果是客戶端的socket可讀,說明當前已經可以去讀取客戶端發送過來的內容了,讀取內容,然後響應給客戶端。
缺點:
1、select模式本身的缺點(1、循環遍歷處理事件、2、內核空間傳遞數據的消耗)
2、單進程對於大量任務處理乏力

代碼實現

class Worker{
    //監聽socket
    protected $socket = NULL;
    //連接事件回調
    public $onConnect = NULL;
    //接收消息事件回調
    public $onMessage = NULL;
    public $workerNum=4; //子進程個數
    public  $allSocket; //存放所有socket

    public function __construct($socket_address) {
        //監聽地址+端口
        $this->socket=stream_socket_server($socket_address);
        stream_set_blocking($this->socket,0); //設置非阻塞
        $this->allSocket[(int)$this->socket]=$this->socket;
    }
    public function start() {
        //獲取配置文件
         $this->fork();
    }

    public function fork(){
        $this->accept();//子進程負責接收客戶端請求
    }
    public  function  accept(){
        //創建多個子進程阻塞接收服務端socket
        while (true){
            $write=$except=[];
            //需要監聽socket
            $read=$this->allSocket;
            //狀態誰改變
            stream_select($read,$write,$except,60);

            //怎麼區分服務端跟客戶端
            foreach ($read as $index=>$val){
                   //當前發生改變的是服務端,有連接進入
                    if($val === $this->socket){
                        $clientSocket=stream_socket_accept($this->socket); //阻塞監聽
                        //觸發事件的連接的回調
                        if(!empty($clientSocket) && is_callable($this->onConnect)){
                            call_user_func($this->onConnect,$clientSocket);
                        }
                        $this->allSocket[(int)$clientSocket]=$clientSocket;
                    }else{
                        //從連接當中讀取客戶端的內容
                        $buffer=fread($val,1024);
                         //如果數據爲空,或者爲false,不是資源類型
                         if(empty($buffer)){
                             if(feof($val) || !is_resource($val)){
                                 //觸發關閉事件
                                fclose($val);
                                unset($this->allSocket[(int)$val]);
                                continue;
                             }
                         }
                        //正常讀取到數據,觸發消息接收事件,響應內容
                        if(!empty($buffer) && is_callable($this->onMessage)){
                            call_user_func($this->onMessage,$val,$buffer);
                        }
                    }
            }
        }

    }

}

$worker = new Worker('tcp://0.0.0.0:9805');

//連接事件
$worker->onConnect = function ($fd) {
    //echo '連接事件觸發',(int)$fd,PHP_EOL;
};

//消息接收
$worker->onMessage = function ($conn, $message) {
    //事件回調當中寫業務邏輯
    $content="回覆的消息";
    $http_resonse = "HTTP/1.1 200 OK\r\n";
    $http_resonse .= "Content-Type: text/html;charset=UTF-8\r\n";
    $http_resonse .= "Connection: keep-alive\r\n"; //連接保持
    $http_resonse .= "Server: php socket server\r\n";
    $http_resonse .= "Content-length: ".strlen($content)."\r\n\r\n";
    $http_resonse .= $content;
    fwrite($conn, $http_resonse);
};

$worker->start(); //啓動

函數

stream_socket_server

在PHP中提供了一個非常方便的函數一次性創建、綁定端口、監聽端口

stream_set_blocking ( resource $stream , int $mode ) : bool

爲資源流設置阻塞或者阻塞模式,$mode 0非阻塞,1阻塞

stream_socket_accept ( resource $server_socket \[, float $timeout = ini_get("default_socket_timeout") [, string &$peername ]] ) : resource

接受由 stream_socket_server() 創建的套接字連接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章