NIO教程 ——檢視閱讀
簡介
NIO中的N可以理解為Non-blocking ,不單純是New 。
不同點:
- 標(biāo)準(zhǔn)的IO編程接口是面向字節(jié)流和字符流的。而NIO是面向通道和緩沖區(qū)的,數(shù)據(jù)總是從通道中讀到buffer緩沖區(qū)內(nèi),或者從buffer寫入到通道中。
- Java NIO使我們可以進行非阻塞IO操作。比如說,單線程中從通道讀取數(shù)據(jù)到buffer,同時可以繼續(xù)做別的事情,當(dāng)數(shù)據(jù)讀取到buffer中后,線程再繼續(xù)處理數(shù)據(jù)。寫數(shù)據(jù)也是一樣的。
- NIO中有一個“slectors”的概念。selector可以檢測多個通道的事件狀態(tài)(例如:鏈接打開,數(shù)據(jù)到達)這樣單線程就可以操作多個通道的數(shù)據(jù)。
概覽
NIO包含下面3個核心的組件,Channel,Buffer和Selector組成了這個核心的API:
- Channels ——通道
- Buffers ——緩沖區(qū)
- Selectors ——選擇器
通常來說NIO中的所有IO都是從Channel開始的。Channel和流有點類似。通過Channel,我們即可以從Channel把數(shù)據(jù)寫到Buffer中,也可以把數(shù)據(jù)沖Buffer寫入到Channel 。

有很多的Channel,Buffer類型。下面列舉了主要的幾種:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
正如你看到的,這些channel基于于UDP和TCP的網(wǎng)絡(luò)IO,以及文件IO。 和這些類一起的還有其他一些比較有趣的接口,在本節(jié)中暫時不多介紹。為了簡潔起見,我們會在必要的時候引入這些概念。 下面是核心的Buffer實現(xiàn)類的列表:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer涵蓋了可以通過IO操作的基礎(chǔ)類型:byte,short,int,long,float,double以及characters. NIO實際上還包含一種MappedBytesBuffer,一般用于和內(nèi)存映射的文件。
選擇器允許單線程操作多個通道。如果你的程序中有大量的鏈接,同時每個鏈接的IO帶寬不高的話,這個特性將會非常有幫助。比如聊天服務(wù)器。 下面是一個單線程中Slector維護3個Channel的示意圖:

