流是用來讀寫數據的。所有 I/O 都被視爲單個的字節的移動,通過一個稱爲 Stream 的對象一次移動一個字節。流 I/O 用於與外部世界接觸。它也在內部使用,用於將對象轉換爲字節,然後再轉換回對象。
流與與 NIO 最重要的區別是數據打包和傳輸的方式,原來的 I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。
流與塊的比較
- 面向流 的 I/O 系統一次一個字節地處理數據,我們很容易將其包裝爲處理流,完成想要的工作
- 面向塊 的 I/O 系統以塊的形式處理數據。每一個操作都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優雅性和簡單性
一、Buffer
NIO中數據都是從通道讀入緩衝區,從緩衝區寫入到通道中的
緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存
1. NIO中的Buffer有以下實現:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer類型代表了不同的數據類型。換句話說,就是可以通過char,short,int,long,float 或 double類型來操作緩衝區中的字節
最核心的是ByteBuffer,前面的一大串類只是包裝了一下它而已,我們使用最多的通常也是 ByteBuffer,可以將Buffer理解爲一個數組,IntBuffer、CharBuffer、DoubleBuffer 等分別對應 int[]、char[]、double[] 等
2. Buffer的重要屬性:
- position
- limit
- capacity
capacity代表緩衝區的容量,不可更改。比如 capacity 爲 1024 的 IntBuffer,代表其一次可以存放 1024 個 int 類型的值。一旦 Buffer 的容量達到 capacity,需要清空 Buffer,才能重新寫入值
首先,對讀寫操作這個概念先解釋一下,否則可能會混淆。
在系統層面:
- 讀操作,從Channel讀取數據到Buffer
- 寫操作,將數據從Buffer寫入到Channel
對於Buffer而言:
- 讀操作,就是從Buffer起始位置讀取它的數據
- 寫操作,就是向Buffer中寫入或者說填充數據
下面讀寫操作中觀察position和limit是站在Buffer角度而言的
position,代表下一次的寫入位置。初始值是 0,每往 Buffer 中寫入一個值,position 就自動加 1,每讀一個值,position 就自動加 1
從寫操作模式到讀操作模式切換的時候(flip),position 都會歸零
limit,寫操作模式下,limit 代表的是最大能寫入的數據。這個時候 limit 等於 capacity。寫結束後,切換到讀模式,此時的 limit 等於 Buffer 中實際的數據大小,因爲 Buffer 不一定被寫滿了
3. Buffer對象分配
初始化Buffer對象
ByteBuffer byteBuf = ByteBuffer.allocate(1024);
IntBuffer intBuf = IntBuffer.allocate(1024);
向Buffer中寫數據:
- 通過Buffer的put()方法寫到Buffer裏
- 從Channel寫到Buffer
對於put方法,有以下函數定義
// 填充一個 byte 值
public abstract ByteBuffer put(byte b);
// 在指定位置填充一個 int 值
public abstract ByteBuffer put(int index, byte b);
// 將一個數組中的值填充進去
public final ByteBuffer put(byte[] src) {...}
這些方法需要自己控制 Buffer 大小,不能超過 capacity,超過會拋 java.nio.BufferOverflowException 異常
使用Channel寫入數據到Buffer,在系統層面上,這個操作我們稱爲讀操作,因爲數據是從外部(文件或網絡等)讀到內存中
int bytesRead = inChannel.read(buf); //這裏會返回寫入Buffer數據的大小
從Buffer中讀取數據。如果要讀 Buffer 中的值,需要切換模式,從寫入模式切換到讀出模式,flip方法將Buffer從寫模式切換到讀模式,flip()方法會將position設回0,並將limit設置成之前position的值,也就是Buffer中實際數據大小
public final Buffer flip() {
limit = position; // 將 limit 設置爲實際寫入的數據數量
position = 0; // 重置 position 爲 0
mark = -1;
return this;
}
- 使用get()方法從Buffer中讀取數據
- 從Buffer讀取數據到Channel
get方法重載多個,允許以不同的方式從Buffer中讀取數據
// 根據 position 來獲取數據
public abstract byte get();
// 獲取指定位置的數據
public abstract byte get(int index);
// 將 Buffer 中的數據寫入到數組中
public ByteBuffer get(byte[] dst)
byte aByte = buf.get();
例如可以通過 FileChannel 將數據寫入到文件中,通過 SocketChannel 將數據寫入網絡發送到遠程機器等
int bytesWritten = channel.write(buf);
4. 相關重要方法
mark() and reset()
mark 用於臨時保存 position 的值,每次調用 mark() 方法都會將 mark 設值爲當前的 position,便於後續需要的時候使用。後面可以通過調用Buffer.reset()方法恢復到這個position
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
rewind() clear() compact()
rewind(): 會重置 position 爲 0,通常用於重新從頭讀寫 Buffer
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear(): 重置 Buffer,相當於重新實例化,position將被設回0,limit被設置成 capacity的值,注意clear() 方法並不會將 Buffer 中的數據清空,只不過後續的寫入會覆蓋掉原來的數據,也就相當於清空了數據了
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
compact(): 先處理還沒有讀取的數據,也就是 position 到 limit 之間的數據(還沒有讀過的數據),將所有未讀的數據拷貝到Buffer起始處,然後將position設到最後一個未讀元素的下一位置,在這個基礎上再開始寫入。此時 limit 還是等於 capacity,寫入新數據不會覆蓋原來數據
二、Channel
Channel是一個對象,可以通過它讀取和寫入數據。拿 NIO 與原來的 I/O 做個比較,通道就像是流。Channel與Buffer交互,讀操作的時候將 Channel 中的數據填充到 Buffer 中,而寫操作時將 Buffer 中的數據寫入到 Channel 中
NIO實現的Channel:
- FileChannel:文件通道,用於文件的讀和寫
- DatagramChannel:用於 UDP 連接的接收和發送
- SocketChannel:把它理解爲 TCP 連接通道,簡單理解就是 TCP 客戶端
- ServerSocketChannel:TCP 對應的服務端,用於監聽某個端口進來的請求
Channel使用
channel.read(buffer);
channel.write(buffer);
FileChannel
FileChannel 是不支持非阻塞
初始化
//使用流獲取通道
FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
FileChannel fileChannel = inputStream.getChannel();
//或者使用RandomAccessFile
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
讀取文件內容到Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = fileChannel.read(buffer);
寫入文件內容到Channel
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("隨機寫入一些內容到 Buffer 中".getBytes());
// Buffer 切換爲讀模式
buffer.flip();
while(buffer.hasRemaining()) {
// 將 Buffer 中的內容寫入文件
fileChannel.write(buffer);
}
SocketChannel ServerSocketChannel
TCP 連接通道
// 打開一個通道
SocketChannel socketChannel = SocketChannel.open();
// 發起連接
socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));
// 實例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 監聽 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 一旦有一個 TCP 連接進來,就對應創建一個 SocketChannel 進行處理
SocketChannel socketChannel = serverSocketChannel.accept();
}
ServerSocketChannel 不和 Buffer 打交道了,因爲它並不實際處理數據,它一旦接收到請求後,實例化 SocketChannel,之後在這個連接通道上的數據傳遞它就不管了,因爲它需要繼續監聽端口,等待下一個連接
三、Selector
選擇器,Selector是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否爲諸如讀寫事件做好準備的組件。一個單獨的線程可以管理多個channel,從而管理多個網絡連接。Selector是註冊對各種 I/O 事件的興趣的地方,而且當那些事件發生時,就是這個對象告訴您所發生的事件
//開啓Selector
Selector selector = Selector.open();
// 將通道設置爲非阻塞模式,因爲默認都是阻塞模式的
channel.configureBlocking(false);
//將通道註冊到Selector上
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register 方法的第二個 int 型參數(使用二進制的標記位)用於表明需要監聽哪些感興趣的事件,共以下四種事件:
- SelectionKey.OP_READ,對應 00000001,通道中有數據可以進行讀取
- SelectionKey.OP_WRITE,對應 00000100,可以往通道中寫入數據
- SelectionKey.OP_CONNECT,對應 00001000,成功建立 TCP 連接
- SelectionKey.OP_ACCEPT,對應 00010000,接受 TCP 連接
可以同時監聽一個 Channel 中的發生的多個事件,比如我們要監聽 ACCEPT 和 READ 事件,那麼指定參數爲二進制的 00010001 即十進制數值 17 即可
註冊方法返回值是 SelectionKey 實例,它包含了以下內容
- Channel
- Selector
- Interest Set ,即我們設置的我們感興趣的正在監聽的事件集合
- ready集合,即返回對應集合的boolean值
相關方法
select()方法
select()方法返回的int值表示有多少通道已經就緒。調用此方法,會將上次 select 之後的準備好的 channel 對應的 SelectionKey 複製到 selected set 中。如果沒有任何通道準備好,這個方法會阻塞,直到至少有一個通道準備好
selectNow()
功能和 select 一樣,區別在於如果沒有準備好的通道,那麼此方法會立即返回 0
select(long timeout)
如果沒有通道準備好,此方法會等待一會