目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO (來自 Java 1.4)可以替代標準 IO 和 Java Networking API ,NIO 提供了與標準 IO 不同的使用方式。學習 NIO 之前建議先掌握標準 IO 和 Java 網絡編程,推薦教程:
本文目的: 掌握了標準 IO 之後繼續學習 NIO 知識。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程 Java NIO Tutorial
Java NIO 概覽
NIO 由以下核心組件組成:
-
通道和緩衝區
在標準 IO API 中,使用字節流和字符流。 在 NIO 中使用通道和緩衝區。 數據總是從通道讀入緩衝區,或從緩衝區寫入通道。 -
非阻塞IO
NIO 可以執行非阻塞 IO 。 例如,當通道將數據讀入緩衝區時,線程可以執行其他操作。 並且一旦數據被讀入緩衝區,線程就可以繼續處理它。 將數據寫入通道也是如此。 -
選擇器
NIO 包含“選擇器”的概念。 選擇器是一個可以監視多個事件通道的對象(例如:連接打開,數據到達等)。 因此,單個線程可以監視多個通道的數據。
NIO 有比這些更多的類和組件,但在我看來,Channel,Buffer 和 Selector 構成了 API 的核心。 其餘的組件,如 Pipe 和 FileLock ,只是與三個核心組件一起使用的實用程序類。
Channels/Buffers 通道和緩衝區
通常,NIO 中的所有 IO 都以 Channel 開頭,頻道有點像流。 數據可以從 Channel 讀入 Buffer,也可以從 Buffer 寫入 Channel :
有幾種 Channel 和 Buffer ,以下是 NIO 中主要 Channel 實現類的列表,這些通道包括 UDP + TCP 網絡 IO 和文件 IO:
- FileChannel :文件通道
- DatagramChannel :數據報通道
- SocketChannel :套接字通道
- ServerSocketChannel :服務器套接字通道
這些類也有一些有趣的接口,但爲了簡單起見,這裏暫時不提,後續會進行學習的。
以下是 NIO 中的核心 Buffer 實現,其實就是 7 種基本類型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
NIO 還有一個 MappedByteBuffer,它與內存映射文件一起使用,同樣這個後續再講。
Selectors 選擇器
選擇器允許單個線程處理多個通道。 如果程序打開了許多連接(通道),但每個連接只有較低的流量,使用選擇器就很方便。 例如,在聊天服務器中, 以下是使用 Selector 處理 3 個 Channel 的線程圖示:
要使用選擇器,需要使用它註冊通道。 然後你調用它的 select() 方法。 此方法將阻塞,直到有一個已註冊通道的事件準備就緒。 一旦該方法返回,該線程就可以處理這些事件。 事件可以是傳入連接,接收數據等。
Channel (通道)
NIO 通道類似於流,但有一些區別:
- 通道可以讀取和寫入。 流通常是單向的(讀或寫)。
- 通道可以異步讀取和寫入。
- 通道始終讀取或寫入緩衝區,即它只面向緩衝區。
如上所述,NIO 中總是將數據從通道讀取到緩衝區,或將數據從緩衝區寫入通道。 這是一個例子:
// 文件內容是 123456789
RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);
int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數
Buffer(緩衝區)
使用 Buffer 與 Channel 交互,數據從通道讀入緩衝區,或從緩衝區寫入通道。
緩衝區本質上是一個可以寫入數據的內存塊,之後可以讀取數據。 Buffer 對象包裝了此內存塊,提供了一組方法,可以更輕鬆地使用內存塊。
Buffer 的基本用法
使用 Buffer 讀取和寫入數據通常遵循以下四個步驟:
- 將數據寫入緩衝區
- 調用 buffer.flip() 反轉讀寫模式
- 從緩衝區讀取數據
- 調用 buffer.clear() 或 buffer.compact() 清除緩衝區內容
將數據寫入Buffer 時,Buffer 會跟蹤寫入的數據量。 當需要讀取數據時,就使用 flip() 方法將緩衝區從寫入模式切換到讀取模式。 在讀取模式下,緩衝區允許讀取寫入緩衝區的所有數據。
讀完所有數據之後,就需要清除緩衝區,以便再次寫入。 可以通過兩種方式執行此操作:通過調用 clear() 或調用 compact() 。區別在於 clear() 是方法清除整個緩衝區,而 compact() 方法僅清除已讀取的數據,未讀數據都會移動到緩衝區的開頭,新數據將在未讀數據之後寫入緩衝區。
這是一個簡單的緩衝區用法示例:
public class ChannelExample {
public static void main(String[] args) throws IOException {
// 文件內容是 123456789
RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48); //創建容量爲48字節的緩衝區
int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數
while (data != -1) {
System.out.println("Read " + data); // Read 9
buffer.flip(); // 將 buffer 從寫入模式切換爲讀取模式
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get()); // 每次讀取1byte,循環輸出 123456789
}
buffer.clear(); // 清除當前緩衝區
data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區
}
accessFile.close();
}
}
Buffer 的 capacity,position 和 limit
緩衝區有 3 個需要熟悉的屬性,以便了解緩衝區的工作原理。 這些是:
- capacity : 容量緩衝區的容量,是它所包含的元素的數量。不能爲負並且不能更改。
- position :緩衝區的位置 是下一個要讀取或寫入的元素的索引。不能爲負,並且不能大於 limit
- limit : 緩衝區的限制,緩衝區的限制不能爲負,並且不能大於 capacity
另外還有標記 mark ,
標記、位置、限制和容量值遵守以下不變式:
0 <= mark<= position <= limit<= capacity
position 和 limit 的含義取決於 Buffer 是處於讀取還是寫入模式。 無論緩衝模式如何,capacity 總是一樣的表示容量。
以下是寫入和讀取模式下的容量,位置和限制的說明:
capacity
作爲存儲器塊,緩衝區具有一定的固定大小,也稱爲“容量”。 只能將 capacity 多的 byte,long,char 等寫入緩衝區。 緩衝區已滿後,需要清空它(讀取數據或清除它),然後才能將更多數據寫入。
position
將數據寫入緩衝區時,可以在某個位置執行操作。 position 初始值爲 0 ,當一個 byte,long,char 等已寫入緩衝區時,position 被移動,指向緩衝區中的下一個單元以插入數據。 position 最大值爲 capacity -1
從緩衝區讀取數據時,也可以從給定位置開始讀取數據。 當緩衝區從寫入模式切換到讀取模式時,position 將重置爲 0 。當從緩衝區讀取數據時,將從 position 位置開始讀取數據,讀取後會將 position 移動到下一個要讀取的位置。
limit
在寫入模式下,Buffer 的 limit 是可以寫入緩衝區的數據量的限制,此時 limit=capacity。
將緩衝區切換爲讀取模式時,limit 表示最多能讀到多少數據。 因此,當將 Buffer 切換到讀取模式時,limit被設置爲之前寫入模式的寫入位置(position ),換句話說,你能讀到之前寫入的所有數據(例如之前寫寫入了 6 個字節,此時 position=6 ,然後切換到讀取模式,limit 代表最多能讀取的字節數,因此 limit 也等於 6)。
分配緩衝區
要獲取 Buffer 對象,必須先分配它。 每個 Buffer 類都有一個 allocate() 方法來執行此操作。 下面是一個顯示ByteBuffer分配的示例,容量爲48字節:
ByteBuffer buffer = ByteBuffer.allocate(48); //創建容量爲48字節的緩衝區
將數據寫入緩衝區
可以通過兩種方式將數據寫入 Buffer:
- 將數據從通道寫入緩衝區
- 通過緩衝區的 put() 方法,自己將數據寫入緩衝區。
這是一個示例,顯示了 Channel 如何將數據寫入 Buffer:
int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數
buffer.put(127); // 此處的 127 是 byte 類型
put() 方法有許多其他版本,允許以多種不同方式將數據寫入 Buffer 。 例如,在特定位置寫入,或將一個字節數組寫入緩衝區。
flip() 切換緩衝區的讀寫模式
flip() 方法將 Buffer 從寫入模式切換到讀取模式。 調用 flip() 會將 position 設置回 0,並將 limit 的值設置爲切換之前的 position 值。換句話說,limit 表示之前寫進了多少個 byte、char 等 —— 現在能讀取多少個 byte、char 等。
從緩衝區讀取數據
有兩種方法可以從 Buffer 中讀取數據:
- 將數據從緩衝區讀入通道。
- 使用 get() 方法之一,自己從緩衝區讀取數據。
以下是將緩衝區中的數據讀入通道的示例:
int bytesWritten = fileChannel.write(buffer);
byte aByte = buffer.get();
和 put() 方法一樣,get() 方法也有許多其他版本,允許以多種不同方式從 Buffer 中讀取數據。有關更多詳細信息,請參閱JavaDoc以獲取具體的緩衝區實現。
以下列出 ByteBuffer 類的部分方法:
方法 | 描述 |
---|---|
byte[] array() | 返回實現此緩衝區的 byte 數組,此緩衝區的內容修改將導致返回的數組內容修改,反之亦然。 |
CharBuffer asCharBuffer() | 創建此字節緩衝區作爲新的獨立的char 緩衝區。新緩衝區的內容將從此緩衝區的當前位置開始 |
XxxBuffer asXxxBuffer() | 同上,創建對應的 Xxx 緩衝區,Xxx 可爲 Short/Int/Long/Float/Double |
byte get() | 相對 get 方法。讀取此緩衝區當前位置的字節,然後該 position 遞增。 |
ByteBuffer get(byte[] dst, int offset, int length) | 相對批量 get 方法,後2個參數可省略 |
byte get(int index) | 絕對 get 方法。讀取指定索引處的字節。 |
char getChar() | 用於讀取 char 值的相對 get 方法。 |
char getChar(int index) | 用於讀取 char 值的絕對 get 方法。 |
xxx getXxx(int index) | 用於讀取 xxx 值的絕對 get 方法。index 可以選,指定位置。 |
衆多 put() 方法 | 參考以上 get() 方法 |
static ByteBuffer wrap(byte[] array) | 將 byte 數組包裝到緩衝區中。 |
rewind() 倒帶
Buffer對象的 rewind() 方法將 position 設置回 0,因此可以重讀緩衝區中的所有數據, limit 則保持不變。
clear() 和 compact()
如果調用 clear() ,則將 position 設置回 0 ,並將 limit 被設置成 capacity 的值。換句話說,Buffer 被清空了。 但是 Buffer 中的實際存放的數據並未清除。
如果在調用 clear() 時緩衝區中有任何未讀數據,數據將被“遺忘”,這意味着不再有任何標記告訴讀取了哪些數據,還沒有讀取哪些數據。
如果緩衝區中仍有未讀數據,並且想稍後讀取它,但需要先寫入一些數據,這時候應該調用 compact() ,它會將所有未讀數據複製到 Buffer 的開頭,然後它將 position 設置在最後一個未讀元素之後。 limit 屬性仍設置爲 capacity ,就像 clear() 一樣。 現在緩衝區已準備好寫入,並且不會覆蓋未讀數據。
mark() 和 reset()
以通過調用 Buffer 對象的 mark() 方法在 Buffer 中標記給定位置。 然後,可以通過調用 Buffer.reset() 方法將位置重置回標記位置,就像在標準 IO 中一樣。
buffer.mark();
// 調用 buffer.get() 等方法讀取數據...
buffer.reset(); // 設置 position 回到 mark 位置。
equals() 和 compareTo()
可以使用 equals() 和 compareTo() 比較兩個緩衝區。
equals() 成立的條件:
- 它們的類型相同(byte,char,int等)
- 它們在緩衝區中具有相同數量的剩餘字節,字符等。
- 所有剩餘的字節,字符等都相等。
如上,equals 僅比較緩衝區的一部分,而不是它內部的每個元素。 實際上,它只是比較緩衝區中的其餘元素。
compareTo() 方法比較兩個緩衝區的剩餘元素(字節,字符等), 在下列情況下,一個 Buffer 被視爲“小於”另一個 Buffer:
- 第一個不相等的元素小於另一個 Buffer 中對應的元素 。
- 所有元素都相等,但第一個 Buffer 在第二個 Buffer 之前耗盡了元素(第一個 Buffer 元素較少)。