要使用Selector的話,我們必須把Channel注冊到Selector上,然后就可以調(diào)用Selector的select()方法。這個方法會進入阻塞,直到有一個channel的狀態(tài)符合條件。當(dāng)方法返回后,線程可以處理這些事件。
Java NIO Channel通道
Java NIO Channel通道和流非常相似,主要有以下3點區(qū)別:
- 通道可以讀也可以寫,流一般來說是單向的(只能讀或者寫)。
- 通道可以異步讀寫。
- 通道總是基于緩沖區(qū)Buffer來讀寫。
Channel的實現(xiàn)
下面列出Java NIO中最重要的集中Channel的實現(xiàn):
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel用于文件的數(shù)據(jù)讀寫。 DatagramChannel用于UDP的數(shù)據(jù)讀寫。 SocketChannel用于TCP的數(shù)據(jù)讀寫。 ServerSocketChannel允許我們監(jiān)聽TCP鏈接請求,每個請求會創(chuàng)建會一個SocketChannel.
RandomAccessFile擴展:
RandomAccessFile(隨機訪問文件)類。該類是Java語言中功能最為豐富的文件訪問類 。RandomAccessFile類支持“隨機訪問”方式,這里“隨機”是指可以跳轉(zhuǎn)到文件的任意位置處讀寫數(shù)據(jù)。在訪問一個文件的時候,不必把文件從頭讀到尾,而是希望像訪問一個數(shù)據(jù)庫一樣“隨心所欲”地訪問一個文件的某個部分,這時使用RandomAccessFile類就是最佳選擇。
四種模式:R RW RWD RWS
| r | 以只讀的方式打開文本,也就意味著不能用write來操作文件 |
|---|---|
| rw | 讀操作和寫操作都是允許的 |
| rws | 每當(dāng)進行寫操作,同步的刷新到磁盤,刷新內(nèi)容和元數(shù)據(jù) |
| rwd | 每當(dāng)進行寫操作,同步的刷新到磁盤,刷新內(nèi)容 |
RandomAccessFile的用處:
1、大型文本日志類文件的快速定位獲取數(shù)據(jù):
得益于seek的巧妙設(shè)計,我認(rèn)為我們可以從超大的文本中快速定位我們的游標(biāo),例如每次存日志的時候,我們可以建立一個索引緩存,索引是日志的起始日期,value是文本的poiniter 也就是光標(biāo),這樣我們可以快速定位某一個時間段的文本內(nèi)容
2、并發(fā)讀寫
也是得益于seek的設(shè)計,我認(rèn)為多線程可以輪流操作seek控制光標(biāo)的位置,從未達到不同線程的并發(fā)寫操作。
3、更方便的獲取二進制文件
通過自帶的讀寫轉(zhuǎn)碼(readDouble、writeLong等),我認(rèn)為可以快速的完成字節(jié)碼到字符的轉(zhuǎn)換功能,對使用者來說比較友好。
RandomAccessFile參考
實例:
public class FileChannelTest {
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("D:\\text\\1_loan.sql", "r");
//mode只有4中,如果不是讀寫的mode或者給的不是4種中的,就會報錯。
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
try {
FileChannel fileChannel = file.getChannel();
FileChannel copyFileChannel = copyFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = fileChannel.read(byteBuffer);
while (read != -1) {
System.out.println("read:" + read);
//byteBuffer緩沖區(qū)切換為讀模式
byteBuffer.flip();
copyFileChannel.write(byteBuffer);
//“清空”byteBuffer緩沖區(qū),以滿足后續(xù)寫入操作
byteBuffer.clear();
//注意,每次讀時都要返回讀后的狀態(tài)read值賦值給循環(huán)判斷體read,否則會陷入死循環(huán)true
read = fileChannel.read(byteBuffer);
}
} finally {
file.close();
copyFile.close();
}
}
}
報錯:
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "w");
//因為沒有"w"的mode
Exception in thread "main" java.lang.IllegalArgumentException: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd"
at java.io.RandomAccessFile.<init>(RandomAccessFile.java:221)
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
//因為沒有"w"的權(quán)限
Exception in thread "main" java.nio.channels.NonWritableChannelException
at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:194)
at com.niotest.FileChannelTest.main(FileChannelTest.java:33)
NIO Buffer緩沖區(qū)
Java NIO Buffers用于和NIO Channel交互。正如你已經(jīng)知道的,我們從channel中讀取數(shù)據(jù)到buffers里,從buffer把數(shù)據(jù)寫入到channels.
buffer本質(zhì)上就是一塊內(nèi)存區(qū),可以用來寫入數(shù)據(jù),并在稍后讀取出來。這塊內(nèi)存被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發(fā)的接口。
Buffer基本用法
利用Buffer讀寫數(shù)據(jù),通常遵循四個步驟:
- 把數(shù)據(jù)寫入buffer;
- 調(diào)用flip;
- 從Buffer中讀取數(shù)據(jù);
- 調(diào)用buffer.clear()或者buffer.compact()
當(dāng)寫入數(shù)據(jù)到buffer中時,buffer會記錄已經(jīng)寫入的數(shù)據(jù)大小。當(dāng)需要讀數(shù)據(jù)時,通過flip()方法把buffer從寫模式調(diào)整為讀模式;在讀模式下,可以讀取所有已經(jīng)寫入的數(shù)據(jù)。
當(dāng)讀取完數(shù)據(jù)后,需要清空buffer,以滿足后續(xù)寫入操作。清空buffer有兩種方式:調(diào)用clear()或compact()方法。clear會清空整個buffer,compact則只清空已讀取的數(shù)據(jù),未被讀取的數(shù)據(jù)會被移動到buffer的開始位置,寫入位置則緊跟著未讀數(shù)據(jù)之后。
Buffer的容量,位置,上限(Buffer Capacity, Position and Limit)
buffer緩沖區(qū)實質(zhì)上就是一塊內(nèi)存,用于寫入數(shù)據(jù),也供后續(xù)再次讀取數(shù)據(jù)。這塊內(nèi)存被NIO Buffer管理,并提供一系列的方法用于更簡單的操作這塊內(nèi)存。
一個Buffer有三個屬性是必須掌握的,分別是:
- capacity容量
- position位置
- limit限制
position和limit的具體含義取決于當(dāng)前buffer的模式。capacity在兩種模式下都表示容量。
下面有張示例圖,描訴了不同模式下position和limit的含義:

