簡介
Java NIO 是由 Java 1.4 引進的非同步 IO.
Java NIO 由以下幾個核心部分組成:
- Channel
- Buffer
- Selector NIO 和 IO 的對比
IO 和 NIO 的區別主要體現在三個方面: IO 基於流(Stream oriented), 而 NIO 基於 Buffer (Buffer oriented) IO 操作是阻塞的, 而 NIO 操作是非阻塞的 IO 沒有 selector 概念, 而 NIO 有 selector 概念. 基於 Stream 與基於 Buffer
傳統的 IO 是面向位元組流或字元流的, 而在 NIO 中, 我們拋棄了傳統的 IO 流, 而是引入了 Channel 和 Buffer 的概念. 在 NIO 中, 我只能從 Channel 中讀取資料到 Buffer 中或將資料從 Buffer 中寫入到 Channel.
那麼什麼是 基於流 呢? 在一般的 Java IO 操作中, 我們以流式的方式順序地從一個 Stream 中讀取一個或多個位元組, 因此我們也就不能隨意改變讀取指標的位置.
而 基於 Buffer 就顯得有點不同了. 我們首先需要從 Channel 中讀取資料到 Buffer 中, 當 Buffer 中有資料後, 我們就可以對這些資料進行操作了. 不像 IO 那樣是順序操作, NIO 中我們可以隨意地讀取任意位置的資料. 阻塞和非阻塞
Java 提供的各種 Stream 操作都是阻塞的, 例如我們調用一個 read 方法讀取一個檔案的內容, 那麼調用 read 的線程會被阻塞住, 直到 read 操作完成.
而 NIO 的非阻塞模式允許我們非阻塞地進行 IO 操作. 例如我們需要從網路中讀取資料, 在 NIO 的非阻塞模式中, 當我們調用 read 方法時, 如果此時有資料, 則 read 讀取並返回; 如果此時沒有資料, 則 read 直接返回, 而不會阻塞當前線程. selector
selector 是 NIO 中才有的概念, 它是 Java NIO 之所以可以非阻塞地進行 IO 操作的關鍵.
通過 Selector, 一個線程可以監聽多個 Channel 的 IO 事件, 當我們向一個 Selector 中註冊了 Channel 後, Selector 內部的機制就可以自動地為我們不斷地查詢(select) 這些註冊的 Channel 是否有已就緒的 IO 事件(例如可讀, 可寫, 網路連接完成等). 通過這樣的 Selector 機制, 我們就可以很簡單地使用一個線程高效地管理多個 Channel 了. Java NIO Channel
通常來說, 所有的 NIO 的 I/O 操作都是從 Channel 開始的. 一個 channel 類似於一個 stream.
java Stream 和 NIO Channel 對比 我們可以在同一個 Channel 中執行讀和寫操作, 然而同一個 Stream 僅僅支援讀或寫. Channel 可以非同步地讀寫, 而 Stream 是阻塞的同步讀寫. Channel 總是從 Buffer 中讀取資料, 或將資料寫入到 Buffer 中.
Channel 類型有: FileChannel, 檔案操作 DatagramChannel, UDP 操作 SocketChannel, TCP 操作 ServerSocketChannel, TCP 操作, 使用在伺服器端.
這些通道涵蓋了 UDP 和 TCP網路 IO以及檔案 IO.
基本的 Channel 使用例子:
public static void main( String[] args ) throws Exception{ RandomAccessFile aFile = new RandomAccessFile("/Users/xiongyongshun/settings.xml", "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); while (bytesRead != -1) { buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get()); } buf.clear(); bytesRead = inChannel.read(buf); } aFile.close();}
FileChannel
FileChannel 是操作檔案的Channel, 我們可以通過 FileChannel 從一個檔案中讀取資料, 也可以將資料寫入到檔案中.
注意, FileChannel 不能設定為非阻塞模式. 開啟 FileChannel
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");FileChannel inChannel = aFile.getChannel();
從 FileChannel 中讀取資料
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buf);
寫入資料
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);}
關閉
當我們對 FileChannel 的操作完成後, 必須將其關閉
channel.close();
設定 position
long pos channel.position();channel.position(pos +123);
檔案大小
我們可以通過 channel.size()擷取關聯到這個 Channel 中的檔案的大小. 注意, 這裡返回的是檔案的大小, 而不是 Channel 中剩餘的元素個數. 截斷檔案
channel.truncate(1024);
將檔案的大小截斷為1024位元組. 強制寫入
我們可以強制將緩衝的未寫入的資料寫入到檔案中:
channel.force(true);
SocketChannel
SocketChannel 是一個用戶端用來進行 TCP 串連的 Channel.
建立一個 SocketChannel 的方法有兩種: 開啟一個 SocketChannel, 然後將其串連到某個伺服器中 當一個 ServerSocketChannel 接受到串連請求時, 會返回一個 SocketChannel 對象. 開啟 SocketChannel
SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress("http://example.com", 80));
關閉
socketChannel.close();
讀取資料
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = socketChannel.read(buf);
如果 read()返回 -1, 那麼表示串連中斷了. 寫入資料
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 為非同步模式, 這樣我們的 connect, read, write 都是非同步了. 串連
socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress("http://example.com", 80));while(! socketChannel.finishConnect() ){ //wait, or do something else... }
在非同步模式中, 或許串連還沒有建立, connect 方法就返回了, 因此我們需要檢查當前是否是串連到了主機, 因此通過一個 while 迴圈來判斷. 讀寫
在非同步模式下, 讀寫的方式是一樣的.
在讀取時, 因為是非同步, 因此我們必須檢查 read 的傳回值, 來判斷當前是否讀取到了資料. ServerSocketChannel
ServerSocketChannel 顧名思義, 是用在伺服器為端的, 可以監聽用戶端的 TCP 串連, 例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(9999));while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel...}
開啟 關閉
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.close();
監聽串連
我們可以使用ServerSocketChannel.accept()方法來監聽用戶端的 TCP 連線要求, accept()方法會阻塞, 直到有串連到來, 當有串連時, 這個方法會返回一個 SocketChannel 對象:
while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel...}
非阻塞模式
在非阻塞模式下, accept()是非阻塞的, 因此如果此時沒有串連到來, 那麼 accept()方法會返回null:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(9999));serverSocketChannel.configureBlocking(false);while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ //do something with socketChannel... }}
DatagramChannel
DatagramChannel 是用來處理 UDP 串連的. 開啟
DatagramChannel channel = DatagramChannel.open();channel.socket().bind(new InetSocketAddress(9999));
讀取資料
ByteBuffer buf = ByteBuffer.allocate(48);buf.clear();channel.receive(buf);
發送資料
String newData = "New String to write to file..." + System.currentTimeMillis();ByteBuffer buf = ByteBuffer.allocate(48);buf.clear();buf.put(newData.getBytes());buf.flip();int bytesSent = channel.send(buf, new InetSocketAddress("example.com", 80));
串連到指定地址
因為 UDP 是非串連的, 因此這個的 connect 並不是向 TCP 一樣真正意義上的串連, 而是它會講 DatagramChannel 鎖住, 因此我們僅僅可以從指定的地址中讀取或寫入資料.
channel.connect(new InetSocketAddress("example.com", 80));
Java NIO Buffer
當我們需要與 NIO Channel 進行互動時, 我們就需要使用到 NIO Buffer, 即資料從 Buffer讀取到 Channel 中, 並且從 Channel 中寫入到 Buffer 中.
實際上, 一個 Buffer 其實就是一塊記憶體地區, 我們可以在這個記憶體地區中進行資料的讀寫. NIO Buffer 其實是這樣的記憶體塊的一個封裝, 並提供了一些操作方法讓我們能夠方便地進行資料的讀寫.
Buffer 類型有: ByteBuffer CharBuffer DoubleBuffer FloatBuffer IntBuffer LongBuffer ShortBuffer
這些 Buffer 覆蓋了能從 IO 中傳輸的所有的 Java 基礎資料型別 (Elementary Data Type). NIO Buffer 的基本使用
使用 NIO Buffer 的步驟如下: 將資料寫入到 Buffer 中. 調用 Buffer.flip()方法, 將 NIO Buffer 轉換為讀模式. 從 Buffer 中讀取資料 調用 Buffer.clear() 或 Buffer.compact()方法, 將 Buffer 轉換為寫入模式.
當我們將資料寫入到 Buffer 中時, Buffer 會記錄我們已經寫了多少的資料, 當我們需要從 Buffer 中讀取資料時, 必須調用 Buffer.flip()將 Buffer 切換為讀模式.
一旦讀取了所有的 Buffer 資料, 那麼我們必須清理 Buffer, 讓其從新可寫, 清理 Buffer 可以調用 Buffer.clear() 或 Buffer.compact().
例如:
public class Test { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(2); intBuffer.put(12345678); intBuffer.put(2); intBuffer.flip(); System.err.println(intBuffer.get()); System.err.println(intBuffer.get()); }}
上述中, 我們分配兩個單位大小的 IntBuffer, 因此它可以寫入兩個 int 值.
我們使用 put 方法將 int 值寫入, 然後使用 flip 方法將 buffer 轉換為讀模式, 然後連續使用 get 方法從 buffer 中擷取這兩個 int 值.
每當調用一次 get 方法讀取資料時, buffer 的讀指標都會向前移動一個單位長度(在這裡是一個 int 長度) Buffer 屬性
一個 Buffer 有三個屬性: capacity position limit
其中 position 和 limit 的含義與 Buffer 處於讀模式或寫入模式有關, 而 capacity 的含義與 Buffer 所處的模式無關. Capacity
一個記憶體塊會有一個固定的大小, 即容量(capacity), 我們最多寫入capacity 個單位的資料到 Buffer 中, 例如一個 DoubleBuffer, 其 Capacity 是100, 那麼我們最多可以寫入100個 double 資料. Position
當從一個 Buffer 中寫入資料時, 我們是從 Buffer 的一個確定的位置(position)開始寫入的. 在最初的狀態時, position 的值是0. 每當我們寫入了一個單位的資料後, position 就會遞增一.
當我們從 Buffer 中讀取資料時, 我們也是從某個特定的位置開始讀取的. 當我們調用了 filp()方法將 Buffer 從寫入模式轉換到讀模式時, position 的值會自動被設定為0, 每當我們讀取一個單位的資料, position 的值遞增1.
position 表示了讀寫操作的位置指標. limit
limit - position 表示此時還可以寫入/讀取多少單位的資料.
例如在寫入模式, 如果此時 limit 是10, position 是2, 則表示已經寫入了2個單位的資料, 還可以寫入 10 - 2 = 8 個單位的資料. 例子:
public class Test { public static void main(String args[]) { IntBuffer intBuffer = IntBuffer.allocate(10); intBuffer.put(10); intBuffer.put(101); System.err.println("Write mode: "); System.err.println("\tCapacity: " + intBuffer.capacity()); System.err.println("\tPosition: " + intBuffer.position()); System.err.println("\tLimit: " + intBuffer.limit()); intBuffer.flip(); System.err.println("Read mode: "); System.err.println("\tCapacity: " + intBuffer.capacity()); System.err.println("\tPosition: " + intBuffer.position()); System.err.println("\tLimit: " + intBuffer.limit()); }}
這裡我們首先寫入兩個 int 值, 此時 capacity = 10, position = 2, limit = 10.
然後我們調用 flip 轉換為讀模式, 此時 capacity = 10, position = 0, limit = 2; 分配 Buffer
為了擷取一個 Buffer 對象, 我們首先需要分配記憶體空間. 每個類型的 Buffer 都有一個 allocate()方法, 我們可以通過這個方法分配 Buffer:
ByteBuffer buf = ByteBuffer.allocate(48);
這裡我們分配了48 * sizeof(Byte)位元組的記憶體空間.
CharBuffer buf = CharBuffer.allocate(1024);
這裡我們分配了大小為1024個字元的 Buffer, 即 這個 Buffer 可以儲存1024 個 Char, 其大小為 1024 * 2 個位元組. 關於 Direct Buffer 和 Non-Direct Buffer 的區別
Direct Buffer: 所分配的記憶體不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 對象是由 GC 管理的, 因此當發生 GC, 對象被回收時, Direct Buffer 也會被釋放) 因為 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 對應用程式的記憶體佔用的影響就不那麼明顯(實際上還是佔用了這麼多記憶體, 但是 JVM 不好統計到非 JVM 管理的記憶體.) 申請和釋放 Direct Buffer 的開銷比較大. 因此正確的使用 Direct Buffer 的方式是在初始化時申請一個 Buffer, 然後不斷複用此 buffer, 在程式結束後才釋放此 buffer. 使用 Direct Buffer 時, 當進行一些底層的系統 IO 操作時, 效率會比較高, 因為此時 JVM 不需要拷貝 buffer 中的記憶體到中間臨時緩衝區中.
Non-Direct Buffer: 直接在 JVM 堆上進行記憶體的分配, 本質上是 byte[] 數組的封裝. 因為 Non-Direct Buffer 在 JVM 堆中, 因此當進行作業系統底層 IO 操作中時, 會將此 buffer 的記憶體複製到中間臨時緩衝區中. 因此 Non-Direct Buffer 的效率就較低. 寫入資料到 Buffer
int bytesRead = inChannel.read(buf); //read into buffer.buf.put(127);
從 Buffer 中讀取資料
//read from buffer into channel.int bytesWritten = inChannel.write(buf);byte aByte = buf.get();
重設 position
Buffer.rewind()方法可以重設 position 的值為0, 因此我們可以重新讀取/寫入 Buffer 了.
如果是讀模式, 則重設的是讀模式的 position, 如果是寫入模式, 則重設的是寫入模式的 position.
例如:
public class Test { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(2); intBuffer.put(1); intBuffer.put(2); System.err.println("position: " + intBuffer.position()); intBuffer.rewind(); System.err.println("position: " + intBuffer.position()); intBuffer.put(1); intBuffer.put(2); System.err.println("position: " + intBuffer.position()); intBuffer.flip(); System.err.println("position: " + intBuffer.position()); intBuffer.get(); intBuffer.get(); System.err.println("position: " + intBuffer.position()); intBuffer.rewind(); System.err.println("position: " + intBuffer.position()); }}
rewind() 主要針對於讀模式. 在讀模式時, 讀取到 limit 後, 可以調用 rewind() 方法, 將讀 position 置為0. 關於 mark()和 reset()
我們可以通過調用 Buffer.mark()將當前的 position 的值儲存起來, 隨後可以通過調用 Buffer.reset()方法將 position 的值回複回來.
例如:
public class Test { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(2); intBuffer.put(1); intBuffer.put(2); intBuffer.flip(); System.err.println(intBuffer.get()); System.err.println("position: " + intBuffer.position()); intBuffer.mark(); System.err.println(intBuffer.get()); System.err.println("position: " + intBuffer.position()); intBuffer.reset(); System.err.println("position: " + intBuffer.position()); System.err.println(intBuffer.get()); }}
這裡我們寫入兩個 int 值, 然後首先讀取了一個值. 此時讀 position 的值為1.
接著我們調用 mark() 方法將當前的 position 儲存起來(在讀模式, 因此儲存的是讀的 position), 然後再次讀取, 此時 position 就是2了.
接著使用 reset() 恢複原來的讀 position, 因此讀 position 就為1, 可以再次讀取資料. flip, rewind 和 clear 的區別 flip
方法源碼:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this;}
Buffer 的讀/寫入模式共用一個 position 和 limit 變數.
當從寫入模式變為讀模式時, 原先的 寫 position 就變成了讀模式的 limit. rewind
方法源碼
public final Buffer rewind() { position = 0; mark = -1; return this;}
rewind, 即倒帶, 這個方法僅僅是將 position 置為0. clear
方法源碼:
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this;}
根據源碼我們可以知道, clear 將 positin 設定為0, 將 limit 設定為 capacity.
clear 方法使用情境:
- 在一個已經寫滿資料的 buffer 中, 調用 clear, 可以從頭讀取 buffer 的資料.
- 為了將一個 buffer 填充滿資料, 可以調用 clear, 然後一直寫入, 直到達到 limit. 例子:
IntBuffer intBuffer = IntBuffer.allocate(2);intBuffer.flip();System.err.println("position: " + intBuffer.position());System.err.println("limit: " + intBuffer.limit());System.err.println("capacity: " + intBuffer.capacity());// 這裡不能讀, 因為 limit == position == 0, 沒有資料.//System.err.println(intBuffer.get());intBuffer.clear();System.err.println("position: " + intBuffer.position());System.err.println("limit: " + intBuffer.limit());System.err.println("capacity: " + intBuffer.capacity());// 這裡可以讀取資料了, 因為 clear 後, limit == capacity == 2, position == 0,// 即使我們沒有寫入任何的資料到 buffer 中.System.err.println(intBuffer.get()); // 讀取到0System.err.println(intBuffer.get()); // 讀取到0
Buffer 的比較
我們可以通過 equals() 或 compareTo() 方法比較兩個 Buffer, 若且唯若如下條件滿足時, 兩個 Buffer 是相等的: 兩個 Buffer 是相同類型的 兩個 Buffer 的剩餘的資料個數是相同的 兩個 Buffer 的剩餘的資料都是相同的.
通過上述條件我們可以發現, 比較兩個 Buffer 時, 並不是 Buffer 中的每個元素都進行比較, 而是比較 Buffer 中剩餘的元素. Selector
Selector 允許一個單一的線程來操作多個 Channel. 如果我們的應用程式中使用了多個 Channel, 那麼使用 Selector 很方便的實現這樣的目的, 但是因為在一個線程中使用了多個 Channel, 因此也會造成了每個 Channel 傳輸效率的降低.
使用 Selector 的圖解如下:
為了使用 Selector, 我們首先需要將 Channel 註冊到 Selector 中, 隨後調用 Selector 的 select()方法, 這個方法會阻塞, 直到註冊在 Selector