3萬(wàn)字加50張圖,帶你深度解析 Netty 架構(gòu)與原理(上)
接下來(lái)我們會(huì)學(xué)習(xí)一個(gè) Netty 系列教程,Netty 系列由「架構(gòu)與原理」,「源碼」,「架構(gòu)」三部分組成,今天我們先來(lái)看看第一部分:Netty 架構(gòu)與原理初探,大綱如下:
前言
1. Netty 基礎(chǔ)
1.4.1. 緩沖區(qū)(Buffer)
1.4.2. 通道(Channel)
1.4.3. 選擇器(Selector)
1.1. Netty 是什么
1.2. Netty 的應(yīng)用場(chǎng)景
1.3. Java 中的網(wǎng)絡(luò) IO 模型
1.4. Java NIO API 簡(jiǎn)單回顧
1.5. 零拷貝技術(shù)
2. Netty 的架構(gòu)與原理
2.2.1. 單 Reactor 單線程模式
2.2.2. 單 Reactor 多線程模式
2.2.3. 主從 Reactor 多線程模式
2.1. 為什么要制造 Netty
2.2. 幾種 Reactor 線程模式
2.3. Netty 的模樣
2.4. 基于 Netty 的 TCP Server/Client 案例
2.5. Netty 的 Handler 組件
2.6. Netty 的 Pipeline 組件
2.7. Netty 的 EventLoopGroup 組件
2.8. Netty 的 TaskQueue
2.9. Netty 的 Future 和 Promise
3. 結(jié)束語(yǔ)
前言
讀者在閱讀本文前最好有 Java 的 IO 編程經(jīng)驗(yàn)(知道 Java 的各種 IO 流),以及 Java 網(wǎng)絡(luò)編程經(jīng)驗(yàn)(用 ServerSocket 和 Socket 寫過(guò) demo),并對(duì) Java NIO 有基本的認(rèn)識(shí)(至少知道 Channel、Buffer、Selector 中的核心屬性和方法,以及三者如何配合使用的),以及 JUC 編程經(jīng)驗(yàn)(至少知道其中的 Future 異步處理機(jī)制),沒(méi)有也沒(méi)關(guān)系,文中多數(shù)會(huì)介紹,不影響整體的理解。
文中對(duì)于 Reactor 的講解使用了幾張來(lái)自網(wǎng)絡(luò)上的深灰色背景的示意圖,但未找到原始出處,文中已標(biāo)注“圖片來(lái)源于網(wǎng)絡(luò)”。
Netty 的設(shè)計(jì)復(fù)雜,接口和類體系龐大,因此我會(huì)從不同的層次對(duì)有些 Netty 中的重要組件反復(fù)描述,以幫助讀者理解。
1. Netty 基礎(chǔ)
基礎(chǔ)好的同學(xué),如果已經(jīng)掌握了 Java NIO 并對(duì) IO 多路復(fù)用的概念有一定的認(rèn)知,可以跳過(guò)本章。
1.1. Netty 是什么
1)Netty 是 JBoss 開(kāi)源項(xiàng)目,是異步的、基于事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用框架,它以高性能、高并發(fā)著稱。所謂基于事件驅(qū)動(dòng),說(shuō)得簡(jiǎn)單點(diǎn)就是 Netty 會(huì)根據(jù)客戶端事件(連接、讀、寫等)做出響應(yīng),關(guān)于這點(diǎn),隨著文章的論述的展開(kāi),讀者自然會(huì)明白。
2)Netty 主要用于開(kāi)發(fā)基于 TCP 協(xié)議的網(wǎng)絡(luò) IO 程序(TCP/IP 是網(wǎng)絡(luò)通信的基石,當(dāng)然也是 Netty 的基石,Netty 并沒(méi)有去改變這些底層的網(wǎng)絡(luò)基礎(chǔ)設(shè)施,而是在這之上提供更高層的網(wǎng)絡(luò)基礎(chǔ)設(shè)施),例如高性能服務(wù)器段/客戶端、P2P 程序等。
3)Netty 是基于 Java NIO 構(gòu)建出來(lái)的,Java NIO 又是基于 Linux 提供的高性能 IO 接口/系統(tǒng)調(diào)用構(gòu)建出來(lái)的。關(guān)于 Netty 在網(wǎng)絡(luò)中的地位,下圖可以很好地表達(dá)出來(lái):