容量(Capacity)
作為一塊內(nèi)存,buffer有一個固定的大小,叫做capacity容量。也就是最多只能寫入容量值的字節(jié),整形等數(shù)據(jù)。一旦buffer寫滿了就需要清空已讀數(shù)據(jù)以便下次繼續(xù)寫入新的數(shù)據(jù)。
位置(Position)
當(dāng)寫入數(shù)據(jù)到Buffer的時候需要中一個確定的位置開始,默認(rèn)初始化時這個位置position為0,一旦寫入了數(shù)據(jù)比如一個字節(jié),整形數(shù)據(jù),那么position的值就會指向數(shù)據(jù)之后的一個單元,position最大可以到capacity-1.
當(dāng)從Buffer讀取數(shù)據(jù)時,也需要從一個確定的位置開始。buffer從寫入模式變?yōu)樽x取模式時,position會歸零,每次讀取后,position向后移動。
上限(Limit)
在寫模式,limit的含義是我們所能寫入的最大數(shù)據(jù)量。它等同于buffer的容量。
一旦切換到讀模式,limit則代表我們所能讀取的最大數(shù)據(jù)量,他的值等同于寫模式下position的位置。
數(shù)據(jù)讀取的上限時buffer中已有的數(shù)據(jù),也就是limit的位置(原寫模式下position所指的位置)。
Buffer Types
Java NIO有如下具體的Buffer類型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
正如你看到的,Buffer的類型代表了不同數(shù)據(jù)類型,換句話說,Buffer中的數(shù)據(jù)可以是上述的基本類型;
分配一個Buffer(Allocating a Buffer)
為了獲取一個Buffer對象,你必須先分配。每個Buffer實現(xiàn)類都有一個allocate()方法用于分配內(nèi)存。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
CharBuffer charBuffer = CharBuffer.allocate(48);
寫入數(shù)據(jù)到Buffer(Writing Data to a Buffer)
寫數(shù)據(jù)到Buffer有兩種方法:
- 從Channel中寫數(shù)據(jù)到Buffer
- 手動寫數(shù)據(jù)到Buffer,調(diào)用put方法
//從Channel中寫數(shù)據(jù)到Buffer
int read = fileChannel.read(byteBuffer);
//調(diào)用put方法寫
buf.put(3);
//把數(shù)據(jù)寫到特定的位置
public ByteBuffer put(int i, byte x);
//把一個具體類型數(shù)據(jù)寫入buffer
public ByteBuffer putInt(int x);
flip()——翻轉(zhuǎn)
flip()方法可以把Buffer從寫模式切換到讀模式。調(diào)用flip方法會把position歸零,并設(shè)置limit為之前的position的值。 也就是說,現(xiàn)在position代表的是讀取位置,limit標(biāo)示的是已寫入的數(shù)據(jù)位置。
從Buffer讀取數(shù)據(jù)(Reading Data from a Buffer)
從Buffer讀數(shù)據(jù)也有兩種方式。
- 從buffer讀數(shù)據(jù)到channel。
- 從buffer直接讀取數(shù)據(jù),調(diào)用get方法。
//讀取數(shù)據(jù)到channel的例子:
int bytesWritten = inChannel.write(buf);
//調(diào)用get讀取數(shù)據(jù)的例子:
byte aByte = buf.get();
rewind()——倒帶
Buffer.rewind()方法將position置為0,這樣我們可以重復(fù)讀取buffer中的數(shù)據(jù)。limit保持不變。
clear() and compact()
一旦我們從buffer中讀取完數(shù)據(jù),需要復(fù)用buffer為下次寫數(shù)據(jù)做準(zhǔn)備。只需要調(diào)用clear或compact方法。
clear方法會重置position為0,limit為capacity,也就是整個Buffer清空。實際上Buffer中數(shù)據(jù)并沒有清空,我們只是把標(biāo)記為修改了。(重新寫入的時候這些存在的數(shù)據(jù)就會被新的數(shù)據(jù)覆蓋)
如果Buffer還有一些數(shù)據(jù)沒有讀取完,調(diào)用clear就會導(dǎo)致這部分?jǐn)?shù)據(jù)被“遺忘”,因為我們沒有標(biāo)記這部分?jǐn)?shù)據(jù)未讀。
針對這種情況,如果需要保留未讀數(shù)據(jù),那么可以使用compact()。 因此compact()和clear()的區(qū)別就在于對未讀數(shù)據(jù)的處理,是保留這部分?jǐn)?shù)據(jù)還是一起清空。
mark() and reset()
通過mark方法可以標(biāo)記當(dāng)前的position,通過reset來恢復(fù)mark的位置,這個非常像canva的save和restore:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals() and compareTo()
可以用eqauls和compareTo比較兩個buffer
equals()
判斷兩個buffer相對,需滿足:
- 類型相同
- buffer中剩余字節(jié)數(shù)相同
- 所有剩余字節(jié)相等
從上面的三個條件可以看出,equals只比較buffer中的部分內(nèi)容,并不會去比較每一個元素。
compareTo()
compareTo也是比較buffer中的剩余元素,只不過這個方法適用于比較排序的:
NIO Scatter (分散)/ Gather(聚集)
——分散讀和聚集寫的場景。
Java NIO發(fā)布時內(nèi)置了對scatter / gather的支持。scatter / gather是通過通道讀寫數(shù)據(jù)的兩個概念。
Scattering read指的是從通道讀取的操作能把數(shù)據(jù)寫入多個buffer,也就是scatters代表了數(shù)據(jù)從一個channel到多個buffer的過程。
gathering write則正好相反,表示的是從多個buffer把數(shù)據(jù)寫入到一個channel中。
Scatter/gather在有些場景下會非常有用,比如需要處理多份分開傳輸?shù)臄?shù)據(jù)。舉例來說,假設(shè)一個消息包含了header和body,我們可能會把header和body保存在不同獨立buffer中,這種分開處理header與body的做法會使開發(fā)更簡明。
Scattering Reads
"scattering read"是把數(shù)據(jù)從單個Channel寫入到多個buffer,下面是示意圖:

