文章開篇先腦補一些知識,有助於閱讀,本篇文章主要以select爲住,介紹select實現原理,並利用select來實現一個單進程阻塞複用的網絡服務器。
IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取,它就通知該進程,目前支持I/O多路複用有 select,poll,epoll,I/O多路複用就是經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做,IO多路複用適用以下場合:php
與多進程和多線程技術相比,I/O多路複用技術的最大優點是系統開銷小,系統沒必要建立進程/線程,也沒必要維護這些進程/線程,從而大大減少了系統的開銷。html
監視並等待多個文件描述符的屬性變化(可讀、可寫或錯誤異常)。
select函數監視的文件描述符分 3 類,分別是writefds、readfds、和 exceptfds。
調用後 select會阻塞,直到有描述符就緒(有數據可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函數才返回。
當 select()函數返回後,能夠經過遍歷 fdset,來找到就緒的描述符,而且描述符最大不能超過1024數組
poll的機制與select相似,與select在本質上沒有多大差異,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,可是poll沒有最大文件描述符數量的限制。poll和select一樣存在一個缺點就是,包含大量文件描述符的數組被總體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增長而線性增大。服務器
select/poll問題很明顯,它們須要循環檢測鏈接是否有事件。若是服務器有上百萬個鏈接,在某一時間只有一個鏈接向服務器發送了數據,select/poll須要作循環100萬次,其中只有1次是命中的,剩下的99萬9999次都是無效的,白白浪費了CPU資源。網絡
epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制,無需輪詢。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中。
簡單點來講就是當鏈接有I/O流事件產生的時候,epoll就會去告訴進程哪一個鏈接有I/O流事件產生,而後進程就去處理這個事件。多線程
單進程阻塞複用的網絡服務器 ,以下圖所示socket
服務監聽流程如上
一、保存全部的socket,經過select系統調用,監聽socket描述符的可讀事件
二、select會在內核空間監聽一旦發現socket可讀,會從內核空間傳遞至用戶空間,在用戶空間經過邏輯判斷是服務端socket可讀,仍是客戶端的socket可讀
三、若是是服務端的socket可讀,說明有新的客戶端創建,將socket保留到監聽數組當中
四、若是是客戶端的socket可讀,說明當前已經能夠去讀取客戶端發送過來的內容了,讀取內容,而後響應給客戶端。
缺點:
一、select模式自己的缺點(一、循環遍歷處理事件、二、內核空間傳遞數據的消耗)
二、單進程對於大量任務處理乏力tcp
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(); //啓動
在PHP中提供了一個很是方便的函數一次性建立、綁定端口、監聽端口
stream_set_blocking ( resource $stream , int $mode ) : boolthis
爲資源流設置阻塞或者阻塞模式,$mode 0非阻塞,1阻塞
接受由 stream_socket_server() 建立的套接字鏈接