1.2. Netty 的應(yīng)用場(chǎng)景
在互聯(lián)網(wǎng)領(lǐng)域,Netty 作為異步高并發(fā)的網(wǎng)絡(luò)組件,常常用于構(gòu)建高性能 RPC 框架,以提升分布式服務(wù)群之間調(diào)用或者數(shù)據(jù)傳輸?shù)牟l(fā)度和速度。例如 Dubbo 的網(wǎng)絡(luò)層就可以(但并非一定)使用 Netty。
一些大數(shù)據(jù)基礎(chǔ)設(shè)施,比如 Hadoop,在處理海量數(shù)據(jù)的時(shí)候,數(shù)據(jù)在多個(gè)計(jì)算節(jié)點(diǎn)之中傳輸,為了提高傳輸性能,也采用 Netty 構(gòu)建性能更高的網(wǎng)絡(luò) IO 層。
在游戲行業(yè),Netty 被用于構(gòu)建高性能的游戲交互服務(wù)器,Netty 提供了 TCP/UDP、HTTP 協(xié)議棧,方便開(kāi)發(fā)者基于 Netty 進(jìn)行私有協(xié)議的開(kāi)發(fā)。
……
Netty 作為成熟的高性能異步通信框架,無(wú)論是應(yīng)用在互聯(lián)網(wǎng)分布式應(yīng)用開(kāi)發(fā)中,還是在大數(shù)據(jù)基礎(chǔ)設(shè)施構(gòu)建中,亦或是用于實(shí)現(xiàn)應(yīng)用層基于公私協(xié)議的服務(wù)器等等,都有出色的表現(xiàn),是一個(gè)極好的輪子。
1.3. Java 中的網(wǎng)絡(luò) IO 模型
Java 中的網(wǎng)絡(luò) IO 模型有三種:BIO、NIO、AIO。
1)BIO:同步的、阻塞式 IO。在這種模型中,服務(wù)器上一個(gè)線程處理一次連接,即客戶端每發(fā)起一個(gè)請(qǐng)求,服務(wù)端都要開(kāi)啟一個(gè)線程專門處理該請(qǐng)求。這種模型對(duì)線程量的耗費(fèi)極大,且線程利用率低,難以承受請(qǐng)求的高并發(fā)。BIO 雖然可以使用線程池+等待隊(duì)列進(jìn)行優(yōu)化,避免使用過(guò)多的線程,但是依然無(wú)法解決線程利用率低的問(wèn)題。

使用 BIO 構(gòu)建 C/S 系統(tǒng)的 Java 編程組件是 ServerSocket 和 Socket。服務(wù)端示例代碼為:
public?static?void?main(String[]?args)?throws?IOException?{
????ExecutorService?threadPool?=?Executors.newCachedThreadPool();
????ServerSocket?serverSocket?=?new?ServerSocket(8080);
????while?(true)?{
????????Socket?socket?=?serverSocket.accept();
????????threadPool.execute(()?->?{
????????????handler(socket);
????????});
????}
}
/**
?*?處理客戶端請(qǐng)求
?*/
private?static?void?handler(Socket?socket)?throws?IOException?{
????byte[]?bytes?=?new?byte[1024];
????InputStream?inputStream?=?socket.getInputStream();
????socket.close();
????while?(true)?{
????????int?read?=?inputStream.read(bytes);
????????if?(read?!=?-1)?{
????????????System.out.println("msg?from?client:?"?+?new?String(bytes,?0,?read));
????????}?else?{
????????????break;
????????}
????}
}
2)NIO:同步的、非阻塞式 IO。在這種模型中,服務(wù)器上一個(gè)線程處理多個(gè)連接,即多個(gè)客戶端請(qǐng)求都會(huì)被注冊(cè)到多路復(fù)用器(后文要講的 Selector)上,多路復(fù)用器會(huì)輪訓(xùn)這些連接,輪訓(xùn)到連接上有 IO 活動(dòng)就進(jìn)行處理。NIO 降低了線程的需求量,提高了線程的利用率。Netty 就是基于 NIO 的(這里有一個(gè)問(wèn)題:前文大力宣揚(yáng) Netty 是一個(gè)異步高性能網(wǎng)絡(luò)應(yīng)用框架,為何這里又說(shuō) Netty 是基于同步的 NIO 的?請(qǐng)讀者跟著文章的描述找尋答案)。

