NIO相比BIO的優(yōu)勢
NIO(Non-blocking I/O,在JAVA領(lǐng)域,也稱為New I/O),是一種同步非阻塞的I/O模型,也是I/O多路復(fù)用的基礎(chǔ),已經(jīng)被越來越多地應(yīng)用到大型應(yīng)用服務(wù)器,成為解決高并發(fā)與大量連接、I/O處理問題的有效方式。

面向流與面向緩沖
Java NIO和BIO之間第一個(gè)最大的區(qū)別是,BIO是面向流的,NIO是面向緩沖區(qū)的。 JavaIO面向流意味著每次從流中讀一個(gè)或多個(gè)字節(jié),直至讀取所有字節(jié),它們沒有被緩存在任何地方。 此外,它不能前后移動(dòng)流中的數(shù)據(jù)。 如果需要前后移動(dòng)從流中讀取的數(shù)據(jù),需要先將它緩存到一個(gè)緩沖區(qū)。 Java NIO的緩沖讀取方法略有不同。 數(shù)據(jù)讀取到一個(gè)緩沖區(qū),需要時(shí)可在緩沖區(qū)中前后移動(dòng)。 這就增加了處理過程中的靈活性。 但是,還需要檢查是否該緩沖區(qū)中包含所有需要處理的數(shù)據(jù)。 而且,需確保當(dāng)更多的數(shù)據(jù)讀入緩沖區(qū)時(shí),不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)。
阻塞IO與非阻塞IO
Java IO的各種流是阻塞的。 這意味著,當(dāng)一個(gè)線程調(diào)用read() 或write()時(shí),該線程被阻塞,直到有數(shù)據(jù)被讀取或者數(shù)據(jù)寫入。 該線程在阻塞期間不能做其他事情。 而Java NIO的非阻塞模式,如果通道沒有東西可讀,或不可寫,讀寫函數(shù)馬上返回,而不會阻塞,這個(gè)線程可以去做別的事情。 線程通常將非阻塞IO的空閑時(shí)間用于在其它通道上執(zhí)行IO操作,所以一個(gè)單獨(dú)的線程可以管理多個(gè)輸入和輸出通道(channel),即IO多路復(fù)用的原理。
零拷貝
在傳統(tǒng)的文件IO操作中,我們都是調(diào)用操作系統(tǒng)提供的底層標(biāo)準(zhǔn)IO系統(tǒng)調(diào)用函數(shù)read()、write() ,此時(shí)調(diào)用此函數(shù)的進(jìn)程(在JAVA中即java進(jìn)程)由當(dāng)前的用戶態(tài)切換到內(nèi)核態(tài),然后OS的內(nèi)核代碼負(fù)責(zé)將相應(yīng)的文件數(shù)據(jù)讀取到內(nèi)核的IO緩沖區(qū),然后再把數(shù)據(jù)從內(nèi)核IO緩沖區(qū)拷貝到進(jìn)程的私有地址空間中去,這樣便完成了一次IO操作。

而NIO的零拷貝與傳統(tǒng)的文件IO操作最大的不同之處就在于它雖然也是要從磁盤讀取數(shù)據(jù),但是它并不需要將數(shù)據(jù)讀取到OS內(nèi)核緩沖區(qū),而是直接將進(jìn)程的用戶私有地址空間中的一部分區(qū)域與文件對象建立起映射關(guān)系,這樣直接從內(nèi)存中讀寫文件,速度大幅度提升。

詳細(xì)的解析,之后會有單獨(dú)的博客進(jìn)行講解
NIO的核心部分
Java NIO主要由以下三個(gè)核心部分組成:
- Channel
- Buffer
- Selector
Channel
基本上,所有的IO在NIO中都從一個(gè)Channel開始。 數(shù)據(jù)可以從Channel讀到Buffer中,也可以從Buffer寫到Channel中。 這里有個(gè)圖示:

Channel和Buffer有好幾種類型。 下面是Java NIO中的一些主要Channel的實(shí)現(xiàn):
- FileChannel(file)
- DatagramChannel(UDP)
- SocketChannel(TCP)
- ServerSocketChannel(TCP)
這些通道涵蓋了UDP和TCP網(wǎng)絡(luò)IO以及文件IO。

最后兩個(gè)channel的關(guān)系。 通過 ServerSocketChannel.accept() 方法監(jiān)聽新進(jìn)來的連接。 當(dāng) accept()方法返回的時(shí)候,它返回一個(gè)包含新進(jìn)來的連接的 SocketChannel。 因此, accept()方法會一直阻塞到有新連接到達(dá)。 通常不會僅僅只監(jiān)聽一個(gè)連接,在while循環(huán)中調(diào)用 accept()方法.
//打開 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
//關(guān)閉ServerSocketChannel
serverSocketChannel.close();
Buffer
緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù),然后可以從中讀取數(shù)據(jù)的內(nèi)存。 這塊內(nèi)存被包裝成NIO Buffer對象,并提供了一組方法,用來方便的訪問該塊內(nèi)存。

Java NIO里關(guān)鍵的Buffer實(shí)現(xiàn):
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer覆蓋了你能通過IO發(fā)送的基本數(shù)據(jù)類型: byte、short、int、long、float、double和char。
為了理解Buffer的工作原理,需要熟悉它的三個(gè)屬性:
- capacity
- position
- limit
position和limit的含義取決于Buffer處在讀模式還是寫模式。 不管Buffer處在什么模式,capacity的含義總是一樣的。

