前面介紹了I/O多路複用模型,那有了I/O複用,有了epoll已經可使服務器併發幾十萬鏈接的同時,還能維持比較高的TPS,難道還不夠嗎?好比如今在使用epoll的時候通常都是起個任務,不斷的去巡檢事件,而後通知處理,而比較理想的方式是最好能以一種回調的機制,提供一個編程框架,讓程序更有結構些,另外一方面,若是但願每一個事件通知以後,作的事情能有機會被代理到某個線程裏面去單獨運行,而線程完成的狀態又能通知回主任務,那麼"異步"的進制就必須被引入。因此這個章節主要介紹下"編程框架"。html
Reactor模式是處理併發I/O比較常見的一種模式,用於同步I/O,中心思想是將全部要處理的I/O事件註冊到一箇中心I/O多路複用器上,同時主線程/進程阻塞在多路複用器上;一旦有I/O事件到來或是準備就緒(文件描述符或socket可讀、寫),多路複用器返回並將事先註冊的相應I/O事件分發到對應的處理器中。react
Reactor是一種事件驅動機制,和普通函數調用的不一樣之處在於:應用程序不是主動的調用某個API完成處理,而是偏偏相反,Reactor逆置了事件處理流程,應用程序須要提供相應的接口並註冊到Reactor上,若是相應的事件發生,Reactor將主動調用應用程序註冊的接口,這些接口又稱爲「回調函數」。用「好萊塢原則」來形容Reactor再合適不過了:不要打電話給咱們,咱們會打電話通知你。linux
Reactor模式與Observer模式在某些方面極爲類似:當一個主體發生改變時,全部依屬體都獲得通知。不過,觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯 。nginx
在Reactor模式中,有5個關鍵的參與者:程序員
這部份內容主要來自:https://blog.csdn.net/russell_tao/article/details/17452997
傳統編程方法
就好像是到了銀行營業廳裏,每一個窗口前排了長隊,業務員們在窗口後一個個的解決客戶們的請求。一個業務員能夠盡情思考着客戶A依次提出的問題,例如:
「我要買2萬XX理財產品。「
「看清楚了,5萬起售。」
「等等,查下我活期餘額。」
「餘額5萬。」
「那就買 5萬吧。」
業務員開始錄入信息。
」對了,XX理財產品年利率8%?」
「是預期8%,最低無利息保本。「
」早不說,拜拜,我去買餘額寶。「
業務員無表情的刪着已經錄入的信息進行事務回滾。
「下一個!」web
IO複用方法
用了IO複用則是大師業務員開始挑戰極限,在超大營業廳裏給客戶們人手一個牌子,黑壓壓的客戶們都在大廳中,有問題時舉牌申請提問,大師目光敏銳點名指定某人提問,該客戶迅速獲得大師的答覆後,要通過一段時間思考,查查本身的銀袋子,諮詢下LD,才能再次進行下一個提問,直到獲得完整的滿意答覆退出大廳。例如:大師剛指導A填寫轉賬單的某一項,B又來申請兌換泰銖,給了B兌換單後,C又來辦理定轉活,而後D與F在爭搶有限的圓珠筆時出現了不和諧現象,被大師叫停業務,暫時等待。redis
這就是基於事件驅動的IO複用編程比起傳統1線程1請求的方式來,有難度的設計點了,客戶們都是上帝,既不能出錯,還不能厚此薄彼。編程
當沒有Reactor時,咱們可能的設計方法是這樣的:大師把每一個客戶的提問都記錄下來,當客戶A提問時,首先查閱A以前問過什麼作過什麼,這叫聯繫上下文,而後再根據上下文和當前提問查閱有關的銀行規章制度,有針對性的回答A,並把回答也記錄下來。當圓滿回答了A的全部問題後,刪除A的全部記錄。windows
某一瞬間,服務器共有10萬個併發鏈接,此時,一次IO複用接口的調用返回了100個活躍的鏈接等待處理。先根據這100個鏈接找出其對應的對象,這並不難,epoll的返回鏈接數據結構裏就有這樣的指針能夠用。接着,循環的處理每個鏈接,找出這個對象此刻的上下文狀態,再使用read、write這樣的網絡IO獲取這次的操做內容,結合上下文狀態查詢此時應當選擇哪一個業務方法處理,調用相應方法完成操做後,若請求結束,則刪除對象及其上下文。後端
這樣,咱們就陷入了面向過程編程方法之中了,在面向應用、快速響應爲王的移動互聯網時代,這樣作遲早得把本身玩死。咱們的主程序須要關注各類不一樣類型的請求,在不一樣狀態下,對於不一樣的請求命令選擇不一樣的業務處理方法。這會致使隨着請求類型的增長,請求狀態的增長,請求命令的增長,主程序複雜度快速膨脹,致使維護愈來愈困難,苦逼的程序員不再敢輕易接新需求、重構。
Reactor是解決上述軟件工程問題的一種途徑,它也許並不優雅,開發效率上也不是最高的,但其執行效率與面向過程的使用IO複用卻幾乎是等價的,因此,不管是nginx、memcached、redis等等這些高性能組件的代名詞,都義無反顧的一頭扎進了反應堆的懷抱中。
Reactor模式能夠在軟件工程層面,將事件驅動框架分離出具體業務,將不一樣類型請求之間用OO的思想分離。一般,Reactor不只使用IO複用處理網絡事件驅動,還會實現定時器來處理時間事件的驅動(請求的超時處理或者定時任務的處理),就像下面的示意圖:
這幅圖有5點意思:
參考資料:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
在web服務中,不少都涉及基本的操做:read request、decode request、process service、encod reply、send reply等。
這是最簡單的Reactor單線程模型。Reactor線程是個多面手,負責多路分離套接字,Accept新鏈接,並分派請求處處理器鏈中。該模型適用於處理器鏈中業務處理組件能快速完成的場景。不過這種單線程模型不能充分利用多核資源,因此實際使用的很少。
該模型在事件處理器(Handler)鏈部分採用了多線程(線程池),也是後端程序經常使用的模型。
比起第二種模型,它是將Reactor分紅兩部分,mainReactor負責監聽並accept新鏈接,而後將創建的socket經過多路複用器(Acceptor)分派給subReactor。subReactor負責多路分離已鏈接的socket,讀寫網絡數據;業務處理功能,其交給worker線程池完成。一般,subReactor個數上可與CPU個數等同。
Proactor是和異步I/O相關的。
在Reactor模式中,事件分離者等待某個事件或者可應用多個操做的狀態發生(好比文件描述符可讀寫,或者是socket可讀寫),事件分離器就把這個事件傳給事先註冊的處理器(事件處理函數或者回調函數),由後者來作實際的讀寫操做。
在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操做(至關於請求),而實際的工做是由操做系統來完成的。發起時,須要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分離者得知了這個請求,它默默等待這個請求的完成,而後轉發完成事件給相應的事件處理者或者回調。
能夠看出二者的區別:Reactor是在事件發生時就通知事先註冊的事件(讀寫由處理函數完成);Proactor是在事件發生時進行異步I/O(讀寫由OS完成),待IO完成事件分離器才調度處理器來處理。
舉個例子,將有助於理解Reactor與Proactor兩者的差別,以讀操做爲例(類操做相似)。
在Reactor(同步)中實現讀:
Proactor(異步)中的讀:
對比幾個常見的I/O編程框架:libevent,libev,libuv,aio,boost.Asio。
libevent是一個C語言寫的網絡庫,官方主要支持的是類linux操做系統,最新的版本添加了對windows的IOCP的支持。在跨平臺方面主要經過select模型來進行支持。
設計模式 :libevent爲Reactor模式;
層次架構:livevent在不一樣的操做系統下,作了多路複用模型的抽象,能夠選擇使用不一樣的模型,經過事件函數提供服務;
可移植性 :libevent主要支持linux平臺,freebsd平臺,其餘平臺下經過select模型進行支持,效率不是過高;
事件分派處理 :libevent基於註冊的事件回調函數來實現事件分發;
涉及範圍 :libevent只提供了簡單的網絡API的封裝,線程池,內存池,遞歸鎖等均須要本身實現;
線程調度 :libevent的線程調度須要本身來註冊不一樣的事件句柄;
發佈方式 :libevent爲開源免費的,通常編譯爲靜態庫進行使用;
開發難度 :基於libevent開發應用,相對容易,具體能夠參考memcached這個開源的應用,裏面使用了 libevent這個庫。
libevent/libev是兩個名字至關相近的I/O Library。既然是庫,第一反應就是對api的包裝。epoll在linux上已經存在了好久,可是linux是SysV的後代,BSD及其衍生的MAC就沒有,只有kqueue。
libev v.s libevent。既然已經有了libevent,爲何還要發明一個輪子叫作libev?
http://www.cnblogs.com/Lifehacker/p/whats_the_difference_between_libevent_and_libev_chinese.html
http://stackoverflow.com/questions/9433864/whats-the-difference-between-libev-and-libevent
上面是libev的做者對於這個問題的回答,下面是網摘的中文翻譯:
就設計哲學來講,libev的誕生,是爲了修復libevent設計上的一些錯誤決策。例如,全局變量的使用,讓libevent很難在多線程環境中使用。watcher結構體很大,由於它們包含了I/O,定時器和信號處理器。額外的組件如HTTP和DNS服務器,由於拙劣的實現品質和安全問題而備受折磨。定時器不精確,並且沒法很好地處理時間跳變。
總而言之,libev試圖作好一件事而已(目標是成爲POSIX的事件庫),這是最高效的方法。libevent則嘗試給你全套解決方案(事件庫,非阻塞IO庫,http庫,DNS客戶端)libev 徹底是單線程的,沒有DNS解析。
libev解決了epoll, kqueuq等API不一樣的問題。保證使用livev的程序能夠在大多數 *nix 平臺上運行(對windows不太友好)。可是 libev 的缺點也是顯而易見,因爲基本只是封裝了 Event Library,用起來有諸多不便。好比 accept(3) 鏈接之後須要手動 setnonblocking 。從 socket 讀寫時須要檢測 EAGAIN 、EWOULDBLOCK 和 EINTER 。這也是大多數人認爲異步程序難寫的根本緣由。
libuv是Joyent給Node作的I/O Library。libuv 須要多線程庫支持,其在內部維護了一個線程池來 處理諸如getaddrinfo(3) 這樣的沒法異步的調用。同時,對windows用戶友好,Windows下用IOCP實現,官網http://docs.libuv.org/en/v1.x/
Boost.Asio類庫,其就是以Proactor這種設計模式來實現。
參見:Proactor(The Boost.Asio library is based on the Proactor pattern. This design note outlines the advantages and disadvantages of this approach.),
其設計文檔連接:http://asio.sourceforge.net/boost_asio_0_3_7/libs/asio/doc/design/index.html
http://stackoverflow.com/questions/11423426/how-does-libuv-compare-to-boost-asio
linux有兩種aio(異步機制),一是glibc提供的(bug不少,幾乎不可用),一是內核提供的(BSD/mac也提供)。固然,機制不等於編程框架。