幾種經典的網絡服務器架構模型的分析與比較

原文出處:http://blog.csdn.net/lmh12506/article/details/7753978linux

 

前言程序員

事件驅動爲廣大的程序員所熟悉,其最爲人津津樂道的是在圖形化界面編程中的應用;事實上,在網絡編程中事件驅動也被普遍使用,並大規模部署在高鏈接數高吞吐量的服務器程序中,如 http 服務器程序、ftp 服務器程序等。相比於傳統的網絡編程方式,事件驅動可以極大的下降資源佔用,增大服務接待能力,並提升網絡傳輸效率。web

關於本文說起的服務器模型,搜索網絡能夠查閱到不少的實現代碼,因此,本文將不拘泥於源代碼的陳列與分析,而側重模型的介紹和比較。使用 libev 事件驅動庫的服務器模型將給出實現代碼。數據庫

本文涉及到線程 / 時間圖例,只爲代表線程在各個 IO 上確實存在阻塞時延,但並不保證時延比例的正確性和 IO 執行前後的正確性;另外,本文所說起到的接口也只是筆者熟悉的 Unix/Linux 接口,並未推薦 Windows 接口,讀者能夠自行查閱對應的 Windows 接口。編程

阻塞型的網絡編程接口緩存

幾乎全部的程序員第一次接觸到的網絡編程都是從 listen()、send()、recv()等接口開始的。使用這些接口能夠很方便的構建服務器 /客戶機的模型。tomcat

咱們假設但願創建一個簡單的服務器程序,實現向單個客戶機提供相似於「一問一答」的內容服務。安全

圖 1. 簡單的一問一答的服務器 /客戶機模型服務器

幾種經典的網絡服務器架構模型的分析與比較

咱們注意到,大部分的 socket接口都是阻塞型的。所謂阻塞型接口是指系統調用(通常是 IO接口)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用得到結果或者超時出錯時才返回。網絡

實際上,除非特別指定,幾乎全部的 IO接口 (包括 socket 接口 )都是阻塞型的。這給網絡編程帶來了一個很大的問題,如在調用 send()的同時,線程將被阻塞,在此期間,線程將沒法執行任何運算或響應任何的網絡請求。這給多客戶機、多業務邏輯的網絡編程帶來了挑戰。這時,不少程序員可能會選擇多線程的方式來解決這個問題。

多線程服務器程序

應對多客戶機的網絡應用,最簡單的解決方式是在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每一個鏈接都擁有獨立的線程(或進程),這樣任何一個鏈接的阻塞都不會影響其餘的鏈接。

具體使用多進程仍是多線程,並無一個特定的模式。傳統意義上,進程的開銷要遠遠大於線程,因此,若是須要同時爲較多的客戶機提供服務,則不推薦使用多進程;若是單個服務執行體須要消耗較多的 CPU 資源,譬如須要進行大規模或長時間的數據運算或文件訪問,則進程較爲安全。一般,使用 pthread_create () 建立新線程,fork() 建立新進程。

咱們假設對上述的服務器 / 客戶機模型,提出更高的要求,即讓服務器同時爲多個客戶機提供一問一答的服務。因而有了以下的模型。

圖 2. 多線程服務器模型
幾種經典的網絡服務器架構模型的分析與比較

在上述的線程 / 時間圖例中,主線程持續等待客戶端的鏈接請求,若是有鏈接,則建立新線程,並在新線程中提供爲前例一樣的問答服務。

不少初學者可能不明白爲什麼一個 socket 能夠 accept 屢次。實際上,socket 的設計者可能特地爲多客戶機的狀況留下了伏筆,讓 accept() 可以返回一個新的 socket。下面是 accept 接口的原型:

1
int accept( int s, struct sockaddr *addr, socklen_t *addrlen);

輸入參數 s 是從 socket(),bind() 和 listen() 中沿用下來的 socket 句柄值。執行完 bind() 和 listen() 後,操做系統已經開始在指定的端口處監聽全部的鏈接請求,若是有請求,則將該鏈接請求加入請求隊列。調用 accept() 接口正是從 socket s 的請求隊列抽取第一個鏈接信息,建立一個與 s 同類的新的 socket 返回句柄。新的 socket 句柄便是後續 read() 和 recv() 的輸入參數。若是請求隊列當前沒有請求,則 accept() 將進入阻塞狀態直到有請求進入隊列。

上述多線程的服務器模型彷佛完美的解決了爲多個客戶機提供問答服務的要求,但其實並不盡然。若是要同時響應成百上千路的鏈接請求,則不管多線程仍是多進程都會嚴重佔據系統資源,下降系統對外界響應效率,而線程與進程自己也更容易進入假死狀態。

