作者:阿凡盧
服務器端幾種模型:
1、阻塞式模型(blocking IO)
我們第一次接觸到的網絡編程都是從 listen()、accpet()、send()、recv() 等接口開始的。使用這些接口可以很方便的構建C/S的模型。這里大部分的 socket 接口都是阻塞型的。所謂阻塞型接口是指系統調用(一般是 IO 接口)不返回調用結果并讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。
如下面一個簡單的Server端實現:

示意圖如下:
這里的socket的接口是阻塞的(blocking),在線程被阻塞期間,線程將無法執行任何運算或響應任何的網絡請求,這給多客戶機、多業務邏輯的網絡編程帶來了挑戰。
2、多線程的服務器模型(Multi-Thread)
應對多客戶機的網絡應用,最簡單的解決方式是在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。
多線程Server端的實現:

上述多線程的服務器模型可以解決一些連接量不大的多客戶端連接請求,但是如果要同時響應成千上萬路的連接請求,則無論多線程還是多進程都會嚴重占據系統資源,降低系統對外界響應效率。
在多線程的基礎上,可以考慮使用“線程池”或“連接池”,“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,并讓空閑的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,盡量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統。
3、非阻塞式模型(Non-blocking IO)
非阻塞的接口相比于阻塞型接口的顯著差異在于,在被調用之后立即返回。
非阻塞型IO的示意圖如下:
從應用程序的角度來說,blocking read 調用會延續很長時間。在內核執行讀操作和其他工作時,應用程序會被阻塞。
非阻塞的IO可能并不會立即滿足,需要應用程序調用許多次來等待操作完成。這可能效率不高,因為在很多情況下,當內核執行這個命令時,應用程序必須要進行忙碌等待,直到數據可用為止。
另一個問題,在循環調用非阻塞IO的時候,將大幅度占用CPU,所以一般使用select等來檢測”是否可以操作“。
4、多路復用IO
支持I/O復用的系統調用有select、poll、epoll、kqueue等,
這里以Select函數為例,select函數用于探測多個文件句柄的狀態變化,以下為一個使用了使用了Select函數的Server實現:

示意圖如下:
這里Select監聽的socket都是Non-blocking的,所以在do_read() do_write()中對返回為EAGAIN/WSAEWOULDBLOCK都做了處理。
從代碼中可以看出使用Select返回后,仍然需要輪訓再檢測每個socket的狀態(讀、寫),這樣的輪訓檢測在大量連接下也是效率不高的。因為當需要探測的句柄值較大時,select?() 接口本身需要消耗大量時間去輪詢各個句柄。
很多操作系統提供了更為高效的接口,如 linux 提供?了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。如果需要實現更高效的服務器程序,類似 epoll 這樣的接口更被推薦。遺憾的是不同的操作系統特供的 epoll 接口有很大差異,所以使用類似于 epoll 的接口實現具有較好跨平臺能力的服務器會比較困難。
5、使用事件驅動庫libevent的服務器模型
Libevent 是一種高性能事件循環/事件驅動庫。
為了實際處理每個請求,libevent 庫提供一種事件機制,它作為底層網絡后端的包裝器。事件系統讓為連接添加處理函數變得非常簡便,同時降低了底層IO復雜性。這是 libevent 系統的核心。
創建 libevent 服務器的基本方法是,注冊當發生某一操作(比如接受來自客戶端的連接)時應該執行的函數,然后調用主事件循環 event_dispatch()。執行過程的控制現在由 libevent 系統處理。注冊事件和將調用的函數之后,事件系統開始自治;在應用程序運行時,可以在事件隊列中添加(注冊)或?刪除(取消注冊)事件。事件注冊非常方便,可以通過它添加新事件以處理新打開的連接,從而構建靈活的網絡處理系統。
使用Libevent實現的一個回顯服務器如下:

6、信號驅動IO模型(Signal-driven IO)
使用信號,讓內核在描述符就緒時發送SIGIO信號通知應用程序,稱這種模型為信號驅動式I/O(signal-driven I/O)。
圖示如下:
首先開啟套接字的信號驅動式I/O功能,并通過sigaction系統調用安裝一個信號處理函數。該系統調用將立即返回,我們的進程繼續工作,也就是說進程沒有被阻塞。當數據報準備好讀取時,內核就為該進程產生一個SIGIO信號。隨后就可以在信號處理函數中調用recvfrom讀取數據報,并通知主循環數據已經準備好待處理,也可以立即通知主循環,讓它讀取數據報。
無論如何處理SIGIO信號,這種模型的優勢在于等待數據報到達期間進程不被阻塞。主循環可以繼續執行?,只要等到來自信號處理函數的通知:既可以是數據已準備好被處理,也可以是數據報已準備好被讀取。
7、異步IO模型(asynchronous IO)
異步I/O(asynchronous I/O)由POSIX規范定義。演變成當前POSIX規范的各種早起標準所定義的實時函數中存在的差異已經取得一致。一般地說,這些函數的工作機制是:告知內核啟動某個操作,并讓內核在整個操作(包括將數據從內核復制到我們自己的緩沖區)完成后通知我們。這種模型與前一節介紹的信號驅動模型的主要區別在于:信號驅動式I/O是由內核通知我們何時可以啟動一個I/O操作,而異步I/O模型是由內核通知我們I/O操作何時完成。
示意圖如下:
我們調用aio_read函數(POSIX異步I/O函數以aio_或lio_開頭),給內核傳遞描述符、緩沖區指針、緩沖區大小(與read相同的三個參數)和文件偏移(與lseek類似),并告訴內核當整個操作完成時如何通知我們。該系統調用立即返回,并且在等待I/O完成期間,我們的進程不被阻塞。本例子中我們假設要求內核在操作完成時產生某個信號,該信號直到數據已復制到應用進程緩沖區才產生,這一點不同于信號驅動I/O模型。
?
參考:
《UNIX網絡編程》
使用 libevent 和 libev 提高網絡應用性能:http://www.ibm.com/developerworks/cn/aix/library/au-libev/
使用異步 I/O 大大提高應用程序的性能:https://www.ibm.com/developerworks/cn/linux/l-async/
?