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

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

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

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

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

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

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

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

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

要使用Selector,得向Selector注冊Channel,然后調用它的select()方法。 這個方法會一直阻塞到某個注冊的通道有事件就緒。 一旦這個方法返回,線程就可以處理這些事件,事件例如有新連接進來,數據接收等。
NIO與epoll的關系
Java NIO根據操作系統不同, 針對NIO中的Selector有不同的實現:
- macosx:KQueueSelectorProvider
- solaris:DevPollSelectorProvider
- linux:EPollSelectorProvider (Linux kernels >= 2.6)或PollSelectorProvider
- windows:WindowsSelectorProvider
所以不需要特別指定,Oracle JDK會自動選擇合適的Selector。 如果想設置特定的Selector,可以設置屬性,例如:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
JDK在Linux已經默認使用epoll方式,但是JDK的epoll采用的是水平觸發,所以Netty自4.0.16起, Netty為Linux通過JNI的方式提供了native socket transport。 Netty重新實現了epoll機制,
- 采用邊緣觸發方式
- netty epoll transport暴露了更多的nio沒有的配置參數,如 TCP_CORK, SO_REUSEADDR等等。
- C代碼,更少GC,更少synchronized
使用native socket transport的方法很簡單,只需將相應的類替換即可。
NioEventLoopGroup → EpollEventLoopGroup NioEventLoop → EpollEventLoop NIOServerSocketChannel → EpollServerSocketChannel NioSocketChannel → EpollSocketChannel
NIO處理消息的核心思路
結合示例代碼,總結NIO的核心思路:
- NIO 模型中通常會有兩個線程,每個線程綁定一個輪詢器 selector ,在上面例子中serverSelector負責輪詢是否有新的連接,clientSelector負責輪詢連接是否有數據可讀
- 服務端監測到新的連接之后,不再創建一個新的線程,而是直接將新連接綁定到clientSelector上,這樣就不用BIO模型中1w 個while循環在阻塞,參見(1)
- clientSelector被一個 while 死循環包裹著,如果在某一時刻有多條連接有數據可讀,那么通過clientSelector.select(1)方法可以輪詢出來,進而批量處理,參見(2)
- 數據的讀寫面向 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 { // 對應IO編程中服務端啟動 ServerSocketChannel listenerChannel = ServerSocketChannel.open(); listenerChannel.socket().bind(new InetSocketAddress(8000)); listenerChannel.configureBlocking(false); listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); while (true) { // 監測是否有新的連接,這里的1指的是阻塞的時間為 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) 每來一個新連接,不需要創建一個線程,而是直接注冊到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) 批量輪詢是否有哪些連接有數據可讀,這里的1指的是阻塞的時間為 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(); } }