不少程序員可能會考慮使用「線程池」或「鏈接池」。「線程池」旨在減小建立和銷燬線程的頻率,其維持必定合理數量的線程,並讓空閒的線程從新承擔新的執行任務。「鏈接池」維持鏈接的緩存池,儘可能重用已有的鏈接、減小建立和關閉鏈接的頻率。這兩種技術均可以很好的下降系統開銷,都被普遍應用不少大型系統,如 websphere、tomcat 和各類數據庫等。

可是,「線程池」和「鏈接池」技術也只是在必定程度上緩解了頻繁調用 IO 接口帶來的資源佔用。並且,所謂「池」始終有其上限,當請求大大超過上限時,「池」構成的系統對外界的響應並不比沒有池的時候效果好多少。因此使用「池」必須考慮其面臨的響應規模,並根據響應規模調整「池」的大小。

對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,「線程池」或「鏈接池」或許能夠緩解部分壓力,可是不能解決全部問題。

總之,多線程模型能夠方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型並非最佳方案。下一章咱們將討論用非阻塞接口來嘗試解決這個問題。

使用select()接口的基於事件驅動的服務器模型

大部分 Unix/Linux 都支持 select 函數,該函數用於探測多個文件句柄的狀態變化。下面給出 select 接口的原型:

1
2
3
4
5
6
FD_ZERO( int fd, fd_set* fds)
FD_SET( int fd, fd_set* fds)
FD_ISSET( int fd, fd_set* fds)
FD_CLR( int fd, fd_set* fds)
int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
        struct timeval *timeout)

這裏,fd_set 類型能夠簡單的理解爲按 bit 位標記句柄的隊列,例如要在某 fd_set 中標記一個值爲 16 的句柄,則該 fd_set 的第 16 個 bit 位被標記爲 1。具體的置位、驗證可以使用 FD_SET、FD_ISSET 等宏實現。在 select() 函數中,readfds、writefds 和 exceptfds 同時做爲輸入參數和輸出參數。若是輸入的 readfds 標記了 16 號句柄,則 select() 將檢測 16 號句柄是否可讀。在 select() 返回後,能夠經過檢查 readfds 有否標記 16 號句柄,來判斷該「可讀」事件是否發生。另外,用戶能夠設置 timeout 時間。

下面將從新模擬上例中從多個客戶端接收數據的模型。

圖4.使用select()的接收數據模型
幾種經典的網絡服務器架構模型的分析與比較

上述模型只是描述了使用 select() 接口同時從多個客戶端接收數據的過程;因爲 select() 接口能夠同時對多個句柄進行讀狀態、寫狀態和錯誤狀態的探測,因此能夠很容易構建爲多個客戶端提供獨立問答服務的服務器系統。

圖5.使用select()接口的基於事件驅動的服務器模型

幾種經典的網絡服務器架構模型的分析與比較

這裏須要指出的是,客戶端的一個 connect() 操做,將在服務器端激發一個「可讀事件」,因此 select() 也能探測來自客戶端的 connect() 行爲。

上述模型中,最關鍵的地方是如何動態維護 select() 的三個參數 readfds、writefds 和 exceptfds。做爲輸入參數,readfds 應該標記全部的須要探測的「可讀事件」的句柄,其中永遠包括那個探測 connect() 的那個「母」句柄;同時,writefds 和 exceptfds 應該標記全部須要探測的「可寫事件」和「錯誤事件」的句柄 ( 使用 FD_SET() 標記 )。

做爲輸出參數,readfds、writefds 和 exceptfds 中的保存了 select() 捕捉到的全部事件的句柄值。程序員須要檢查的全部的標記位 ( 使用 FD_ISSET() 檢查 ),以肯定到底哪些句柄發生了事件。

上述模型主要模擬的是「一問一答」的服務流程,因此,若是 select() 發現某句柄捕捉到了「可讀事件」,服務器程序應及時作 recv() 操做,並根據接收到的數據準備好待發送數據,並將對應的句柄值加入 writefds,準備下一次的「可寫事件」的 select() 探測。一樣,若是 select() 發現某句柄捕捉到「可寫事件」,則程序應及時作 send() 操做,並準備好下一次的「可讀事件」探測準備。下圖描述的是上述模型中的一個執行週期。

圖6. 一個執行週期

幾種經典的網絡服務器架構模型的分析與比較

這種模型的特徵在於每個執行週期都會探測一次或一組事件,一個特定的事件會觸發某個特定的響應。咱們能夠將這種模型歸類爲「事件驅動模型」。

相比其餘模型,使用 select() 的事件驅動模型只用單線程(進程)執行,佔用資源少,不消耗太多 CPU,同時可以爲多客戶端提供服務。若是試圖創建一個簡單的事件驅動的服務器程序,這個模型有必定的參考價值。

但這個模型依舊有着不少問題。