觀察代碼可以發(fā)現(xiàn),我們把多個buffer寫在了一個數(shù)組中,然后把數(shù)組傳遞給channel.read()方法。read()方法內(nèi)部會負(fù)責(zé)把數(shù)據(jù)按順序?qū)戇M傳入的buffer數(shù)組內(nèi)。一個buffer寫滿后,接著寫到下一個buffer中。
實際上,scattering read內(nèi)部必須寫滿一個buffer后才會向后移動到下一個buffer,因此這并不適合消息大小會動態(tài)改變的部分,也就是說,如果你有一個header和body,并且header有一個固定的大小(比如128字節(jié)),這種情形下可以正常工作。
gathering Writes
"gathering write"把多個buffer的數(shù)據(jù)寫入到同一個channel中.

傳入一個buffer數(shù)組給write,內(nèi)部會按順序?qū)?shù)組內(nèi)的內(nèi)容寫進channel,這里需要注意,寫入的時候針對的是buffer中position到limit之間的數(shù)據(jù)。也就是如果buffer的容量是128字節(jié),但它只包含了58字節(jié)數(shù)據(jù),那么寫入的時候只有58字節(jié)會真正寫入。因此gathering write是可以適用于可變大小的message的,這和scattering reads不同。
NIO Channel to Channel Transfers通道傳輸接口
在Java NIO中如果一個channel是FileChannel類型的,那么他可以直接把數(shù)據(jù)傳輸?shù)搅硪粋€channel。這個特性得益于FileChannel包含的transferTo和transferFrom兩個方法。
transferFrom()——目標(biāo)channel用,參數(shù)為源數(shù)據(jù)channel。
transferFrom的參數(shù)position和count表示目標(biāo)文件的寫入位置和最多寫入的數(shù)據(jù)量。如果通道源的數(shù)據(jù)小于count那么就傳實際有的數(shù)據(jù)量。 另外,有些SocketChannel的實現(xiàn)在傳輸時只會傳輸哪些處于就緒狀態(tài)的數(shù)據(jù),即使SocketChannel后續(xù)會有更多可用數(shù)據(jù)。因此,這個傳輸過程可能不會傳輸整個的數(shù)據(jù)。
transferTo()——源數(shù)據(jù)用,參數(shù)為目標(biāo)channel
SocketChannel的問題也存在于transferTo.SocketChannel的實現(xiàn)可能只在發(fā)送的buffer填充滿后才發(fā)送,并結(jié)束。
實例:
public class ChannelTransferTest {
public static void main(String[] args) throws IOException {
RandomAccessFile fromfile = new RandomAccessFile("D:\\text\\1_loan.sql", "rw");
//mode只有4中,如果不是讀寫的mode或者給的不是4種中的,就會報錯。
RandomAccessFile toFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "rw");
FileChannel fromfileChannel = fromfile.getChannel();
FileChannel toFileChannel = toFile.getChannel();
//==========================transferTo=================================
//transferTo方法把fromfileChannel數(shù)據(jù)傳輸?shù)搅硪粋€toFileChannel
//long transferSize = fromfileChannel.transferTo(0, fromfileChannel.size(), toFileChannel);
//System.out.println(transferSize);
//=============================transferFrom==============================
//把數(shù)據(jù)從通道源傳輸?shù)絫oFileChannel,相比通過buffer讀寫更加的便捷
long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size());
//參數(shù)position和count表示目標(biāo)文件的寫入位置和最多寫入的數(shù)據(jù)量
//long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()-1000);
//如果通道源的數(shù)據(jù)小于count那么就傳實際有的數(shù)據(jù)量。
//long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()+1000);
System.out.println(transferSize1);
}
}
NIO Selector選擇器
Selector是Java NIO中的一個組件,用于檢查一個或多個NIO Channel的狀態(tài)是否處于可讀、可寫。如此可以實現(xiàn)單線程管理多個channels,也就是可以管理多個網(wǎng)絡(luò)鏈接。
為什么使用Selector
用單線程處理多個channels的好處是我需要更少的線程來處理channel。實際上,你甚至可以用一個線程來處理所有的channels。從操作系統(tǒng)的角度來看,切換線程開銷是比較昂貴的,并且每個線程都需要占用系統(tǒng)資源,因此暫用線程越少越好。
需要留意的是,現(xiàn)代操作系統(tǒng)和CPU在多任務(wù)處理上已經(jīng)變得越來越好,所以多線程帶來的影響也越來越小。如果一個CPU是多核的,如果不執(zhí)行多任務(wù)反而是浪費了機器的性能。不過這些設(shè)計討論是另外的話題了。簡而言之,通過Selector我們可以實現(xiàn)單線程操作多個channel。