NIO 是面向緩沖區(qū)編程的,從緩沖區(qū)讀取數(shù)據(jù)的時(shí)候游標(biāo)在緩沖區(qū)中是可以前后移動(dòng)的,這就增加了數(shù)據(jù)處理的靈活性。這和面向流的 BIO 只能順序讀取流中數(shù)據(jù)有很大的不同。
Java NIO 的非阻塞模式,使得一個(gè)線程從某個(gè)通道讀取數(shù)據(jù)的時(shí)候,若當(dāng)前有可用數(shù)據(jù),則該線程進(jìn)行處理,若當(dāng)前無(wú)可用數(shù)據(jù),則該線程不會(huì)保持阻塞等待狀態(tài),而是可以去處理其他工作(比如處理其他通道的讀寫);同樣,一個(gè)線程向某個(gè)通道寫入數(shù)據(jù)的時(shí)候,一旦開(kāi)始寫入,該線程無(wú)需等待寫完即可去處理其他工作(比如處理其他通道的讀寫)。這種特性使得一個(gè)線程能夠處理多個(gè)客戶端請(qǐng)求,而不是像 BIO 那樣,一個(gè)線程只能處理一個(gè)請(qǐng)求。
使用 NIO 構(gòu)建 C/S 系統(tǒng)的 Java 編程組件是 Channel、Buffer、Selector。服務(wù)端示例代碼為:
public?static?void?main(String[]?args)?throws?IOException?{
????ServerSocketChannel?serverSocketChannel?=?ServerSocketChannel.open();
????Selector?selector?=?Selector.open();
????//?綁定端口
????serverSocketChannel.socket().bind(new?InetSocketAddress(8080));
????//?設(shè)置?serverSocketChannel?為非阻塞模式
????serverSocketChannel.configureBlocking(false);
????//?注冊(cè)?serverSocketChannel?到?selector,關(guān)注?OP_ACCEPT?事件
????serverSocketChannel.register(selector,?SelectionKey.OP_ACCEPT);
????while?(true)?{
????????//?沒(méi)有事件發(fā)生
????????if?(selector.select(1000)?==?0)?{
????????????continue;
????????}
????????//?有事件發(fā)生,找到發(fā)生事件的?Channel?對(duì)應(yīng)的?SelectionKey?的集合
????????Set?selectionKeys?=?selector.selectedKeys();
????????Iterator?iterator?=?selectionKeys.iterator();
????????while?(iterator.hasNext())?{
????????????SelectionKey?selectionKey?=?iterator.next();
????????????//?發(fā)生?OP_ACCEPT?事件,處理連接請(qǐng)求
????????????if?(selectionKey.isAcceptable())?{
????????????????SocketChannel?socketChannel?=?serverSocketChannel.accept();
????????????????//?將?socketChannel?也注冊(cè)到?selector,關(guān)注?OP_READ
????????????????//?事件,并給?socketChannel?關(guān)聯(lián)?Buffer
????????????????socketChannel.register(selector,?SelectionKey.OP_READ,?ByteBuffer.allocate(1024));
????????????}
????????????//?發(fā)生?OP_READ?事件,讀客戶端數(shù)據(jù)
????????????if?(selectionKey.isReadable())?{
????????????????SocketChannel?channel?=?(SocketChannel)?selectionKey.channel();
????????????????ByteBuffer?buffer?=?(ByteBuffer)?selectionKey.attachment();
????????????????channel.read(buffer);
????????????????System.out.println("msg?form?client:?"?+?new?String(buffer.array()));
????????????}
????????????//?手動(dòng)從集合中移除當(dāng)前的?selectionKey,防止重復(fù)處理事件
????????????iterator.remove();
????????}
????}
}
3)AIO:異步非阻塞式 IO。在這種模型中,由操作系統(tǒng)完成與客戶端之間的 read/write,之后再由操作系統(tǒng)主動(dòng)通知服務(wù)器線程去處理后面的工作,在這個(gè)過(guò)程中服務(wù)器線程不必同步等待 read/write 完成。由于不同的操作系統(tǒng)對(duì) AIO 的支持程度不同,AIO 目前未得到廣泛應(yīng)用。因此本文對(duì) AIO 不做過(guò)多描述。
使用 Java NIO 構(gòu)建的 IO 程序,它的工作模式是:主動(dòng)輪訓(xùn) IO 事件,IO 事件發(fā)生后程序的線程主動(dòng)處理 IO 工作,這種模式也叫做 Reactor 模式。使用 Java AIO 構(gòu)建的 IO 程序,它的工作模式是:將 IO 事件的處理托管給操作系統(tǒng),操作系統(tǒng)完成 IO 工作之后會(huì)通知程序的線程去處理后面的工作,這種模式也叫做 Proactor 模式。
本節(jié)最后,討論一下網(wǎng)路 IO 中阻塞、非阻塞、異步、同步這幾個(gè)術(shù)語(yǔ)的含義和關(guān)系:
阻塞:如果線程調(diào)用 read/write 過(guò)程,但 read/write 過(guò)程沒(méi)有就緒或沒(méi)有完成,則調(diào)用 read/write 過(guò)程的線程會(huì)一直等待,這個(gè)過(guò)程叫做阻塞式讀寫。
非阻塞:如果線程調(diào)用 read/write 過(guò)程,但 read/write 過(guò)程沒(méi)有就緒或沒(méi)有完成,調(diào)用 read/write 過(guò)程的線程并不會(huì)一直等待,而是去處理其他工作,等到 read/write 過(guò)程就緒或完成后再回來(lái)處理,這個(gè)過(guò)程叫做阻塞式讀寫。
異步:read/write 過(guò)程托管給操作系統(tǒng)來(lái)完成,完成后操作系統(tǒng)會(huì)通知(通過(guò)回調(diào)或者事件)應(yīng)用網(wǎng)絡(luò) IO 程序(其中的線程)來(lái)進(jìn)行后續(xù)的處理。
同步:read/write 過(guò)程由網(wǎng)絡(luò) IO 程序(其中的線程)來(lái)完成。
基于以上含義,可以看出:異步 IO 一定是非阻塞 IO;同步 IO 既可以是阻塞 IO、也可以是非阻塞 IO。
1.4. Java NIO API 簡(jiǎn)單回顧
BIO 以流的方式處理數(shù)據(jù),而 NIO 以緩沖區(qū)(也被叫做塊)的方式處理數(shù)據(jù),塊 IO 效率比流 IO 效率高很多。BIO 基于字符流或者字節(jié)流進(jìn)行操作,而 NIO 基于 Channel 和 Buffer 進(jìn)行操作,數(shù)據(jù)總是從通道讀取到緩沖區(qū)或者從緩沖區(qū)寫入到通道。Selector 用于監(jiān)聽(tīng)多個(gè)通道上的事件(比如收到連接請(qǐng)求、數(shù)據(jù)達(dá)到等等),因此使用單個(gè)線程就可以監(jiān)聽(tīng)多個(gè)客戶端通道。如下圖所示:

