我們之前采用的多進程方式實現的服務器端,一次創建多個工作子進程來給客戶端提供服務。其實這種方式是存在問題的。
可以打個比方:如果我們先前創建的幾個進程承載不了目前快速發展的業務的話,是不是還得增加進程數?我們都知道系統創建進程是需要消耗大量資源的,所以這樣就會導致系統資源不足的情況。
那么有沒有一種方式可以讓一個進程同時為多個客戶端端提供服務?
接下來要講的IO復用技術就是對於上述問題的最好解答。
對於IO復用,我們可以通過一個例子來很好的理解它。(例子來自於《TCP/IP網絡編程》)
某教室有10名學生和1名老師,這些學生上課會不停的提問,所以一個老師處理不了這么多的問題。那么學校為每個學生都配一名老師,
也就是這個教室目前有10名老師。此后,只要有新的轉校生,那么就會為這個學生專門分配一個老師,因為轉校生也喜歡提問題。如果把以上例子中的學生比作客戶端,那么老師就是負責進行數據交換的服務端。則該例子可以比作是多進程的方式。
后來有一天,來了一位具有超能力的老師,這位老師回答問題非常迅速,並且可以應對所有的問題。而這位老師采用的方式是學生提問前必須先舉手,確認舉手學生后在回答問題。則現在的情況就是IO復用。
目前的常用的IO復用模型有三種:select,poll,epoll。
select模型:
說的通俗一點就是各個客戶端連接的文件描述符也就是套接字,都被放到了一個集合中,調用select函數之后會一直監視這些文件描述符中有哪些可讀,如果有可讀的描述符那么我們的工作進程就去讀取資源。PHP 中有內置的函數來完成 select 系統調用。
函數原型:
int socket_select (array &$read ,array &$write ,array &$except ,int $tv_sec [,int $tv_usec= 0 ])
作用說明:用於確定一個或多個套接字的狀態,對每一個套接字,調用者可查詢它的可讀性、可寫性及錯誤狀態信息
參數說明:
read: 指向一組等待可讀性檢查的套接字
write: 指向一組等待可寫性檢查的套接字
except: 指向一組等待錯誤檢查的套接字
tv_sec: 用來設置 select() 的等待時間,秒
tv_usec: 用來設置 select() 的等待時間,微妙
這里注意一下,如果 tv_sec 設置為0,則 socket_select 立即返回,也就是非阻塞的。如果 tv_sec 設置為 null ,則 socket_select 將一直阻塞到有套接字滿足條件。
下面通過代碼代碼來簡單舉例:
poll模型:
poll 和 select 的實現非常類似,本質上的區別就是存放 fd 集合的數據結構不一樣。select 在一個進程內可以維持最多 1024 個連接,poll 在此基礎上做了加強,可以維持任意數量的連接。
但 select 和 poll 方式有一個很大的問題就是,我們不難看出來 select 是通過輪訓的方式來查找是否可讀或者可寫,打個比方,如果同時有100萬個連接都沒有斷開,而只有一個客戶端發送了數據,所以這里它還是需要循環這么多次,造成資源浪費。
所以后來出現了 epoll系統調用。
epoll模型:
epoll 是 select 和 poll 的增強版,epoll 同 poll 一樣,文件描述符數量無限制。
epoll是基於內核的反射機制,在有活躍的 socket 時,系統會調用我們提前設置的回調函數。而 poll 和 select 都是遍歷。
但是也並不是所有情況下 epoll 都比 select/poll 好,比如在如下場景:
在大多數客戶端都很活躍的情況下,系統會把所有的回調函數都喚醒,所以會導致負載較高。既然要處理這么多的連接,那倒不如 select 遍歷簡單有效。
在 PHP 中我們可以使用 libevet 拓展來實現 epoll。
libevent 是一個用C語言寫的,基於事件驅動的高性能網絡庫。支持多種 I/O 多路復用技術,epoll、 poll、 dev/poll、 select 和 kqueue 等。 libevent 同時為文件描述符、信號、超時設定等事件提供了監聽回調。所以這種編程方式也可以說是事件編程。
先放代碼體驗一番:
服務端:
客戶端:
先說簡單的客戶端,客戶端的主要作用也就是像服務端發送了兩句話。第一句是 hello world!,然后等待兩秒之后再次發送 send again!
並且每次發送之后都將接收到服務端返回的字節數。
講解服務端之前先了解一下關於時間循環的一些函數:
event_base_new 創建一個事件庫(只需創建一次)
event_new 創建事件
event_set 為創建的事件設置要監聽文件描述符fd,以及事件類型、回調函數
event_base_set 將創建的事件與事件庫關聯
event_add 將設置好的事件加入事件監聽器
event_base_loop 開啟事件循環
還有 event_set 的幾個參數:
* EV_TIMEOUT: 超時
* EV_READ: 只要網絡緩沖中還有數據,回調函數就會被觸發
* EV_WRITE: 只要塞給網絡緩沖的數據被寫完,回調函數就會被觸發
* EV_SIGNAL: POSIX信號量
* EV_PERSIST: 不指定這個屬性的話,回調函數被觸發后事件會被刪除
服務端的三個函數:
read_cb() 接受數據,發送數據
error_cb() 錯誤處理
accept_cb() 受理請求並且把新的文件描述符加入事件庫,同時注冊 read_cb 回調
整個服務端的主要流程如下:
1.創建事件庫
2.設置事件回調
3.綁定事件
4.開始事件循環
5.如有符合條件的文件描述符則系統開始調用我們提前設定好的處理函數
至於詳細的流程我就不分析了,大致流程應該都能理解,接下來就靠自己鞏固了。一定要自己動手實踐才行。
當然本文也只是起到拋磚引玉而已,以上有問題的地方歡迎評論指出。
在php 的libevent擴展具有如下函數:
event_base_free() 釋放資源,這不能銷毀綁定事件
event_base_loop() 處理事件,根據指定的base來處理事件循環
event_base_loopbreak() 立即取消事件循環,行為和break語句相同
event_base_loopexit() 在指定的時間后退出循環
event_base_new() 創建並且初始事件
event_base_priority_init() 設定事件的優先級
event_base_set() 關聯事件到事件base
event_buffer_base_set() 關聯緩存的事件到event_base
event_buffer_disable() 禁用一個緩存的事件
event_buffer_enable() 啟用一個指定的緩存的事件
event_buffer_fd_set() 改變一個緩存的文件系統描述
event_buffer_free() 釋放緩存事件
event_buffer_new() 建立一個新的緩存事件
event_buffer_priority_set() 緩存事件的優先級設定
event_buffer_read() 讀取緩存事件中的數據
event_buffer_set_callback() 給緩存的事件設置或重置回調hansh函數
event_buffer_timeout_set() 給一個緩存的事件設定超時的讀寫時間
event_buffer_watermark_set 設置讀寫事件的水印標記
event_buffer_write() 向緩存事件中寫入數據
event_add() 向指定的設置中添加一個執行事件
event_del() 從設置的事件中移除事件
event_free() 清空事件句柄
event_new() 創建一個新的事件
event_set() 准備想要在event_add中添加事件
event_set一些參數的解釋:
(a) EV_TIMEOUT: 超時
(b) EV_READ: 只要網絡緩沖中還有數據,回調函數就會被觸發
(c) EV_WRITE: 只要塞給網絡緩沖的數據被寫完,回調函數就會被觸發
(d) EV_SIGNAL: POSIX信號量
(e) EV_PERSIST: 不指定這個屬性的話,回調函數被觸發后事件會被刪除
(f) EV_ET: Edge-Trigger邊緣觸發