capacity
作為一個(gè)內(nèi)存塊,Buffer有個(gè)固定的最大值,就是capacity。 Buffer只能寫capacity個(gè)byte、long、char等類型。 一旦Buffer滿了,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)寫數(shù)據(jù)往里寫數(shù)據(jù)。
position
當(dāng)寫數(shù)據(jù)到Buffer中時(shí),position表示當(dāng)前的位置。 初始的position值為0。 當(dāng)一個(gè)byte、long等數(shù)據(jù)寫到Buffer后, position會向前移動(dòng)到下一個(gè)可插入數(shù)據(jù)的Buffer單元。 position最大可為capacity – 1.
當(dāng)讀取數(shù)據(jù)時(shí),也是從某個(gè)特定位置讀。 當(dāng)將Buffer從寫模式切換到讀模式,position會被重置為0。 當(dāng)從Buffer的position處讀取數(shù)據(jù)時(shí),position向前移動(dòng)到下一個(gè)可讀的位置。

limit
在寫模式下,Buffer的limit表示最多能往Buffer里寫多少數(shù)據(jù)。 寫模式下,limit等于capacity。
當(dāng)切換Buffer到讀模式時(shí), limit表示你最多能讀到多少數(shù)據(jù)。 因此,當(dāng)切換Buffer到讀模式時(shí),limit會被設(shè)置成寫模式下的position值。
Selector
Selector允許單線程處理多個(gè) Channel。 如果你的應(yīng)用打開了多個(gè)連接(通道),但每個(gè)連接的流量都很低,使用Selector就會很方便。 例如,在一個(gè)聊天服務(wù)器中。
這是在一個(gè)單線程中使用一個(gè)Selector處理3個(gè)Channel的圖示:

要使用Selector,得向Selector注冊Channel,然后調(diào)用它的select()方法。 這個(gè)方法會一直阻塞到某個(gè)注冊的通道有事件就緒。 一旦這個(gè)方法返回,線程就可以處理這些事件,事件例如有新連接進(jìn)來,數(shù)據(jù)接收等。
NIO與epoll的關(guān)系
Java NIO根據(jù)操作系統(tǒng)不同, 針對NIO中的Selector有不同的實(shí)現(xiàn):
- macosx:KQueueSelectorProvider
- solaris:DevPollSelectorProvider
- linux:EPollSelectorProvider (Linux kernels >= 2.6)或PollSelectorProvider
- windows:WindowsSelectorProvider
所以不需要特別指定,Oracle JDK會自動(dòng)選擇合適的Selector。 如果想設(shè)置特定的Selector,可以設(shè)置屬性,例如:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
JDK在Linux已經(jīng)默認(rèn)使用epoll方式,但是JDK的epoll采用的是水平觸發(fā),所以Netty自4.0.16起, Netty為Linux通過JNI的方式提供了native socket transport。 Netty重新實(shí)現(xiàn)了epoll機(jī)制,
- 采用邊緣觸發(fā)方式
- netty epoll transport暴露了更多的nio沒有的配置參數(shù),如 TCP_CORK, SO_REUSEADDR等等。
- C代碼,更少GC,更少synchronized
使用native socket transport的方法很簡單,只需將相應(yīng)的類替換即可。
NioEventLoopGroup → EpollEventLoopGroup NioEventLoop → EpollEventLoop NIOServerSocketChannel → EpollServerSocketChannel NioSocketChannel → EpollSocketChannel
NIO處理消息的核心思路
結(jié)合示例代碼,總結(jié)NIO的核心思路:
- NIO 模型中通常會有兩個(gè)線程,每個(gè)線程綁定一個(gè)輪詢器 selector ,在上面例子中serverSelector負(fù)責(zé)輪詢是否有新的連接,clientSelector負(fù)責(zé)輪詢連接是否有數(shù)據(jù)可讀
- 服務(wù)端監(jiān)測到新的連接之后,不再創(chuàng)建一個(gè)新的線程,而是直接將新連接綁定到clientSelector上,這樣就不用BIO模型中1w 個(gè)while循環(huán)在阻塞,參見(1)
- clientSelector被一個(gè) while 死循環(huán)包裹著,如果在某一時(shí)刻有多條連接有數(shù)據(jù)可讀,那么通過clientSelector.select(1)方法可以輪詢出來,進(jìn)而批量處理,參見(2)
- 數(shù)據(jù)的讀寫面向 Buffer,參見(3)
NIO的示例代碼
public class NIOServer { public static void main(String[] args) throws IOException { Selector serverSelector = Selector.open(); Selector clientSelector = Selector.open(); new Thread(() -> { try { // 對應(yīng)IO編程中服務(wù)端啟動(dòng) ServerSocketChannel listenerChannel = ServerSocketChannel.open(); listenerChannel.socket().bind(new InetSocketAddress(8000)); listenerChannel.configureBlocking(false); listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); while (true) { // 監(jiān)測是否有新的連接,這里的1指的是阻塞的時(shí)間為 1ms if (serverSelector.select(1) > 0) { Set<SelectionKey> set = serverSelector.selectedKeys(); Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { try { // (1) 每來一個(gè)新連接,不需要?jiǎng)?chuàng)建一個(gè)線程,而是直接注冊到clientSelector SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); clientChannel.register(clientSelector, SelectionKey.OP_READ); } finally { keyIterator.remove(); } } } } } } catch (IOException ignored) { } }).start(); new Thread(() -> { try { while (true) { // (2) 批量輪詢是否有哪些連接有數(shù)據(jù)可讀,這里的1指的是阻塞的時(shí)間為 1ms if (clientSelector.select(1) > 0) { Set<SelectionKey> set = clientSelector.selectedKeys(); Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isReadable()) { try { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // (3) 面向 Buffer clientChannel.read(byteBuffer); byteBuffer.flip(); System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer) .toString()); } finally { keyIterator.remove(); key.interestOps(SelectionKey.OP_READ); } } } } } } catch (IOException ignored) { } }).start(); } }