創(chuàng)建Selector
創(chuàng)建一個Selector可以通過Selector.open()方法:
Selector selector = Selector.open();
注冊Channel到Selector上
先把Channel注冊到Selector上,這個操作使用SelectableChannel的register()。SocketChannel等都有繼承此抽象類。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必須是非阻塞的。所以FileChannel不適用Selector,因為FileChannel不能切換為非阻塞模式。Socket channel可以正常使用。
注意register的第二個參數(shù),這個參數(shù)是一個“關(guān)注集合”,代表我們關(guān)注的channel狀態(tài),有四種基礎(chǔ)類型可供監(jiān)聽:
- Connect——連接就緒(連接成功后)
- Accept——可連接就緒(接受請求連接時)
- Read——讀就緒
- Write——寫就緒
一個channel觸發(fā)了一個事件也可視作該事件處于就緒狀態(tài)。因此當(dāng)channel與server連接成功后,那么就是“連接就緒”狀態(tài)。server channel接收請求連接時處于“可連接就緒”狀態(tài)。channel有數(shù)據(jù)可讀時處于“讀就緒”狀態(tài)。channel可以進行數(shù)據(jù)寫入時處于“寫就緒”狀態(tài)。
上述的四種就緒狀態(tài)用SelectionKey中的常量表示如下:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果對多個事件感興趣可利用位的或運算結(jié)合多個常量,比如:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
在上一小節(jié)中,我們利用register方法把Channel注冊到了Selectors上,這個方法的返回值是SelectionKeys,這個返回的對象包含了一些比較有價值的屬性:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
Interest Set
這個“關(guān)注集合”實際上就是我們希望處理的事件的集合,它的值就是注冊時傳入的參數(shù),我們可以用按為與運算把每個事件取出來:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
Ready Set
"就緒集合"中的值是當(dāng)前channel處于就緒的值,一般來說在調(diào)用了select方法后都會需要用到就緒狀態(tài)
int readySet = selectionKey.readyOps();
從“就緒集合”中取值的操作類似于“關(guān)注集合”的操作,當(dāng)然還有更簡單的方法,SelectionKey提供了一系列返回值為boolean的的方法:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
從SelectionKey操作Channel和Selector非常簡單:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
我們可以給一個SelectionKey附加一個Object,這樣做一方面可以方便我們識別某個特定的channel,同時也增加了channel相關(guān)的附加信息。例如,可以把用于channel的buffer附加到SelectionKey上:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
附加對象的操作也可以在register的時候就執(zhí)行:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
從Selector中選擇channel
一旦我們向Selector注冊了一個或多個channel后,就可以調(diào)用select來獲取channel。select方法會返回所有處于就緒狀態(tài)的channel。 select方法具體如下:
- int select()
- int select(long timeout)
- int selectNow()
select()方法在返回channel之前處于阻塞狀態(tài)。 select(long timeout)和select做的事一樣,不過他的阻塞有一個超時限制。
selectNow()不會阻塞,根據(jù)當(dāng)前狀態(tài)立刻返回合適的channel。
select()方法的返回值是一個int整形,代表有多少channel處于就緒了。也就是自上一次select后有多少channel進入就緒。舉例來說,假設(shè)第一次調(diào)用select時正好有一個channel就緒,那么返回值是1,并且對這個channel做任何處理,接著再次調(diào)用select,此時恰好又有一個新的channel就緒,那么返回值還是1,現(xiàn)在我們一共有兩個channel處于就緒,但是在每次調(diào)用select時只有一個channel是就緒的。
selectedKeys()
在調(diào)用select并返回了有channel就緒之后,可以通過選中的key集合來獲取channel,這個操作通過調(diào)用selectedKeys()方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
還記得在register時的操作吧,我們register后的返回值就是SelectionKey實例,也就是我們現(xiàn)在通過selectedKeys()方法所返回的SelectionKey。
遍歷這些SelectionKey可以通過如下方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
上述循環(huán)會迭代key集合,針對每個key我們單獨判斷他是處于何種就緒狀態(tài)。
注意:keyIterater.remove()方法的調(diào)用,Selector本身并不會移除SelectionKey對象,這個操作需要我們手動執(zhí)行。當(dāng)下次channel處于就緒是,Selector仍然會把這些key再次加入進來。
SelectionKey.channel返回的channel實例需要強轉(zhuǎn)為我們實際使用的具體的channel類型,例如ServerSocketChannel或SocketChannel.
wakeUp()
由于調(diào)用select而被阻塞的線程,可以通過調(diào)用Selector.wakeup()來喚醒即便此時已然沒有channel處于就緒狀態(tài)。具體操作是,在另外一個線程調(diào)用wakeup,被阻塞與select方法的線程就會立刻返回。
close()
當(dāng)操作Selector完畢后,需要調(diào)用close方法。close的調(diào)用會關(guān)閉Selector并使相關(guān)的SelectionKey都無效。channel本身不會被關(guān)閉。
示例:首先打開一個Selector,然后注冊channel,最后監(jiān)聽Selector的狀態(tài)。
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1.獲取通道
ServerSocketChannel server = ServerSocketChannel.open();
// 2.切換成非阻塞模式
server.configureBlocking(false);
// 3. 綁定連接
server.bind(new InetSocketAddress(6666));
// 4. 獲取選擇器
Selector selector = Selector.open();
// 4.1將通道注冊到選擇器上,指定接收“監(jiān)聽通道”事件
server.register(selector, SelectionKey.OP_ACCEPT);
// 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒
while (selector.select() > 0) {
// 6. 獲取當(dāng)前選擇器所有注冊的“選擇鍵”(已就緒的監(jiān)聽事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 獲取已“就緒”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 接收事件就緒
if (selectionKey.isAcceptable()) {
// 8. 獲取客戶端的鏈接
SocketChannel client = server.accept();
// 8.1 切換成非阻塞狀態(tài)
client.configureBlocking(false);
// 8.2 注冊到選擇器上-->拿到客戶端的連接為了讀取通道的數(shù)據(jù)(監(jiān)聽讀就緒事件)
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) { // 讀事件就緒
// 9. 獲取當(dāng)前選擇器讀就緒狀態(tài)的通道
SocketChannel client = (SocketChannel) selectionKey.channel();
// 9.1讀取數(shù)據(jù)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 9.2得到文件通道,將客戶端傳遞過來的圖片寫到本地項目下(寫模式、沒有則創(chuàng)建)
FileChannel outChannel = FileChannel.open(Paths.get("2_loan.sql"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
while (client.read(buffer) > 0) {
// 在讀之前都要切換成讀模式
buffer.flip();
outChannel.write(buffer);
// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
buffer.clear();
}
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("yeah,i know,i got your message!".getBytes());
byteBuffer.flip();
client.write(byteBuffer);
}
// 10. 取消選擇鍵(已經(jīng)處理過的事件,就應(yīng)該取消掉了)
iterator.remove();
}
}
}
}
public class NIOClientTwo {
public static void main(String[] args) throws IOException {
// 1. 獲取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 1.1切換成非阻塞模式
socketChannel.configureBlocking(false);
// 1.2獲取選擇器
Selector selector = Selector.open();
// 1.3將通道注冊到選擇器中,獲取服務(wù)端返回的數(shù)據(jù)
socketChannel.register(selector, SelectionKey.OP_READ);
// 2. 發(fā)送一張圖片給服務(wù)端吧
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\text\\1_loan.sql"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數(shù)據(jù)打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.讀取本地文件(圖片),發(fā)送到服務(wù)器
while (fileChannel.read(buffer) != -1) {
// 在讀之前都要切換成讀模式
buffer.flip();
socketChannel.write(buffer);
// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
buffer.clear();
}
// 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒
while (selector.select() > 0) {
// 6. 獲取當(dāng)前選擇器所有注冊的“選擇鍵”(已就緒的監(jiān)聽事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 獲取已“就緒”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 8. 讀事件就緒
if (selectionKey.isReadable()) {
// 8.1得到對應(yīng)的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
// 9. 知道服務(wù)端要返回響應(yīng)的數(shù)據(jù)給客戶端,客戶端在這里接收
int readBytes = channel.read(responseBuffer);
if (readBytes > 0) {
// 切換讀模式
responseBuffer.flip();
System.out.println(new String(responseBuffer.array(), 0, readBytes));
}
}
// 10. 取消選擇鍵(已經(jīng)處理過的事件,就應(yīng)該取消掉了)
iterator.remove();
}
}
}
}
NIO FileChannel文件通道
Java NIO中的FileChannel是用于連接文件的通道。通過文件通道可以讀、寫文件的數(shù)據(jù)。Java NIO的FileChannel是相對標(biāo)準(zhǔn)Java IO API的可選接口。
FileChannel不可以設(shè)置為非阻塞模式,他只能在阻塞模式下運行。
打開文件通道
在使用FileChannel前必須打開通道,打開一個文件通道需要通過輸入/輸出流或者RandomAccessFile,下面是通過RandomAccessFile打開文件通道的案例:
RandomAccessFile aFile = new RandomAccessFile("D:\text\1_loan.sql", "rw");
FileChannel inChannel = aFile.getChannel();
從文件通道內(nèi)讀取數(shù)據(jù)
讀取文件通道的數(shù)據(jù)可以通過read方法:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
首先開辟一個Buffer,從通道中讀取的數(shù)據(jù)會寫入Buffer內(nèi)。接著就可以調(diào)用read方法,read的返回值代表有多少字節(jié)被寫入了Buffer,返回-1則表示已經(jīng)讀取到文件結(jié)尾了。
向文件通道寫入數(shù)據(jù)
寫數(shù)據(jù)用write方法,入?yún)⑹荁uffer:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
注意這里的write調(diào)用寫在了wihle循環(huán)匯總,這是因為write不能保證有多少數(shù)據(jù)真實被寫入,因此需要循環(huán)寫入直到?jīng)]有更多數(shù)據(jù)。
關(guān)閉通道
操作完畢后,需要把通道關(guān)閉:
channel.close();
FileChannel Position
當(dāng)操作FileChannel的時候讀和寫都是基于特定起始位置的(position),獲取當(dāng)前的位置可以用FileChannel的position()方法,設(shè)置當(dāng)前位置可以用帶參數(shù)的position(long pos)方法。
//獲取當(dāng)前的位置
long position = fileChannel.position();
//設(shè)置當(dāng)前位置為pos +123
fileChannel.position(pos +123);
假設(shè)我們把當(dāng)前位置設(shè)置為文件結(jié)尾之后,那么當(dāng)我們視圖從通道中讀取數(shù)據(jù)時就會發(fā)現(xiàn)返回值是-1,表示已經(jīng)到達文件結(jié)尾了。 如果把當(dāng)前位置設(shè)置為文件結(jié)尾之后,再向通道中寫入數(shù)據(jù),文件會自動擴展以便寫入數(shù)據(jù),但是這樣會導(dǎo)致文件中出現(xiàn)類似空洞,即文件的一些位置是沒有數(shù)據(jù)的。
FileChannel Size
size()方法可以返回FileChannel對應(yīng)的文件的文件大?。?/p>
long fileSize = channel.size();
FileChannel Truncate
利用truncate方法可以截取指定長度的文件:
FileChannel truncateFile = fileChannel.truncate(1024);
FileChannel Force
force方法會把所有未寫磁盤的數(shù)據(jù)都強制寫入磁盤。這是因為在操作系統(tǒng)中出于性能考慮回把數(shù)據(jù)放入緩沖區(qū),所以不能保證數(shù)據(jù)在調(diào)用write寫入文件通道后就及時寫到磁盤上了,除非手動調(diào)用force方法。 force方法需要一個布爾參數(shù),代表是否把meta data也一并強制寫入。
channel.force(true);
NIO SocketChannel套接字通道
在Java NIO體系中,SocketChannel是用于TCP網(wǎng)絡(luò)連接的套接字接口,相當(dāng)于Java網(wǎng)絡(luò)編程中的Socket套接字接口。創(chuàng)建SocketChannel主要有兩種方式,如下:
- 打開一個SocketChannel并連接網(wǎng)絡(luò)上的一臺服務(wù)器。
- 當(dāng)ServerSocketChannel接收到一個連接請求時,會創(chuàng)建一個SocketChannel。
建立一個SocketChannel連接
打開一個SocketChannel可以這樣操作:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
關(guān)閉一個SocketChannel連接
關(guān)閉一個SocketChannel只需要調(diào)用他的close方法,如下:
socketChannel.close();
從SocketChannel中讀數(shù)據(jù)
從一個SocketChannel連接中讀取數(shù)據(jù),可以通過read()方法,如下:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
首先需要開辟一個Buffer。從SocketChannel中讀取的數(shù)據(jù)將放到Buffer中。
接下來就是調(diào)用SocketChannel的read()方法.這個read()會把通道中的數(shù)據(jù)讀到Buffer中。read()方法的返回值是一個int數(shù)據(jù),代表此次有多少字節(jié)的數(shù)據(jù)被寫入了Buffer中。如果返回的是-1,那么意味著通道內(nèi)的數(shù)據(jù)已經(jīng)讀取完畢,到底了(鏈接關(guān)閉)。
向SocketChannel寫數(shù)據(jù)
向SocketChannel中寫入數(shù)據(jù)是通過write()方法,write也需要一個Buffer作為參數(shù)。下面看一下具體的示例:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
非阻塞模式
我們可以把SocketChannel設(shè)置為non-blocking(非阻塞)模式。這樣的話在調(diào)用connect(), read(), write()時都是異步的。
socketChannel.configureBlocking(false);
connect()
如果我們設(shè)置了一個SocketChannel是非阻塞的,那么調(diào)用connect()后,方法會在鏈接建立前就直接返回。為了檢查當(dāng)前鏈接是否建立成功,我們可以調(diào)用finishConnect(),如下:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
write()
在非阻塞模式下,調(diào)用write()方法不能確保方法返回后寫入操作一定得到了執(zhí)行。因此我們需要把write()調(diào)用放到循環(huán)內(nèi)。這和前面在講write()時是一樣的,此處就不在代碼演示。
read()
在非阻塞模式下,調(diào)用read()方法也不能確保方法返回后,確實讀到了數(shù)據(jù)。因此我們需要自己檢查的整型返回值,這個返回值會告訴我們實際讀取了多少字節(jié)的數(shù)據(jù)。
Selector結(jié)合非阻塞模式
SocketChannel的非阻塞模式可以和Selector很好的協(xié)同工作。把一個或多個SocketChannel注冊到一個Selector后,我們可以通過Selector指導(dǎo)哪些channels通道是處于可讀,可寫等等狀態(tài)的。
NIO ServerSocketChannel服務(wù)端套接字通道
在Java NIO中,ServerSocketChannel是用于監(jiān)聽TCP鏈接請求的通道,正如Java網(wǎng)絡(luò)編程中的ServerSocket一樣。
ServerSocketChannel實現(xiàn)類位于java.nio.channels包下面。
void test() throws IOException {
//打開一個ServerSocketChannel我們需要調(diào)用他的open()方法
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
if (socketChannel.isConnected()) {
break;
}
}
//關(guān)閉一個ServerSocketChannel我們需要調(diào)用close()方法
serverSocketChannel.close();
}
監(jiān)聽鏈接
通過調(diào)用accept()方法,我們就開始監(jiān)聽端口上的請求連接。當(dāng)accept()返回時,他會返回一個SocketChannel連接實例,實際上accept()是阻塞操作,他會阻塞帶去線程知道返回一個連接; 很多時候我們是不滿足于監(jiān)聽一個連接的,因此我們會把accept()的調(diào)用放到循環(huán)中,就像這樣:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
當(dāng)然我們可以在循環(huán)體內(nèi)加上合適的中斷邏輯,而不是單純的在while循環(huán)中寫true,以此來結(jié)束循環(huán)監(jiān)聽;
非阻塞模式
實際上ServerSocketChannel是可以設(shè)置為非阻塞模式的。在非阻塞模式下,調(diào)用accept()函數(shù)會立刻返回,如果當(dāng)前沒有請求的鏈接,那么返回值為空null。因此我們需要手動檢查返回的SocketChannel是否為空,例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
//設(shè)置為非阻塞模式
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();