首先,select() 接口並非實現「事件驅動」的最好選擇。由於當須要探測的句柄值較大時,select() 接口自己須要消耗大量時間去輪詢各個句柄。不少操做系統提供了更爲高效的接口,如 linux 提供了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。若是須要實現更高效的服務器程序,相似 epoll 這樣的接口更被推薦。遺憾的是不一樣的操做系統特供的 epoll 接口有很大差別,因此使用相似於 epoll 的接口實現具備較好跨平臺能力的服務器會比較困難。

其次,該模型將事件探測和事件響應夾雜在一塊兒,一旦事件響應的執行體龐大,則對整個模型是災難性的。以下例,龐大的執行體 1 的將直接致使響應事件 2 的執行體遲遲得不到執行,並在很大程度上下降了事件探測的及時性。

圖7. 龐大的執行體對使用select()的事件驅動模型的影響
幾種經典的網絡服務器架構模型的分析與比較

幸運的是,有不少高效的事件驅動庫能夠屏蔽上述的困難,常見的事件驅動庫有 libevent 庫,還有做爲 libevent 替代者的 libev 庫。這些庫會根據操做系統的特色選擇最合適的事件探測接口,而且加入了信號 (signal) 等技術以支持異步響應,這使得這些庫成爲構建事件驅動模型的不二選擇。下章將介紹如何使用 libev 庫替換 select 或 epoll 接口,實現高效穩定的服務器模型。

使用事件驅動庫libev的服務器模型

Libev 是一種高性能事件循環 / 事件驅動庫。做爲 libevent 的替代做品,其第一個版本發佈與 2007 年 11 月。Libev 的設計者聲稱 libev 擁有更快的速度,更小的體積,更多功能等優點,這些優點在不少測評中獲得了證實。正由於其良好的性能,不少系統開始使用 libev 庫。本章將介紹如何使用 Libev 實現提供問答服務的服務器。

(事實上,現存的事件循環 / 事件驅動庫有不少,做者也無心推薦讀者必定使用 libev 庫,而只是爲了說明事件驅動模型給網絡服務器編程帶來的便利和好處。大部分的事件驅動庫都有着與 libev 庫相相似的接口,只要明白大體的原理,便可靈活挑選合適的庫。)

與前章的模型相似,libev 一樣須要循環探測事件是否產生。Libev 的循環體用 ev_loop 結構來表達,並用 ev_loop( ) 來啓動。

1
void ev_loop( ev_loop* loop, int flags )

Libev 支持八種事件類型,其中包括 IO 事件。一個 IO 事件用 ev_io 來表徵,並用 ev_io_init() 函數來初始化:

1
void ev_io_init(ev_io *io, callback, int fd, int events)

初始化內容包括回調函數 callback,被探測的句柄 fd 和須要探測的事件,EV_READ 表「可讀事件」,EV_WRITE 表「可寫事件」。

如今,用戶須要作的僅僅是在合適的時候,將某些 ev_io 從 ev_loop 加入或剔除。一旦加入,下個循環即會檢查 ev_io 所指定的事件有否發生;若是該事件被探測到,則 ev_loop 會自動執行 ev_io 的回調函數 callback();若是 ev_io 被註銷,則再也不檢測對應事件。

不管某 ev_loop 啓動與否,均可以對其添加或刪除一個或多個 ev_io,添加刪除的接口是 ev_io_start() 和 ev_io_stop()。

1
2
void ev_io_start( ev_loop *loop, ev_io* io )
void ev_io_stop( EV_A_* )

由此,咱們能夠容易得出以下的「一問一答」的服務器模型。因爲沒有考慮服務器端主動終止鏈接機制,因此各個鏈接能夠維持任意時間,客戶端能夠自由選擇退出時機。

圖8. 使用libev庫的服務器模型
幾種經典的網絡服務器架構模型的分析與比較

上述模型能夠接受任意多個鏈接,且爲各個鏈接提供徹底獨立的問答服務。藉助 libev 提供的事件循環 / 事件驅動接口,上述模型有機會具有其餘模型不能提供的高效率、低資源佔用、穩定性好和編寫簡單等特色。

因爲傳統的 web 服務器,ftp 服務器及其餘網絡應用程序都具備「一問一答」的通信邏輯,因此上述使用 libev 庫的「一問一答」模型對構建相似的服務器程序具備參考價值;另外,對於須要實現遠程監視或遠程遙控的應用程序,上述模型一樣提供了一個可行的實現方案。

總結

本文圍繞如何構建一個提供「一問一答」的服務器程序,前後討論了用阻塞型的 socket 接口實現的模型,使用多線程的模型,使用 select() 接口的基於事件驅動的服務器模型,直到使用 libev 事件驅動庫的服務器模型。文章對各類模型的優缺點都作了比較,從比較中得出結論,即便用「事件驅動模型」能夠的實現更爲高效穩定的服務器程序。文中描述的多種模型能夠爲讀者的網絡編程提供參考價值。

相關文章
相關標籤/搜索