關(guān)于上圖,再進(jìn)行幾點(diǎn)說(shuō)明:
一個(gè) Selector 對(duì)應(yīng)一個(gè)處理線程
一個(gè) Selector 上可以注冊(cè)多個(gè) Channel
每個(gè) Channel 都會(huì)對(duì)應(yīng)一個(gè) Buffer(有時(shí)候一個(gè) Channel 可以使用多個(gè) Buffer,這時(shí)候程序要進(jìn)行多個(gè) Buffer 的分散和聚集操作),Buffer 的本質(zhì)是一個(gè)內(nèi)存塊,底層是一個(gè)數(shù)組
Selector 會(huì)根據(jù)不同的事件在各個(gè) Channel 上切換
Buffer 是雙向的,既可以讀也可以寫,切換讀寫方向要調(diào)用 Buffer 的 flip()方法
同樣,Channel 也是雙向的,數(shù)據(jù)既可以流入也可以流出
1.4.1. 緩沖區(qū)(Buffer)
緩沖區(qū)(Buffer)本質(zhì)上是一個(gè)可讀可寫的內(nèi)存塊,可以理解成一個(gè)容器對(duì)象,Channel 讀寫文件或者網(wǎng)絡(luò)都要經(jīng)由 Buffer。在 Java NIO 中,Buffer 是一個(gè)頂層抽象類,它的常用子類有(前綴表示該 Buffer 可以存儲(chǔ)哪種類型的數(shù)據(jù)):
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
DoubleBuffer
FloatBuffer
涵蓋了 Java 中除 boolean 之外的所有的基本數(shù)據(jù)類型。其中 ByteBuffer 支持類型化的數(shù)據(jù)存取,即可以往 ByteBuffer 中放 byte 類型數(shù)據(jù)、也可以放 char、int、long、double 等類型的數(shù)據(jù),但讀取的時(shí)候要做好類型匹配處理,否則會(huì)拋出 BufferUnderflowException。
另外,Buffer 體系中還有一個(gè)重要的 MappedByteBuffer(ByteBuffer 的子類),可以讓文件內(nèi)容直接在堆外內(nèi)存中被修改,而如何同步到文件由 NIO 來(lái)完成。本文重點(diǎn)不在于此,有興趣的可以去探究一下 MappedByteBuffer 的底層原理。
1.4.2. 通道(Channel)
通道(Channel)是雙向的,可讀可寫。在 Java NIO 中,Buffer 是一個(gè)頂層接口,它的常用子類有:
FileChannel:用于文件讀寫
DatagramChannel:用于 UDP 數(shù)據(jù)包收發(fā)
ServerSocketChannel:用于服務(wù)端 TCP 數(shù)據(jù)包收發(fā)
SocketChannel:用于客戶端 TCP 數(shù)據(jù)包收發(fā)
1.4.3. 選擇器(Selector)
選擇器(Selector)是實(shí)現(xiàn) IO 多路復(fù)用的關(guān)鍵,多個(gè) Channel 注冊(cè)到某個(gè) Selector 上,當(dāng) Channel 上有事件發(fā)生時(shí),Selector 就會(huì)取得事件然后調(diào)用線程去處理事件。也就是說(shuō)只有當(dāng)連接上真正有讀寫等事件發(fā)生時(shí),線程才會(huì)去進(jìn)行讀寫等操作,這就不必為每個(gè)連接都創(chuàng)建一個(gè)線程,一個(gè)線程可以應(yīng)對(duì)多個(gè)連接。這就是 IO 多路復(fù)用的要義。
Netty 的 IO 線程 NioEventLoop 聚合了 Selector,可以同時(shí)并發(fā)處理成百上千的客戶端連接,后文會(huì)展開(kāi)描述。
在 Java NIO 中,Selector 是一個(gè)抽象類,它的常用方法有:
public?abstract?class?Selector?implements?Closeable?{
????......
????
????/**
?????*?得到一個(gè)選擇器對(duì)象
?????*/
????public?static?Selector?open()?throws?IOException?{
????????return?SelectorProvider.provider().openSelector();
????}
????......
????/**
?????*?返回所有發(fā)生事件的?Channel?對(duì)應(yīng)的?SelectionKey?的集合,通過(guò)
?????*?SelectionKey?可以找到對(duì)應(yīng)的?Channel
?????*/
????public?abstract?Set?selectedKeys() ;
????......
????
????/**
?????*?返回所有?Channel?對(duì)應(yīng)的?SelectionKey?的集合,通過(guò)?SelectionKey
?????*?可以找到對(duì)應(yīng)的?Channel
?????*/
????public?abstract?Set?keys() ;
????......
????
????/**
?????*?監(jiān)控所有注冊(cè)的?Channel,當(dāng)其中的?Channel?有?IO?操作可以進(jìn)行時(shí),
?????*?將這些 Channel 對(duì)應(yīng)的 SelectionKey 找到。參數(shù)用于設(shè)置超時(shí)時(shí)間
?????*/
????public?abstract?int?select(long?timeout)?throws?IOException;
????
????/**
????*?無(wú)超時(shí)時(shí)間的?select?過(guò)程,一直等待,直到發(fā)現(xiàn)有?Channel?可以進(jìn)行
????*?IO?操作
????*/
????public?abstract?int?select()?throws?IOException;
????
????/**
????*?立即返回的?select?過(guò)程
????*/
????public?abstract?int?selectNow()?throws?IOException;
????......
????
????/**
????*?喚醒?Selector,對(duì)無(wú)超時(shí)時(shí)間的?select?過(guò)程起作用,終止其等待
????*/
????public?abstract?Selector?wakeup();
????......
}
在上文的使用 Java NIO 編寫的服務(wù)端示例代碼中,服務(wù)端的工作流程為:
1)當(dāng)客戶端發(fā)起連接時(shí),會(huì)通過(guò) ServerSocketChannel 創(chuàng)建對(duì)應(yīng)的 SocketChannel。
2)調(diào)用 SocketChannel 的注冊(cè)方法將 SocketChannel 注冊(cè)到 Selector 上,注冊(cè)方法返回一個(gè) SelectionKey,該 SelectionKey 會(huì)被放入 Selector 內(nèi)部的 SelectionKey 集合中。該 SelectionKey 和 Selector 關(guān)聯(lián)(即通過(guò) SelectionKey 可以找到對(duì)應(yīng)的 Selector),也和 SocketChannel 關(guān)聯(lián)(即通過(guò) SelectionKey 可以找到對(duì)應(yīng)的 SocketChannel)。
4)Selector 會(huì)調(diào)用 select()/select(timeout)/selectNow()方法對(duì)內(nèi)部的 SelectionKey 集合關(guān)聯(lián)的 SocketChannel 集合進(jìn)行監(jiān)聽(tīng),找到有事件發(fā)生的 SocketChannel 對(duì)應(yīng)的 SelectionKey。
5)通過(guò) SelectionKey 找到有事件發(fā)生的 SocketChannel,完成數(shù)據(jù)處理。
以上過(guò)程的相關(guān)源碼為:
/**
*?SocketChannel?繼承?AbstractSelectableChannel
*/
public?abstract?class?SocketChannel
????extends?AbstractSelectableChannel
????implements?ByteChannel,?
???????????????ScatteringByteChannel,?
???????????????GatheringByteChannel,?
???????????????NetworkChannel
{
????......
}
public?abstract?class?AbstractSelectableChannel
????extends?SelectableChannel
{
????......
????/**
?????*?AbstractSelectableChannel?中包含注冊(cè)方法,SocketChannel?實(shí)例
?????*?借助該注冊(cè)方法注冊(cè)到?Selector?實(shí)例上去,該方法返回?SelectionKey
?????*/
????public?final?SelectionKey?register(
????????//?指明注冊(cè)到哪個(gè)?Selector?實(shí)例
????????Selector?sel,?
????????//?ops?是事件代碼,告訴?Selector?應(yīng)該關(guān)注該通道的什么事件
????????int?ops,
????????//?附加信息?attachment
????????Object?att)?throws?ClosedChannelException?{
????????......
????}
????......
}
public?abstract?class?SelectionKey?{
????......
????/**
?????*?獲取該?SelectionKey?對(duì)應(yīng)的?Channel
?????*/
????public?abstract?SelectableChannel?channel();
????/**
?????*?獲取該?SelectionKey?對(duì)應(yīng)的?Selector
?????*/
????public?abstract?Selector?selector();
????......
????
????/**
?????*?事件代碼,上面的?ops?參數(shù)取這里的值
?????*/
????public?static?final?int?OP_READ?=?1?<0;
????public?static?final?int?OP_WRITE?=?1?<2;
????public?static?final?int?OP_CONNECT?=?1?<3;
????public?static?final?int?OP_ACCEPT?=?1?<4;
????......
????
????/**
?????*?檢查該?SelectionKey?對(duì)應(yīng)的?Channel?是否可讀
?????*/
????public?final?boolean?isReadable()?{
????????return?(readyOps()?&?OP_READ)?!=?0;
????}
????/**
?????*?檢查該?SelectionKey?對(duì)應(yīng)的?Channel?是否可寫
?????*/
????public?final?boolean?isWritable()?{
????????return?(readyOps()?&?OP_WRITE)?!=?0;
????}
????/**
?????*?檢查該?SelectionKey?對(duì)應(yīng)的?Channel?是否已經(jīng)建立起?socket?連接
?????*/
????public?final?boolean?isConnectable()?{
????????return?(readyOps()?&?OP_CONNECT)?!=?0;
????}
????/**
?????*?檢查該?SelectionKey?對(duì)應(yīng)的?Channel?是否準(zhǔn)備好接受一個(gè)新的?socket?連接
?????*/
????public?final?boolean?isAcceptable()?{
????????return?(readyOps()?&?OP_ACCEPT)?!=?0;
????}
????/**
?????*?添加附件(例如?Buffer)
?????*/
????public?final?Object?attach(Object?ob)?{
????????return?attachmentUpdater.getAndSet(this,?ob);
????}
????/**
?????*?獲取附件
?????*/
????public?final?Object?attachment()?{
????????return?attachment;
????}
????......
}
下圖用于輔助讀者理解上面的過(guò)程和源碼:

