NIO組件Buffer,Channel和Selector

流是用來讀寫數據的。所有 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;
}
  1. 使用get()方法從Buffer中讀取數據
  2. 從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)
如果沒有通道準備好,此方法會等待一會

發佈了140 篇原創文章 · 獲贊 18 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章