簡介: I/O技術在系統設計、性能優化、中間件研發中的使用愈來愈重要,學習和掌握I/O相關技術已經不只是一個Java攻城獅的加分技能,而是一個必備技能。本文將帶你瞭解BIO/NIO/AIO的發展歷程及實現原理,並介紹當前流行框架Netty的基本原理。redis
BIO是同步阻塞模型,一個客戶端鏈接對應一個處理線程。在BIO中,accept和read方法都是阻塞操做,若是沒有鏈接請求,accept方法阻塞;若是無數據可讀取,read方法阻塞。數組
NIO是同步非阻塞模型,服務端的一個線程能夠處理多個請求,客戶端發送的鏈接請求註冊在多路複用器Selector上,服務端線程經過輪詢多路複用器查看是否有IO請求,有則進行處理。性能優化
NIO的三大核心組件:服務器
Buffer:用於存儲數據,底層基於數組實現,針對8種基本類型提供了對應的緩衝區類。網絡
Channel:用於進行數據傳輸,面向緩衝區進行操做,支持雙向傳輸,數據能夠從Channel讀取到Buffer中,也能夠從Buffer寫到Channel中。多線程
Selector:選擇器,當向一個Selector中註冊Channel後,Selector 內部的機制就能夠自動不斷地查詢(Select)這些註冊的Channel是否有已就緒的 I/O 事件(例如可讀,可寫,網絡鏈接完成等),這樣程序就能夠很簡單地使用一個線程高效地管理多個Channel,也能夠說管理多個網絡鏈接,所以,Selector也被稱爲多路複用器。當某個Channel上面發生了讀或者寫事件,這個Channel就處於就緒狀態,會被Selector監聽到,而後經過SelectionKeys能夠獲取就緒Channel的集合,進行後續的I/O操做。併發
Epoll是Linux下多路複用IO接口select/poll的加強版本,它能顯著提升程序在大量併發鏈接中只有少許活躍的狀況下的系統CPU利用率,獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就好了。框架
AIO是異步非阻塞模型,通常用於鏈接數較多且鏈接時間較長的應用,在讀寫事件完成後由回調服務去通知程序啓動線程進行處理。與NIO不一樣,當進行讀寫操做時,只需直接調用read或write方法便可。這兩種方法均爲異步的,對於讀操做而言,當有流可讀取時,操做系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操做而言,當操做系統將write方法傳遞的流寫入完畢時,操做系統主動通知應用程序。能夠理解爲,read/write方法都是異步的,完成後會主動調用回調函數。異步
對於傳統的I/O通訊方式來講,客戶端鏈接到服務端,服務端接收客戶端請求並響應的流程爲:讀取 -> 解碼 -> 應用處理 -> 編碼 -> 發送結果。服務端爲每個客戶端鏈接新建一個線程,創建通道,從而處理後續的請求,也就是BIO的方式。函數
這種方式在客戶端數量不斷增長的狀況下,對於鏈接和請求的響應會急劇降低,而且佔用太多線程浪費資源,線程數量也不是沒有上限的,會遇到各類瓶頸。雖然可使用線程池進行優化,可是依然有諸多問題,好比在線程池中全部線程都在處理請求時,沒法響應其餘的客戶端鏈接,每一個客戶端依舊須要專門的服務端線程來服務,即便此時客戶端無請求,也處於阻塞狀態沒法釋放。基於此,提出了基於事件驅動的Reactor模型。
Reactor模式是基於事件驅動開發的,服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor模式也叫Dispatcher模式,即I/O多路複用統一監聽事件,收到事件後分發(Dispatch給某進程),這是編寫高性能網絡服務器的必備技術之一。
Reactor模式以NIO爲底層支持,核心組成部分包括Reactor和Handler:
根據Reactor的數量和Handler線程數量,能夠將Reactor分爲三種模型:
單線程模型
Reactor內部經過Selector監控鏈接事件,收到事件後經過dispatch進行分發,若是是鏈接創建的事件,則由Acceptor處理,Acceptor經過accept接受鏈接,並建立一個Handler來處理鏈接後續的各類事件,若是是讀寫事件,直接調用鏈接對應的Handler來處理。
Handler完成read -> (decode -> compute -> encode) ->send的業務流程。
這種模型好處是簡單,壞處卻很明顯,當某個Handler阻塞時,會致使其餘客戶端的handler和accpetor都得不到執行,沒法作到高性能,只適用於業務處理很是快速的場景,如redis讀寫操做。
多線程模型
主線程中,Reactor對象經過Selector監控鏈接事件,收到事件後經過dispatch進行分發,若是是鏈接創建事件,則由Acceptor處理,Acceptor經過accept接收鏈接,並建立一個Handler來處理後續事件,而Handler只負責響應事件,不進行業務操做,也就是隻進行read讀取數據和write寫出數據,業務處理交給一個線程池進行處理。
線程池分配一個線程完成真正的業務處理,而後將響應結果交給主進程的Handler處理,Handler將結果send給client。
單Reactor承擔全部事件的監聽和響應,而當咱們的服務端遇到大量的客戶端同時進行鏈接,或者在請求鏈接時執行一些耗時操做,好比身份認證,權限檢查等,這種瞬時的高併發就容易成爲性能瓶頸。
主從多線程模型
存在多個Reactor,每一個Reactor都有本身的Selector選擇器,線程和dispatch。
主線程中的mainReactor經過本身的Selector監控鏈接創建事件,收到事件後經過Accpetor接收,將新的鏈接分配給某個子線程。
子線程中的subReactor將mainReactor分配的鏈接加入鏈接隊列中經過本身的Selector進行監聽,並建立一個Handler用於處理後續事件。
Handler完成read -> 業務處理 -> send的完整業務流程。
關於Reactor,最權威的資料應該是Doug Lea大神的Scalable IO in Java,有興趣的同窗能夠看看。
Netty線程模型就是Reactor模式的一個實現,以下圖所示:
Netty抽象了兩組線程池BossGroup和WorkerGroup,其類型都是NioEventLoopGroup,BossGroup用來接受客戶端發來的鏈接,WorkerGroup則負責對完成TCP三次握手的鏈接進行處理。
NioEventLoopGroup裏面包含了多個NioEventLoop,管理NioEventLoop的生命週期。每一個NioEventLoop中包含了一個NIO Selector、一個隊列、一個線程;其中線程用來作輪詢註冊到Selector上的Channel的讀寫事件和對投遞到隊列裏面的事件進行處理。
Boss NioEventLoop線程的執行步驟:
Worker NioEventLoop線程的執行步驟:
Worker NIOEventLoop處理NioSocketChannel業務時,使用了pipeline (管道),管道中維護了handler處理器鏈表,用來處理channel中的數據。
Netty將Channel的數據管道抽象爲ChannelPipeline,消息在ChannelPipline中流動和傳遞。ChannelPipeline持有I/O事件攔截器ChannelHandler的雙向鏈表,由ChannelHandler對I/O事件進行攔截和處理,能夠方便的新增和刪除ChannelHandler來實現不一樣的業務邏輯定製,不須要對已有的ChannelHandler進行修改,可以實現對修改封閉和對擴展的支持。
ChannelPipeline是一系列的ChannelHandler實例,流經一個Channel的入站和出站事件能夠被ChannelPipeline 攔截。每當一個新的Channel被建立了,都會創建一個新的ChannelPipeline並綁定到該Channel上,這個關聯是永久性的;Channel既不能附上另外一個ChannelPipeline也不能分離當前這個。這些都由Netty負責完成,而無需開發人員的特別處理。
根據起源,一個事件將由ChannelInboundHandler或ChannelOutboundHandler處理,ChannelHandlerContext實現轉發或傳播到下一個ChannelHandler。一個ChannelHandler處理程序能夠通知ChannelPipeline中的下一個ChannelHandler執行。Read事件(入站事件)和write事件(出站事件)使用相同的pipeline,入站事件會從鏈表head 日後傳遞到最後一個入站的handler,出站事件會從鏈表tail往前傳遞到最前一個出站的 handler,兩種類型的 handler 互不干擾。
ChannelInboundHandler回調方法:
ChannelOutboundHandler回調方法:
寫操做:經過NioSocketChannel的write方法向鏈接裏面寫入數據時候是非阻塞的,立刻會返回,即便調用寫入的線程是咱們的業務線程。Netty經過在ChannelPipeline中判斷調用NioSocketChannel的write的調用線程是否是其對應的NioEventLoop中的線程,若是發現不是則會把寫入請求封裝爲WriteTask投遞到其對應的NioEventLoop中的隊列裏面,而後等其對應的NioEventLoop中的線程輪詢讀寫事件時候,將其從隊列裏面取出來執行。
讀操做:當從NioSocketChannel中讀取數據時候,並非須要業務線程阻塞等待,而是等NioEventLoop中的IO輪詢線程發現Selector上有數據就緒時,經過事件通知方式來通知業務數據已就緒,能夠來讀取並處理了。
每一個NioSocketChannel對應的讀寫事件都是在其對應的NioEventLoop管理的單線程內執行,對同一個NioSocketChannel不存在併發讀寫,因此無需加鎖處理。
使用Netty框架進行網絡通訊時,當咱們發起I/O請求後會立刻返回,而不會阻塞咱們的業務調用線程;若是想要獲取請求的響應結果,也不須要業務調用線程使用阻塞的方式來等待,而是當響應結果出來的時候,使用I/O線程異步通知業務的方式,因此在整個請求 -> 響應過程當中業務線程不會因爲阻塞等待而不能幹其餘事情。