首先說(shuō)明,本文以 Linux 系統(tǒng)為對(duì)象來(lái)研究文件 IO 模型和網(wǎng)絡(luò) IO 模型。
1.5. 零拷貝技術(shù)
注:本節(jié)討論的是 Linux 系統(tǒng)下的 IO 過(guò)程。并且對(duì)于零拷貝技術(shù)的講解采用了一種淺顯易懂但能觸及其本質(zhì)的方式,因?yàn)檫@個(gè)話題,展開(kāi)來(lái)講實(shí)在是有太多的細(xì)節(jié)要關(guān)注。
在“將本地磁盤中文件發(fā)送到網(wǎng)絡(luò)中”這一場(chǎng)景中,零拷貝技術(shù)是提升 IO 效率的一個(gè)利器,為了對(duì)比出零拷貝技術(shù)的優(yōu)越性,下面依次給出使用直接 IO 技術(shù)、內(nèi)存映射文件技術(shù)、零拷貝技術(shù)實(shí)現(xiàn)將本地磁盤文件發(fā)送到網(wǎng)絡(luò)中的過(guò)程。
1)直接 IO 技術(shù)
使用直接 IO 技術(shù)實(shí)現(xiàn)文件傳輸?shù)倪^(guò)程如下圖所示。

上圖中,內(nèi)核緩沖區(qū)是 Linux 系統(tǒng)的 Page Cahe。為了加快磁盤的 IO,Linux 系統(tǒng)會(huì)把磁盤上的數(shù)據(jù)以 Page 為單位緩存在操作系統(tǒng)的內(nèi)存里,這里的 Page 是 Linux 系統(tǒng)定義的一個(gè)邏輯概念,一個(gè) Page 一般為 4K。
可以看出,整個(gè)過(guò)程有四次數(shù)據(jù)拷貝,讀進(jìn)來(lái)兩次,寫回去又兩次:磁盤-->內(nèi)核緩沖區(qū)-->Socket 緩沖區(qū)-->網(wǎng)絡(luò)。
直接 IO 過(guò)程使用的 Linux 系統(tǒng) API 為:
ssize_t?read(int?filedes,?void?*buf,?size_t?nbytes);
ssize_t?write(int?filedes,?void?*buf,?size_t?nbytes);
等函數(shù)。
2)內(nèi)存映射文件技術(shù)
使用內(nèi)存映射文件技術(shù)實(shí)現(xiàn)文件傳輸?shù)倪^(guò)程如下圖所示。

