NIO深入理解
- 零拷貝
-
在理解0拷貝之前 我們應(yīng)該先需要了解傳統(tǒng)IO的一個(gè)操作流程
1. 傳統(tǒng)的io操作:首先需要進(jìn)行一個(gè) read操作 這里會(huì)發(fā)生一次用戶空間切換到內(nèi)核空間 內(nèi)核會(huì)采用DMA(直接內(nèi)存訪問(wèn)的方式)從磁盤讀取數(shù)據(jù)到內(nèi)核緩沖區(qū)
2. 內(nèi)核緩沖區(qū)將數(shù)據(jù)拷貝到用戶空間 同時(shí)再次上下文切換到 用戶空間
3. wirte 操作 也會(huì)發(fā)生一次上下文切換到內(nèi)核空間 同時(shí)將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)
4. 內(nèi)核空間會(huì)將數(shù)據(jù)拷貝到 socket buffer 在由 socket buffer 寫入到協(xié)議引擎進(jìn)行數(shù)據(jù)的發(fā)送(這里有一個(gè)異步的過(guò)程 寫操作后 會(huì)切換到用戶空間)- 用戶發(fā)送 sendfile 命令 進(jìn)行一次上下文切換到內(nèi)核空間 并采用DMA方式將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)
- 這里內(nèi)核緩沖區(qū)的數(shù)據(jù)不會(huì)拷貝到 socket buffer而是將文件的描述信息放入如 內(nèi)存地址 以及文件長(zhǎng)度信息(異步 進(jìn)行以此上下文切換到用戶空間)
- 協(xié)議引擎會(huì)根據(jù) socketbuffer 中的文件描述信息 直接從內(nèi)核緩沖區(qū)將數(shù)據(jù)寫入 協(xié)議引擎發(fā)送出去
-
什么是Reactor模式
-
深入理解 Reactor模式對(duì)理解netty的設(shè)計(jì)模式有莫大的幫助担忧,所以在理解netty的設(shè)計(jì)模式時(shí)帖渠,我們需要理解Reactor他的一個(gè)設(shè)計(jì)理念:Reactor的五大角色構(gòu)成
- Handle (句柄或是描述符)
- 即操作系統(tǒng)中的句柄师痕,是操作系統(tǒng)對(duì)資源的一種抽象,可以是打開的文件暖眼、一個(gè)連接(Socket)、Timer等。在網(wǎng)絡(luò)編程中药磺,一般指Socket Handle趴泌,文件描述符(fd)舟舒。將這個(gè)Handle注冊(cè)到Synchronous Event Demultiplexer中,就可以它發(fā)生的事件嗜憔,如READ秃励、WRITE、CLOSE等事件吉捶;handle本身是事件產(chǎn)生的發(fā)源地
- Synchronous Event Demultiplexer
- 同步事件多路分用器夺鲜,本質(zhì)上是系統(tǒng)調(diào)用。比如linux中的select呐舔、poll币励、epoll等。它會(huì)一直阻塞直在handle上珊拼,直到有事件發(fā)生時(shí)才會(huì)返回 (在java nio領(lǐng)域中對(duì)應(yīng)的組件是selector食呻,對(duì)應(yīng)的阻塞方法就是 select方法)
- Initiation Dispatcher
- 初始分發(fā)器,它提供了注冊(cè)澎现、刪除與轉(zhuǎn)發(fā)event handler的方法仅胞。當(dāng)Synchronous Event Demultiplexer檢測(cè)到handle上有事件發(fā)生時(shí),便會(huì)通知initiation dispatcher調(diào)用特定的event handler的回調(diào)(handle_event())方法昔头。
- Event Handler
- 事件處理器饼问,定義事件處理的回調(diào)方法:handle_event(),以供InitiationDispatcher回調(diào)使用揭斧。(Netty相對(duì)于java nio在事件處理器上進(jìn)行了升級(jí)莱革,它為我們開發(fā)者提供了大量的回調(diào)方法,供我們?cè)谔囟ǖ氖录a(chǎn)生時(shí)實(shí)現(xiàn)相應(yīng)的回調(diào)方法進(jìn)行業(yè)務(wù)邏輯的處理)
- Concrete Event Handler
- 具體的事件處理器讹开,繼承自Event Handler盅视,在回調(diào)方法中會(huì)實(shí)現(xiàn)具體的業(yè)務(wù)邏輯
-
Reactor模式的流程
- 當(dāng)應(yīng)用向Initiation Dispatcher 注冊(cè)具體的事件處理器時(shí),應(yīng)用會(huì)標(biāo)識(shí)出該事件處理器希望Initiation Dispatcher在某個(gè)事件發(fā)生時(shí)向其通知的該事件旦万,該事件與handle關(guān)聯(lián)
- Initiation Dispatcher會(huì)要求每個(gè)事件處理器向其傳遞內(nèi)部handle闹击。該handle向操作系統(tǒng)標(biāo)識(shí)了事件處理器
- 當(dāng)所有的事件處理器注冊(cè)完畢后,應(yīng)用會(huì)調(diào)用handle_events方法來(lái)啟動(dòng)Initiation Dispatcher的事件循環(huán)成艘。這時(shí)赏半,Initiation Dispatcher會(huì)將每個(gè)注冊(cè)的事件管理器的handle合并起來(lái),并使用同步事件分離器等待這些事件的發(fā)生淆两。比如說(shuō):TCP協(xié)議層會(huì)使用select同步事件分離器操作來(lái)等待客戶端發(fā)送數(shù)據(jù)到達(dá)連接的socket handle上
- 當(dāng)與某個(gè)事件源對(duì)應(yīng)的Handle變?yōu)閞eady狀態(tài)時(shí)(比如:TCP socket變?yōu)榈却x狀態(tài)時(shí))断箫。同步事件分離器就會(huì)通知Initiation Dispatcher
- Initiation Dispatcher會(huì)觸發(fā)事件處理器的回調(diào)方法,從而響應(yīng)這個(gè)處于ready狀態(tài)的Handle秋冰。當(dāng)事件發(fā)生時(shí)仲义,Initiation Dispatcher會(huì)將被事件源激活的Handle作為【key】來(lái)尋找并分發(fā)恰當(dāng)?shù)氖录幚砥鞯幕卣{(diào)方法。
- Initiation Dispatcher會(huì)回調(diào)事件處理器的handle_events回調(diào)方法來(lái)執(zhí)行特定與應(yīng)用的功能(開發(fā)者自己編寫功能)從而響應(yīng)這個(gè)事件。所發(fā)生的事件類型可以作為該方法的參數(shù)并被該方法內(nèi)部使用來(lái)執(zhí)行額外的特定于服務(wù)的分離與分發(fā)埃撵。
-
Netty 核心的幾個(gè)概念
- 一個(gè)EventLoopGroup當(dāng)中包含一個(gè)或多個(gè)EventLoop
- 一個(gè)EventLoop在它的整個(gè)生命周期當(dāng)中都只會(huì)與唯一一個(gè)Thread進(jìn)行綁定
- 所有由EventLoop所處理的各種I/O事件都將在它所關(guān)聯(lián)的那個(gè)Thread上進(jìn)行處理
- 一個(gè)Channel在它的整個(gè)生命周期中只會(huì)注冊(cè)在一個(gè)EventLoop上
- 一個(gè)EventLoop在運(yùn)行過(guò)程當(dāng)中赵颅,會(huì)被分配到多個(gè)Channel
-
結(jié)論:
- 在Netty中,Channel的實(shí)現(xiàn)一定是線程安全的暂刘,基于此饺谬,我們可以存儲(chǔ)一個(gè)Channel的引用,并且在需要向遠(yuǎn)端發(fā)送數(shù)據(jù)時(shí)鸳惯,通過(guò)整個(gè)引用來(lái)調(diào)用Channel的相應(yīng)方法商蕴;即便當(dāng)時(shí)有很多線程都在使用它也不會(huì)出現(xiàn)線程問(wèn)題;而且消息一定會(huì)按照順序發(fā)送出去芝发。
- 我們?cè)跇I(yè)務(wù)開發(fā)中绪商,不要將長(zhǎng)時(shí)間執(zhí)行的耗時(shí)任務(wù)放入到EventLoop的執(zhí)行隊(duì)列中,因?yàn)樗鼘?huì)一直阻塞該線程所對(duì)應(yīng)的所有Channel上的其他執(zhí)行任務(wù)辅鲸,如果我們需要進(jìn)行阻塞調(diào)用或是耗時(shí)的操作格郁,那么我們就需要使用一個(gè)專門的EventExecutor(業(yè)務(wù)線程池)
- JDK所提供的Future只能通過(guò)手工的方式檢查執(zhí)行結(jié)果,而這個(gè)操作是會(huì)阻塞的独悴;Netty則對(duì)ChannelFuture進(jìn)行了增強(qiáng)例书,通過(guò)ChannelFutureListener以回調(diào)的方式獲取結(jié)果,去除了手工檢查的操作(觀察者模式);值得注意的是:ChannelFutureListener的operationComplete方法是由I/O線程執(zhí)行的刻炒,因此要注意的是不要在這里執(zhí)行耗時(shí)的操作决采,否則需要通過(guò)另外的線程或線程池來(lái)執(zhí)行。
- 在Netty中有兩種發(fā)送消息的方式坟奥,可以直接寫到Channel中树瞭,也可以寫到ChannelHandler所關(guān)聯(lián)的那個(gè)ChannelHandlerContext中。對(duì)于前一種方式來(lái)說(shuō)爱谁,消息會(huì)從ChannelPipline的末尾開始流動(dòng)晒喷;對(duì)于后一種方式來(lái)說(shuō),消息將從ChannelPipline中的下一個(gè)ChannelHandler開始流動(dòng)访敌。
- ChannelHandlerContext與ChannelHandler之間的關(guān)聯(lián)綁定關(guān)系是永遠(yuǎn)都不會(huì)發(fā)生改變的凉敲,因此對(duì)其進(jìn)行緩存是沒有任何問(wèn)題的。
- 對(duì)于Channel的同名方法來(lái)說(shuō)寺旺,ChannelHandlerContext的方法將會(huì)產(chǎn)生更短的事件流爷抓,所以我們應(yīng)該在可能的情況下利用這個(gè)特性來(lái)提升應(yīng)用的性能。
- 在實(shí)際的開發(fā)中我們經(jīng)常會(huì)遇到一個(gè)服務(wù)端可能會(huì)去要調(diào)用另外一個(gè)客戶端阻塑,這時(shí)這個(gè)服務(wù)端的角色就相當(dāng)于即作為服務(wù)端也作為客戶端蓝撇。這時(shí)我們需要注意在我們作為客戶端時(shí)我們應(yīng)該將對(duì)服務(wù)端和客戶端的channel綁定在同一個(gè)eventLoop上;
-
Netty 提供的三種緩沖區(qū)類型
- heap buffer 堆緩沖區(qū)
- 優(yōu)點(diǎn):由于數(shù)據(jù)是存儲(chǔ)在JVM的堆中叮姑,因此可以快速的創(chuàng)建于快速的釋放唉地,并且他提供了直接訪問(wèn)內(nèi)部字節(jié)數(shù)組的方法
- 缺點(diǎn):每次的讀寫操作,都需要先將數(shù)據(jù)復(fù)制到直接緩沖區(qū)中在進(jìn)行網(wǎng)絡(luò)傳輸
- direct buffer 直接緩沖區(qū)(在堆之外直接分配內(nèi)存空間传透,直接緩沖區(qū)不會(huì)占用堆的容量空間耘沼,因?yàn)樗怯刹僮飨到y(tǒng)在本地內(nèi)存進(jìn)行的數(shù)據(jù)分配)
- 優(yōu)點(diǎn):在使用Sockte進(jìn)行數(shù)據(jù)傳遞時(shí),性能非常好朱盐,因?yàn)閿?shù)據(jù)直接位于操作系統(tǒng)的本地內(nèi)存中群嗤,所以不需要從JVM將數(shù)據(jù)復(fù)制到直接緩沖區(qū),性能很好兵琳。
- 缺點(diǎn):因?yàn)镈irect Buffer是直接在操作系統(tǒng)內(nèi)存中狂秘,所以內(nèi)存空間的分配與釋放要比堆空間更加復(fù)雜,而且速度慢一些躯肌。(Netty通過(guò)提供內(nèi)存池來(lái)解決這個(gè)問(wèn)題)
- 注意:直接緩沖區(qū)不支持通過(guò)字節(jié)數(shù)組的方式來(lái)直接訪問(wèn)數(shù)據(jù)(對(duì)于后端的業(yè)務(wù)消息的編解碼來(lái)說(shuō)者春,推薦使用HeapByteBuf;對(duì)于I/O通信線程在讀寫緩沖區(qū)時(shí)清女,推薦使用DirectByteBuf)
- composite buffer 復(fù)合緩沖區(qū)
- heap buffer 堆緩沖區(qū)
-
JDK的ByteBuffer和Netty的ByteBuf的差異比對(duì)
- Netty的ByteBuf采用讀寫索引分離的策略(readerIndex與writerIndex)钱烟,一個(gè)初始化(里面尚未有任何數(shù)據(jù))的ByteBuf的readerIndex與witerIndex值都為0
- 當(dāng)讀索引和寫索引處于同一個(gè)位置時(shí),如果我們繼續(xù)讀取嫡丙,那么就會(huì)拋出IndexOutofBoundsException
- 對(duì)于ByteBuf的任何讀寫操作都會(huì)分別單獨(dú)維護(hù)讀索引與寫索引拴袭。maxCapacity最大的容量默認(rèn)是 Integer.MAX_VALUE。
-
-
代碼實(shí)例:
public static void main(String[] args) {
// 創(chuàng)建一個(gè)長(zhǎng)度為10 的Bytebuf
ByteBuf byteBuf = Unpooled.buffer(10);
for (int i=0;i<10;i++) {
byteBuf.writeByte(i);
}
for ( int i=0; i<byteBuf.capacity();i++){
System.out.println(byteBuf.getByte(i));
}
}
public static void main(String[] args) {
ByteBuf byteBuf = Unpooled.copiedBuffer("hello你 world", Charset.forName("utf-8"));
if( byteBuf.hasArray()){
// hasArray 判斷這個(gè)byteBuf背后真正的支持是不是一個(gè)字節(jié)數(shù)組 如果是 表示這是一個(gè)堆上的緩沖
// 獲取byteBuf背后的真正數(shù)據(jù)載體
byte[] array = byteBuf.array();
System.out.println(new String(array,Charset.forName("utf-8")));
System.out.println(byteBuf);
System.out.println(byteBuf.arrayOffset());
System.out.println(byteBuf.readerIndex());
System.out.println(byteBuf.writerIndex());
System.out.println(byteBuf.capacity());
System.out.println(byteBuf.readableBytes());
while (byteBuf.isReadable()){
System.out.println((char) byteBuf.readByte());
}
}
}
public static void main(String[] args) {
// 創(chuàng)建一個(gè) CompositeBuffer 復(fù)合緩沖區(qū)
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
ByteBuf heapBuf = Unpooled.buffer(10);
ByteBuf directBuf = Unpooled.directBuffer(8);
// 將堆緩沖 和直接緩沖添加到復(fù)合緩沖區(qū)中
compositeByteBuf.addComponents(heapBuf,directBuf);
//compositeByteBuf.removeComponent(0);
Iterator<ByteBuf> iterator = compositeByteBuf.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
compositeByteBuf.forEach(System.out::println);
}