一次讀懂 Select、Poll、Epoll IO複用技術

一次讀懂 Select、Poll、Epoll IO複用技術

“ 閱讀本文大概需要 6 分鐘。”

我們之前採用的多程序方式實現的伺服器端,一次建立多個工作子程序來給客戶端提供服務。其實這種方式是存在問題的。

可以打個比方:如果我們先前建立的幾個程序承載不了目前快速發展的業務的話,是不是還得增加程序數?我們都知道系統建立程序是需要消耗大量資源的,所以這樣就會導致系統資源不足的情況。

那麼有沒有一種方式可以讓一個程序同時為多個客戶端端提供服務?

接下來要講的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 將一直阻塞到有套接字滿足條件。

下面透過程式碼程式碼來簡單舉例:

一次讀懂 Select、Poll、Epoll IO複用技術

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 同時為檔案描述符、訊號、超時設定等事件提供了監聽回撥。所以這種程式設計方式也可以說是事件程式設計。

先放程式碼體驗一番:

服務端:

一次讀懂 Select、Poll、Epoll IO複用技術

客戶端:

一次讀懂 Select、Poll、Epoll IO複用技術

先說簡單的客戶端,客戶端的主要作用也就是像服務端傳送了兩句話。第一句是 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),記錄自己的自學程式設計之路。同時希望自己的分享能夠幫助一些對程式設計感興趣以及正在程式設計道路上的朋友。喜歡的話可以關注我的頭條號哦!

私信“java”、“php”、“python”可免費領取學習資料哦!系統自動傳送~