可以看出,整個(gè)過(guò)程有三次數(shù)據(jù)拷貝,不再經(jīng)過(guò)應(yīng)用程序內(nèi)存,直接在內(nèi)核空間中從內(nèi)核緩沖區(qū)拷貝到 Socket 緩沖區(qū)。
內(nèi)存映射文件過(guò)程使用的 Linux 系統(tǒng) API 為:
void?*mmap(void?*addr,?size_t?length,?int?prot,?int?flags,?int?fd,?off_t?offset);
3)零拷貝技術(shù)
使用零拷貝技術(shù),連內(nèi)核緩沖區(qū)到 Socket 緩沖區(qū)的拷貝也省略了,如下圖所示:

內(nèi)核緩沖區(qū)到 Socket 緩沖區(qū)之間并沒(méi)有做數(shù)據(jù)的拷貝,只是一個(gè)地址的映射。底層的網(wǎng)卡驅(qū)動(dòng)程序要讀取數(shù)據(jù)并發(fā)送到網(wǎng)絡(luò)上的時(shí)候,看似讀取的是 Socket 的緩沖區(qū)中的數(shù)據(jù),其實(shí)直接讀的是內(nèi)核緩沖區(qū)中的數(shù)據(jù)。
零拷貝中所謂的“零”指的是內(nèi)存中數(shù)據(jù)拷貝的次數(shù)為 0。
零拷貝過(guò)程使用的 Linux 系統(tǒng) API 為:
ssize_t?sendfile(int?out_fd,?int?in_fd,?off_t?*offset,?size_t?count);
在 JDK 中,提供的:
FileChannel.transderTo(long?position,?long?count,?WritableByteChannel?target);
方法實(shí)現(xiàn)了零拷貝過(guò)程,其中的第三個(gè)參數(shù)可以傳入 SocketChannel 實(shí)例。例如客戶端使用以上的零拷貝接口向服務(wù)器傳輸文件的代碼為:
public?static?void?main(String[]?args)?throws?IOException?{
????SocketChannel?socketChannel?=?SocketChannel.open();
????socketChannel.connect(new?InetSocketAddress("127.0.0.1",?8080));
????String?fileName?=?"test.zip";
????//?得到一個(gè)文件?channel
????FileChannel?fileChannel?=?new?FileInputStream(fileName).getChannel();
????
????//?使用零拷貝?IO?技術(shù)發(fā)送
????long?transferSize?=?fileChannel.transferTo(0,?fileChannel.size(),?socketChannel);
????System.out.println("file?transfer?done,?size:?"?+?transferSize);
????fileChannel.close();
}
以上部分為第一章,學(xué)習(xí) Netty 需要的基礎(chǔ)知識(shí)。

騰訊、阿里、滴滴后臺(tái)面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學(xué)?那是因?yàn)槟銢](méi)認(rèn)真看完這篇文章

關(guān)注作者微信公眾號(hào) —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識(shí)以及最新面試寶典


看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動(dòng)力
