? ? Java??
? ? NIO 由以下幾個核心部分組成:
? ? Channels、Buffers、Selectors
? ? 雖然Java NIO 中除此之外還有很多類和組件,但在我看來,Channel,Buffer 和Selector 構成了核心的API。其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類。
Channel 和Buffer
? ? ? ? 基本上,所有的 IO 在NIO中都從一個Channel 開始。Channel 有點象流。數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。
? ? ? ? Channel和Buffer有好幾種類型。下面是JAVA NIO中的一些主要Channel的實現:
? ? ? ? FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel
? ? ? ? 正如你所看到的,這些通道涵蓋了UDP 和 TCP 網絡IO,以及文件IO。與這些類一起的有一些有趣的接口,但為簡單起見,我盡量在概述中不提到它們。本教程其它章節與它們相關的地方我會進行解釋。以下是Java NIO里關鍵的Buffer實現:
? ? ? ?ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
? ? ? 這些Buffer覆蓋了你能通過IO發送的基本數據類型:byte, short, int, long, float,double 和 char。Java NIO 還有個 MappedByteBuffer,用于表示內存映射文件。
? ? ? ?Selector
? ? ? ? Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。這是在一個單線程中使用一個Selector處理3個Channel的圖示:
? ? ? ? 要使用Selector,得向Selector注冊Channel,然后調用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。
Buffer的使用
? ? ? ?總結出使用Buffer一般遵循下面幾個步驟:
? ? ? ? 1. 分配空間(ByteBuffer buf = ByteBuffer.allocate(1024); 還有一種allocateDirector后面再陳述)
? ? ? ? 2. 寫入數據到Buffer(int bytesRead = fileChannel.read(buf);)
? ? ? ? 3. 調用filp()方法( buf.flip();)
? ? ? ? 4. 從Buffer中讀取數據(System.out.print((char)buf.get());)
? ? ? ? 5. 調用clear()方法或者compact()方法
? ? ? ?Buffer顧名思義:緩沖區,實際上是一個容器,一個連續數組。Channel提供從文件、網絡讀取數據的渠道,但是讀寫的數據都必須經過Buffer。如圖:
向Buffer中寫數據:
? ? ? ?從Channel寫到Buffer (fileChannel.read(buf))
? ? ? ?通過Buffer的put()方法 (buf.put(…))
從Buffer中讀取數據:
? ? ? ?從Buffer讀取到Channel (channel.write(buf))
? ? ? ?使用get()方法從Buffer中讀取數據 (buf.get())
? ? ? ? 可以把Buffer簡單地理解為一組基本數據類型的元素列表,它通過幾個變量來保存這個數據的當前位置狀態:capacity, position, limit, mark: 具體說明如圖表所示:
? ? ? ? capacity
? ? ? ? 作為一個內存塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往里寫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等于Buffer的capacity。當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)
? ? ? ?flip()方法
? ? ? ?flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,并將limit設置成之前position的值。換句話說,position現在用于標記讀的位置,limit表示之前寫進了多少個byte、char等 —— 現在能讀取多少個byte、char等。
? ? ? ? Scatter/Gather
? ? ? ?Java NIO開始支持scatter/gather,scatter/gather用于描述從Channel(譯者注:Channel在中文經常翻譯為通道)中讀取或者寫入到Channel的操作。
分散(scatter)從Channel中讀取是指在讀操作時將讀取的數據寫入多個buffer中。因此,Channel將從Channel中讀取的數據“分散(scatter)”到多個Buffer中。
聚集(gather)寫入Channel是指在寫操作時將多個buffer的數據寫入同一個Channel,因此,Channel 將多個Buffer中的數據“聚集(gather)”后發送到Channel。
? ? ? ?scatter/ gather經常用于需要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中,這樣你可以方便的處理消息頭和消息體。
代碼示例如下:
注意buffer首先被插入到數組,然后再將數組作為channel.read() 的輸入參數。read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿后,channel緊接著向另一個buffer中寫。
Scattering
Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味著它不適用于動態消息(譯者注:消息大小不固定)。換句話說,如果存在消息頭和消息體,消息頭必須完成填充(例如 128byte),Scattering Reads才能正常工作。
GatheringWrites
Gathering
Writes是指數據從多個buffer寫入到同一個channel。
代碼示例如下:
buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據才會被寫入。因此,如果一個buffer的容量為128byte,但是僅僅包含58byte的數據,那么這58byte的數據將被寫入到channel中。因此與Scattering Reads相反,Gathering Writes能較好的處理動態消息。
? ? ? ?舉例:我們通過ByteBuffer.allocate(11)方法創建了一個11個byte的數組的緩沖區,初始狀態如圖,position的位置為0,capacity和limit默認都是數組長度。
當我們寫入5個字節時,變化如下圖:
? ? ? ? 這時我們需要將緩沖區中的5個字節數據寫入Channel的通信信道,所以我們調用ByteBuffer.flip()方法,變化如下圖所示(position設回0,并將limit設成之前的position的值):
這時底層操作系統就可以從緩沖區中正確讀取這個5個字節數據并發送出去了。在下一次寫數據之前我們再調用clear()方法,緩沖區的索引位置又回到了初始位置。
調用clear()方法:
? ? ? ? position將被設回0,limit設置成capacity,換句話說,Buffer被清空了,其實Buffer中的數據并未被清楚,只是這些標記告訴我們可以從哪里開始往Buffer里寫數據。如果Buffer中有一些未讀的數據,調用clear()方法,數據將“被遺忘”,意味著不再有任何標記會告訴你哪些數據被讀過,哪些還沒有。如果Buffer中仍有未讀的數據,且后續還需要這些數據,但是此時想要先先寫些數據,那么使用compact()方法。
compact()方法:
? ? ? ? 將所有未讀的數據拷貝到Buffer起始處。然后將position設到最后一個未讀元素正后面。limit屬性依然像clear()方法一樣,設置成capacity?,F在Buffer準備好寫數據了,但是不會覆蓋未讀的數據。
通過調用Buffer.mark()方法:
? ? ? ? 可以標記Buffer中的一個特定的position,之后可以通過調用Buffer.reset()方法恢復到這個position。Buffer.rewind()方法將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素。
Selector
Selector運行單線程處理多個Channel,如果你的應用打開了多個通道,但每個連接的流量都很低,使用Selector就會很方便。例如在一個聊天服務器中。要使用Selector, 得向Selector注冊Channel,然后調用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新的連接進來、數據接收等。
? ? ? ? Selector的創建
? ? ? ? 通過調用Selector.open()方法創建一個Selector,如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ?Selectorselector = Selector.open();
? ? ? ? 向Selector注冊通道
? ? ? ? 為了將Channel和Selector配合使用,必須將channel注冊到selector上。通過SelectableChannel.register()方法來實現,如下:
? ? ? ?與Selector一起使用時,Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以。
注意register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什么事件感興趣。可以監聽四種不同類型的事件:
? ? ? ?Connect、Accept、Read、Write
? ? ? ? 通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱為“連接就緒”。一個server socket
? ? ? ? channel準備好接收新進入的連接稱為“接收就緒”。一個有數據可讀的通道可以說是“讀就緒”。等待寫數據的通道可以說是“寫就緒”。
這四種事件用SelectionKey的四個常量來表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果你對不止一種事件感興趣,那么可以用“位或”操作符將常量連接起來.
Netty架構按照Reactor模式設計和實現,它的服務端通信序列圖如下
? ? ?Netty的IO線程NioEventLoop由于聚合了多路復用器Selector,可以同時并發處理成百上千個客戶端Channel,由于讀寫操作都是非阻塞的,這就可以充分提升IO線程的運行效率,避免由于頻繁IO阻塞導致的線程掛起。另外,由于Netty采用了異步通信模式,一個IO線程可以并發處理N個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞IO一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。
注:參考部分其他網站內容: