Java核心技術讀書筆記11-3 Java NIO介紹與核心功能概述
1.什麼是NIO?
Java NIO(New IO)在Java 1.4被引入,該API可以實現與傳統IO完全不同的機制。對於NIO來說,其主要提供了新的對網路的多路複用IO技術。在面向網路的資料傳輸中,如果含有連線數很多而每次傳輸的資料量不是很大,那麼這個場景會更適合使用NIO技術。
注意,Java NIO的NIO不是non blocking I/O(非阻塞IO)的縮寫。一來,並非其所有讀寫時執行緒都是非阻塞的。二來,非阻塞IO需要持續佔用處理機進行輪詢,不適合於併發情況,Java NIO使用的是多路複用技術。
2.傳統IO與NIO的區別
首先,在底層,傳統I/O是面向流的,每次讀寫一個或多個位元組。在流中,不可以對資料進行隨機存取。
而對於NIO來說,其傳輸是對緩衝區以塊為單位進行的,使用緩衝區後讀寫位置可以進行移動,傳輸的工具使用的是通道,相比於單向的流,通道可以雙向讀寫。
其次,在IO模型上,傳統I/O是同步阻塞IO模型,而NIO使用的是多路複用技術,關於IO底層原理和IO模型可以看:
另外,NIO可以僅使用一個執行緒管理多個讀寫資料的通道,IO多路複用最大的優勢就是系統開銷小。
最後,使用傳統的IO通過組合流可以使得對檔案內容複雜處理需求會更容易,但NIO包含的一些API也支援更方便的處理整個檔案。
3.NIO核心元件概述
NIO的三個核心元件:Selector、Channel與Buffer。
3.1 Selector
在瞭解了什麼是多路複用之後,我們知道實現該機制必須有一個能跟蹤所有Socket狀態的元件,在NIO中,這個元件就是Selector。
3.2 Channel
另外在NIO中,讀寫資料必須經過一個通道,也就是Channel。資料經過Channel進行傳輸,它有些像流,但通道是雙向的,也支援NIO的非阻塞讀寫。
通道的一些實現類:
· FileChannel 從檔案中讀寫資料
· DatagramChannel 通過UDP讀寫資料
· SocketChannel 通過TCP讀寫資料
· ServerSocketChannel 監聽TCP連線,對每個TCP連線提供一個SocketChannel
建立不同的Channel將通道連線至硬碟檔案或者網路。
3.3 Buffer
Buffer是在記憶體中的一段區域用作使用者程序讀寫的緩衝區。使用Channel時,要麼會將資料讀入到Buffer(寫操作),要麼從Buffer中寫出資料(讀操作)。
因此對於緩衝區的讀入操作可以呼叫Channel物件的read方法從Channel中讀入資料,也可以呼叫緩衝區的put方法直接在程式中向其放入資料。
而對於緩衝區的寫出操作可以呼叫Channel物件的write方法將緩衝區的資料寫出到Channel,也可以呼叫緩衝區的get方法直接返回緩衝區的資料。
Buffer的一些實現類:
· ByteBuffer
· CharBuffer
· DoubleBuffer
· FloatBuffer
· IntBuffer
· LongBuffer
· ShortBuffer
覆蓋了除了boolean型別外的基本資料型別,此外還存在一個MappedByteBuffer表示記憶體示例檔案。
緩衝區的capacity、position與limit
capacity:其值都表示在構造緩衝區時分配的容量,根據緩衝區的不同表示容量為多少個Byte、多少個Char等等。如LongBuffer.allocate(8)表示分配了8個Long的大小。
position:其值表示當前對緩衝區的操作位置,position對緩衝區相當於以下標表示,範圍為0~capacity-1。初始化、呼叫flip方法、clear方法都會將其置為0。呼叫compact方法可以清空已讀取的資料,然後使所有未讀取的資料前移填充空隙,並置position為最後一個元素的下標+1。每次使用read或put方法將元素放入到緩衝區都是從position指示的下標開始,元素放置完畢後position+1。每次使用get方法會返回當前position位置的元素並使得position+1。
此外,使用mark/reset方法也可標記position位置以及將position置為標記。
注意每個元素單元是一個緩衝區型別,這與陣列類似,例如charBuffer的每個元素單元是一個char,position後移代表向後移動一個單元。
limit:其值表示當前緩衝區內有多少元素可讀。在初始化、呼叫clear、compact、rewind方法後會將limit置為capacity,只有呼叫flip方法才會將limit置為呼叫前的position位置。所以當緩衝區讀入資料完畢,希望從緩衝區寫出資料時(無論是使用get或者write)最好使用flip方法,使緩衝區做好寫出準備。
一段包含Channel與Buffer的基本程式碼示例:
public static void main(String[] args) throws IOException {
//檔案內容為:aaaaaaaa\nbbbb 檔案共有八個位元組長度的a加一個位元組換行符\n以及四個位元組的b共十三個位元組
RandomAccessFile raf = new RandomAccessFile(new File("file/Test.txt"), "rw"); //建立一個隨機訪問檔案
FileChannel inChannel = raf.getChannel(); //構建一個連通檔案的Channel
ByteBuffer buf = ByteBuffer.allocate(8); //構建一個位元組緩衝區,緩衝區大小為8位元組
int read = inChannel.read(buf); //緩衝區從通道中讀取資料,讀取位元組數最大為緩衝區大小,不足則為實際位元組數,該方法返回讀入位元組的個數
while (read != -1){ //當緩衝區存在讀入資料時
System.out.println("讀取到位元組數:" + read); //緩衝區大小為8,會讀取兩次,第一次為輸出8 第二次輸出5
buf.flip(); //使緩衝區準備寫出資料,置limit = position,position=0
//該方法返回的值為position < limit,顯然,返回true代表當前position位置仍有元素可讀
//剩餘的元素個數即為 limit - position
while (buf.hasRemaining()){
System.out.print((char)buf.get()); //輸出position指向的位元組
}
System.out.println();
buf.clear(); //清空緩衝區,position = 0, limit = capacity
read = inChannel.read(buf); //再次試著讀入
}
raf.close();
}
Butter的equals與compareTo方法
equals方法比較兩個緩衝區中範圍為[position,limit)的元素,要求必須個數,型別與值全部相同才返回true。該方法由Object方法重寫
compareTo方法,同樣比較兩個緩衝區[position,limit)範圍的元素。該方法依次遍歷兩個緩衝區範圍內的元素並呼叫Byte.compare(byte a, byte b)方法進行比較。其中a為呼叫者當前遍歷的元素,b為引數緩衝區的當前遍歷的元素。若兩個元素相同,則比較下一個,否則直接返回a - b。若比較到最後發現兩個緩衝區長度不匹配則返回兩個緩衝區範圍的長度之差,若兩個緩衝區大小、元素完全相同,則最後返回0。該方法為Comparable介面實現方法。
Channel對多個Butter的讀寫
Scattering Reads
使用Channel物件的read方法可以將Butter陣列作為引數,然後把Channel中的資料讀入到這些緩衝區中,當陣列中第一個緩衝區讀滿後才可讀入第二個緩衝區,以此類推。該方法不適合與動態訊息而是適合定長的資料,例如固定長度的訊息頭和訊息體就可以分別使用相應長度緩衝區來接收。
Gathering Writes
使用Channel物件的write方法可以也將Butter陣列作為引數,相反地,該方法可以按順序將陣列中Buffer在[position,limit)範圍的資料寫出到Channel中。
示例程式碼如下:
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile(new File("file/Test.txt"), "rw"); //建立一個隨機訪問檔案
FileChannel inChannel = raf.getChannel(); //構建一個連通檔案的Channel
ByteBuffer b1 = ByteBuffer.allocate(8);
ByteBuffer b2 = ByteBuffer.allocate(5);
ByteBuffer[] byteBuffers = {b1, b2};
//Channel資料讀入到兩個緩衝區中
inChannel.read(byteBuffers);
printBufByte(b1, "b1");
printBufByte(b2, "b2");
//從兩個緩衝區中寫出資料到Channel中
b1.put((byte)'b');
b1.put((byte)'1');
b2.put((byte)'b');
b2.put((byte)'2');
b1.flip(); //更新指標
b2.flip();
inChannel.write(byteBuffers);
inChannel.write(b1);
}
通道間的資料傳輸
如果現在有一個FileChannel,那麼可以使用這個Channel直接與其它Channel進行讀寫資料,也就是說可以直接將該FileChannel連線的檔案內容寫出到別的Channel連線的源中,或反過來。使用的方法為transerFrom與transerTo。
public static void main(String[] args) throws IOException {
RandomAccessFile raf1 = new RandomAccessFile(new File("file/Test.txt"), "rw"); //建立一個隨機訪問檔案
RandomAccessFile raf2 = new RandomAccessFile(new File("file/Test2.txt"), "rw"); //建立一個隨機訪問檔案
FileChannel inChannel1 = raf1.getChannel(); //構建一個連通檔案的Channel
FileChannel inChannel2 = raf2.getChannel(); //代表另一個Channel,實際上該Channel可以是任何其它的Channel
//inChannel1向inChannel2中傳送資料
inChannel1.transferTo(1, inChannel1.size(), inChannel2); //第一個引數代表從inChannel1何處開始傳送,此處為從第一個位元組,第二個引數為最多傳輸多少個位元組
//inChannel1從inChannel2中接收傳送的資料
inChannel1.transferFrom(inChannel2, 0, inChannel2.size()); //引數含義與transerTo相同
}