新聞中心
I/O技術在系統(tǒng)設計、性能優(yōu)化、中間件研發(fā)中的使用越來越重要,學習和掌握I/O相關技術已經(jīng)不僅是一個Java攻城獅的加分技能,而是一個必備技能。本文將帶你了解BIO/NIO/AIO的發(fā)展歷程及實現(xiàn)原理,并介紹當前流行框架Netty的基本原理。

成都創(chuàng)新互聯(lián)主要從事成都做網(wǎng)站、網(wǎng)站制作、網(wǎng)頁設計、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務。立足成都服務章丘,十載網(wǎng)站建設經(jīng)驗,價格優(yōu)惠、服務專業(yè),歡迎來電咨詢建站服務:13518219792
文末福利:孤盡獨家解讀《Java開發(fā)手冊(嵩山版)》。
一 Java I/O模型
1 BIO(Blocking IO)
BIO是同步阻塞模型,一個客戶端連接對應一個處理線程。在BIO中,accept和read方法都是阻塞操作,如果沒有連接請求,accept方法阻塞;如果無數(shù)據(jù)可讀取,read方法阻塞。
2 NIO(Non Blocking IO)
NIO是同步非阻塞模型,服務端的一個線程可以處理多個請求,客戶端發(fā)送的連接請求注冊在多路復用器Selector上,服務端線程通過輪詢多路復用器查看是否有IO請求,有則進行處理。
NIO的三大核心組件:
Buffer:用于存儲數(shù)據(jù),底層基于數(shù)組實現(xiàn),針對8種基本類型提供了對應的緩沖區(qū)類。
Channel:用于進行數(shù)據(jù)傳輸,面向緩沖區(qū)進行操作,支持雙向傳輸,數(shù)據(jù)可以從Channel讀取到Buffer中,也可以從Buffer寫到Channel中。
Selector:選擇器,當向一個Selector中注冊Channel后,Selector 內部的機制就可以自動不斷地查詢(Select)這些注冊的Channel是否有已就緒的 I/O 事件(例如可讀,可寫,網(wǎng)絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個Channel,也可以說管理多個網(wǎng)絡連接,因此,Selector也被稱為多路復用器。當某個Channel上面發(fā)生了讀或者寫事件,這個Channel就處于就緒狀態(tài),會被Selector監(jiān)聽到,然后通過SelectionKeys可以獲取就緒Channel的集合,進行后續(xù)的I/O操作。
Epoll是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率,獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。
3 AIO(NIO 2.0)
AIO是異步非阻塞模型,一般用于連接數(shù)較多且連接時間較長的應用,在讀寫事件完成后由回調服務去通知程序啟動線程進行處理。與NIO不同,當進行讀寫操作時,只需直接調用read或write方法即可。這兩種方法均為異步的,對于讀操作而言,當有流可讀取時,操作系統(tǒng)會將可讀的流傳入read方法的緩沖區(qū),并通知應用程序;對于寫操作而言,當操作系統(tǒng)將write方法傳遞的流寫入完畢時,操作系統(tǒng)主動通知應用程序??梢岳斫鉃?,read/write方法都是異步的,完成后會主動調用回調函數(shù)。
二 I/O模型演化
1 傳統(tǒng)I/O模型
對于傳統(tǒng)的I/O通信方式來說,客戶端連接到服務端,服務端接收客戶端請求并響應的流程為:讀取 -> 解碼 -> 應用處理 -> 編碼 -> 發(fā)送結果。服務端為每一個客戶端連接新建一個線程,建立通道,從而處理后續(xù)的請求,也就是BIO的方式。
這種方式在客戶端數(shù)量不斷增加的情況下,對于連接和請求的響應會急劇下降,并且占用太多線程浪費資源,線程數(shù)量也不是沒有上限的,會遇到各種瓶頸。雖然可以使用線程池進行優(yōu)化,但是依然有諸多問題,比如在線程池中所有線程都在處理請求時,無法響應其他的客戶端連接,每個客戶端依舊需要專門的服務端線程來服務,即使此時客戶端無請求,也處于阻塞狀態(tài)無法釋放。基于此,提出了基于事件驅動的Reactor模型。
2 Reactor模型
Reactor模式是基于事件驅動開發(fā)的,服務端程序處理傳入多路請求,并將它們同步分派給請求對應的處理線程,Reactor模式也叫Dispatcher模式,即I/O多路復用統(tǒng)一監(jiān)聽事件,收到事件后分發(fā)(Dispatch給某進程),這是編寫高性能網(wǎng)絡服務器的必備技術之一。
Reactor模式以NIO為底層支持,核心組成部分包括Reactor和Handler:
- Reactor:Reactor在一個單獨的線程中運行,負責監(jiān)聽和分發(fā)事件,分發(fā)給適當?shù)奶幚沓绦騺韺/O事件做出反應。它就像公司的電話接線員,它接聽來自客戶的電話并將線路轉移到適當?shù)穆?lián)系人。
- Handlers:處理程序執(zhí)行I/O事件要完成的實際事件,Reactor通過調度適當?shù)奶幚沓绦騺眄憫?I/O 事件,處理程序執(zhí)行非阻塞操作。類似于客戶想要與之交談的公司中的實際員工。
根據(jù)Reactor的數(shù)量和Handler線程數(shù)量,可以將Reactor分為三種模型:
- 單線程模型 (單Reactor單線程)
- 多線程模型 (單Reactor多線程)
- 主從多線程模型 (多Reactor多線程)
單線程模型
Reactor內部通過Selector監(jiān)控連接事件,收到事件后通過dispatch進行分發(fā),如果是連接建立的事件,則由Acceptor處理,Acceptor通過accept接受連接,并創(chuàng)建一個Handler來處理連接后續(xù)的各種事件,如果是讀寫事件,直接調用連接對應的Handler來處理。
Handler完成read -> (decode -> compute -> encode) ->send的業(yè)務流程。
這種模型好處是簡單,壞處卻很明顯,當某個Handler阻塞時,會導致其他客戶端的handler和accpetor都得不到執(zhí)行,無法做到高性能,只適用于業(yè)務處理非常快速的場景,如redis讀寫操作。
多線程模型
主線程中,Reactor對象通過Selector監(jiān)控連接事件,收到事件后通過dispatch進行分發(fā),如果是連接建立事件,則由Acceptor處理,Acceptor通過accept接收連接,并創(chuàng)建一個Handler來處理后續(xù)事件,而Handler只負責響應事件,不進行業(yè)務操作,也就是只進行read讀取數(shù)據(jù)和write寫出數(shù)據(jù),業(yè)務處理交給一個線程池進行處理。
線程池分配一個線程完成真正的業(yè)務處理,然后將響應結果交給主進程的Handler處理,Handler將結果send給client。
單Reactor承擔所有事件的監(jiān)聽和響應,而當我們的服務端遇到大量的客戶端同時進行連接,或者在請求連接時執(zhí)行一些耗時操作,比如身份認證,權限檢查等,這種瞬時的高并發(fā)就容易成為性能瓶頸。
主從多線程模型
存在多個Reactor,每個Reactor都有自己的Selector選擇器,線程和dispatch。
主線程中的mainReactor通過自己的Selector監(jiān)控連接建立事件,收到事件后通過Accpetor接收,將新的連接分配給某個子線程。
子線程中的subReactor將mainReactor分配的連接加入連接隊列中通過自己的Selector進行監(jiān)聽,并創(chuàng)建一個Handler用于處理后續(xù)事件。
Handler完成read -> 業(yè)務處理 -> send的完整業(yè)務流程。
關于Reactor,最權威的資料應該是Doug Lea大神的Scalable IO in Java,有興趣的同學可以看看。
三 Netty線程模型
Netty線程模型就是Reactor模式的一個實現(xiàn),如下圖所示:
1 線程組
Netty抽象了兩組線程池BossGroup和WorkerGroup,其類型都是NioEventLoopGroup,BossGroup用來接受客戶端發(fā)來的連接,WorkerGroup則負責對完成TCP三次握手的連接進行處理。
NioEventLoopGroup里面包含了多個NioEventLoop,管理NioEventLoop的生命周期。每個NioEventLoop中包含了一個NIO Selector、一個隊列、一個線程;其中線程用來做輪詢注冊到Selector上的Channel的讀寫事件和對投遞到隊列里面的事件進行處理。
Boss NioEventLoop線程的執(zhí)行步驟:
- 處理accept事件, 與client建立連接, 生成NioSocketChannel。
- 將NioSocketChannel注冊到某個worker NIOEventLoop上的selector。
- 處理任務隊列的任務, 即runAllTasks。
Worker NioEventLoop線程的執(zhí)行步驟:
- 輪詢注冊到自己Selector上的所有NioSocketChannel的read和write事件。
- 處理read和write事件,在對應NioSocketChannel處理業(yè)務。
- runAllTasks處理任務隊列TaskQueue的任務,一些耗時的業(yè)務處理可以放入TaskQueue中慢慢處理,這樣不影響數(shù)據(jù)在pipeline中的流動處理。
Worker NIOEventLoop處理NioSocketChannel業(yè)務時,使用了pipeline (管道),管道中維護了handler處理器鏈表,用來處理channel中的數(shù)據(jù)。
2 ChannelPipeline
Netty將Channel的數(shù)據(jù)管道抽象為ChannelPipeline,消息在ChannelPipline中流動和傳遞。ChannelPipeline持有I/O事件攔截器ChannelHandler的雙向鏈表,由ChannelHandler對I/O事件進行攔截和處理,可以方便的新增和刪除ChannelHandler來實現(xiàn)不同的業(yè)務邏輯定制,不需要對已有的ChannelHandler進行修改,能夠實現(xiàn)對修改封閉和對擴展的支持。
ChannelPipeline是一系列的ChannelHandler實例,流經(jīng)一個Channel的入站和出站事件可以被ChannelPipeline 攔截。每當一個新的Channel被創(chuàng)建了,都會建立一個新的ChannelPipeline并綁定到該Channel上,這個關聯(lián)是永久性的;Channel既不能附上另一個ChannelPipeline也不能分離當前這個。這些都由Netty負責完成,而無需開發(fā)人員的特別處理。
根據(jù)起源,一個事件將由ChannelInboundHandler或ChannelOutboundHandler處理,ChannelHandlerContext實現(xiàn)轉發(fā)或傳播到下一個ChannelHandler。一個ChannelHandler處理程序可以通知ChannelPipeline中的下一個ChannelHandler執(zhí)行。Read事件(入站事件)和write事件(出站事件)使用相同的pipeline,入站事件會從鏈表head 往后傳遞到最后一個入站的handler,出站事件會從鏈表tail往前傳遞到最前一個出站的 handler,兩種類型的 handler 互不干擾。
ChannelInboundHandler回調方法:
ChannelOutboundHandler回調方法:
3 異步非阻塞
寫操作:通過NioSocketChannel的write方法向連接里面寫入數(shù)據(jù)時候是非阻塞的,馬上會返回,即使調用寫入的線程是我們的業(yè)務線程。Netty通過在ChannelPipeline中判斷調用NioSocketChannel的write的調用線程是不是其對應的NioEventLoop中的線程,如果發(fā)現(xiàn)不是則會把寫入請求封裝為WriteTask投遞到其對應的NioEventLoop中的隊列里面,然后等其對應的NioEventLoop中的線程輪詢讀寫事件時候,將其從隊列里面取出來執(zhí)行。
讀操作:當從NioSocketChannel中讀取數(shù)據(jù)時候,并不是需要業(yè)務線程阻塞等待,而是等NioEventLoop中的IO輪詢線程發(fā)現(xiàn)Selector上有數(shù)據(jù)就緒時,通過事件通知方式來通知業(yè)務數(shù)據(jù)已就緒,可以來讀取并處理了。
每個NioSocketChannel對應的讀寫事件都是在其對應的NioEventLoop管理的單線程內執(zhí)行,對同一個NioSocketChannel不存在并發(fā)讀寫,所以無需加鎖處理。
使用Netty框架進行網(wǎng)絡通信時,當我們發(fā)起I/O請求后會馬上返回,而不會阻塞我們的業(yè)務調用線程;如果想要獲取請求的響應結果,也不需要業(yè)務調用線程使用阻塞的方式來等待,而是當響應結果出來的時候,使用I/O線程異步通知業(yè)務的方式,所以在整個請求 -> 響應過程中業(yè)務線程不會由于阻塞等待而不能干其他事情。
當前標題:Java開發(fā)必備!I/O與Netty原理精講
網(wǎng)站路徑:http://m.5511xx.com/article/dhoegee.html


咨詢
建站咨詢
