java NIO

1、簡介

一、I/O

I/O指的是計算機與外部世界,或者程序與計算機其餘部分的接口,即輸入/輸出。緩存

在JAVA中,一般都以流的方式完成I/O,經過一個Stream對象操做。這種操做方法是堵塞的,沒法移動讀取位置的(只能一直往下讀,不能後退),而且效率較低。JAVA爲了提升I/O效率,在1.4以後,推出了NIO。服務器

二、NIO vs I/O

I/O NIO
面向流,一次讀取一個或多個字節,在流中沒法先後移動 面向緩存,讀取的數據先統一放在緩存中,在緩存中能先後移動
堵塞,從流讀取數據時,線程沒法作其餘事情 非堵塞,數據還未完整讀取到緩存中時,線程能夠先作其餘事
一對一:一條線程負責一個數據操做任務 一對多,一條線程負責多個數據操做任務

 

 

 

2、主要概念

NIO要實現面向緩存的非堵塞數據讀取,依賴"Channel(通道)"和"Buffer(緩衝區)";網絡

NIO要實現一條線程管理多個數據操做,依賴"Selector(選擇器)"。app

一、Channel通道

定義

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.異步

即經過Channel,你能夠和外部進行數據的讀寫。socket

雖然NIO的Channel和I/O的stream很像,不過仍是有區別的:ide

  • Channel老是面向緩存的,而流既能夠一次讀取一個或多個字節,也能夠選擇先把數據讀到緩存中。
  • Channel可讀也可寫,流只能讀或者寫。
  • Channel能夠異步讀寫

類型

根據Channel數據來源不一樣,Channel有不一樣的實現:性能

  • FileChannel : A channel for reading, writing, mapping, and manipulating a file -> 用於文件的讀寫
  • DatagramChannel : A selectable channel for datagram-oriented sockets -> 用於UDP的數據讀寫
  • SocketChannel : A selectable channel for stream-oriented connecting sockets -> 用於TCP的數據讀寫
  • ServerSocketChannel : A selectable channel for stream-oriented listening sockets -> 用於監聽TCP鏈接請求,爲每一個請求再創建一個SocketChannel

I/O 和 NIO 數據操做對比spa

// I/O流操做
FileInputStream inputStream = new FileInputStream("io.txt");
byte[] data = new byte[1024];
while(inputStream.read(data) != -1){
    // 從流中讀取多個字節,進行操做
}

 

// NIO channel操做
// 從文件流中獲取channel
FileInputStream inputStream = new FileInputStream("nio.txt");
FileChannel fileChannel = inputStream.getChannel();

// 新建緩存,用於存放Channel讀取的數據
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 經過Channel讀取數據到緩存中
fileChannel.read(buffer);

Channel間數據傳輸

對於FileChannel之間,利用transferFrom和transferTo能夠直接進行數據傳輸,提升性能。線程

將fromChannel中數據傳輸到toChannel 中,position指toChannel中開始的位置,count指接收的數據:

toChannel.transferFrom(fromChannel, position, count) 或

fromChannel.transferTo(position, count, toChannel)

二、buffer緩衝區

定義

A container for data of a specific primitive type.

A buffer is a linear, finite sequence of elements of a specific primitive type.  Aside from its content, the essential properties of a buffer are its capacity, limit, and position.

buffer緩衝區是一塊內存,用於存放原始類型數據。它的特色是:線性,有限,有序,只能存一種原始類型數據

屬性

  • capacity : A buffer's capacity is the number of elements it contains. -> 緩衝區大小
  • limit : A buffer's limit is the index of the first element that should not be read or written. -> 用於標記讀寫的邊界。讀模式,limit表示目前緩衝區內存放的數據量。寫模式,limit表示緩衝區最大的存放數,等於capacity.
  • position: A buffer's position is the index of the next element to be read or written. -> 標記目前讀或寫的位置

類型

根據buffer中存放的原始類型不一樣,有如下幾種Buffer實現

  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • ByteBuffer
  • CharBuffer

基本用法

  • 調用allocate()分配大小
  • channel寫數據到buffer
  • 調用flip()  -> buffer默認是寫模式,當要從Buffer中讀數據時,須要手動調flip方法,轉爲讀模式。(這時,limit置爲position,position置爲0)
  • 從buffer中讀數據
  • 當要從新往buffer中寫數據時,須要調用clear()或compact方法。

其餘方法介紹

  • clear : 將position置爲0, limit置爲capacity,清空buffer。對於還未讀取的數據,直接丟失。
  • compact:先將未讀的數據複製到緩存的開頭,position置到未讀數據的後一位,limit置爲capacity。
  • rewind: 將position重置爲0,這樣咱們能夠重讀Buffer數據
  • mark和reset:用mark標記此時的posiiton,以後移動position。調用reset,會將position重置爲mark標記的地方。

三、Selector選擇器

定義

A multiplexor of SelectableChannel objects.

一個Selector管理多個可管理的Channel通道,它主要用於檢查它負責的Channel通道的狀態。

以下是一個線程經過使用selector管理多個Channel通道的示意圖:

 使用Selector管理多個Channel通道的過程以下

  • 建立Selector
  • 將Channel通道註冊到Selector上(能夠指定讓Selector檢查的狀態:可讀、可寫、可鏈接、是否合法、是否已鏈接等),此時會返回一個Channel對應的惟一KEY對象
  • 經過Selector查詢處於就緒狀態的KYE集合
  • 遍歷KEY集合,從每一個KEY獲取到Channel通道,進行相應的處理。

使用詳解

建立Selector

Selector是由SelectorProvider建立出來的。具體有兩種方式建立:

  • Selector selector = Selector.open();  這種方式將調用默認的SelectorProvider來建立Selector
  • 本身實現SelectorProvider的openSelector()方法,用於建立Selector

註冊Channel通道到Selector上

Selector要管理Channel,必須先將Channel註冊到Selector上。只有SelectableChannel類才能用Selector管理。註冊的方法有兩個:

  • SelectionKey register(Selector sel, int ops)
  • SelectionKey register(Selector sel, int ops, Object att)
方法說明
  • 當SelectableChannel的register被其餘線程調用,或者SelectableChannel正在運行configureBlocking方法,那麼調用都會block。
  • 方法會同步該Channel在Selector上感興趣的key集合,所以並行調用會致使block。
  • 當Selector處於closed狀態,或者Channel處於block狀態時,都會拋出異常。所以在調用register以前,通常會先調用下channel.configureBlocking(false);
參數說明:
  • ops是Selector關注的Channel事件,有SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT、SelectionKey.OP_ACCEPT四種。若是對多個事件感興趣可利用位的或運算結合多個常量,好比:int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE
  • sel是Channel註冊到的Selector
  • att: 能夠在註冊時,多傳一個對象,通常用於記錄上下文信息
返回值說明

SelectionKey用於記錄Channel通道與Selector之間的關係。它有如下幾個重要的方法

  • int interestOps(). 返回Selector對於Channel的關注集合。咱們能夠經過與SelectionKey的四種事件進行&,來判斷是否關注該事件。好比:
    boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;

     

  • int readyOps(). 返回處於就緒狀態的事件。經過與四種事件進行&操做,便可知道哪一種事件已就緒。固然也能夠經過直接調用isAcceptable(),isConnectable(),isReadable(),isWritable()來直接獲取就緒狀態。
  • channel() : 獲取key上的Channel通道
  • selector() : 獲取key上的Selector
  • attach(Object ob) : 附加一個對象到key上,也可在register時帶上。這個對象能夠記錄一些附加信息,後面經過調用attachment()方法把對象從新取出。

從Selector上選擇Channel

先調用select方法獲取處於就緒狀態的Channel數量,當數量大於0時,再調用selectedKeys方法獲取處於就緒狀態的SelectionKey集合。

select方法有如下三個:

  • int select() : 返回處於就緒狀態的Channel通道數量,當沒有Channel通道就緒時,該方法block。
  • int select(long timeout) : 返回就緒狀態的Channel通道數量,沒有就緒的Channel則block, 但超過timeout返回。
  • int selectNow() : 不會block,當即返回。

Set<SelectionKey> selectedKeys() : 返回SelectionKey集合。

因select()而block的線程,另一個線程能夠調用該selector的wakeup()來喚醒。

selector使用完後,須要調用close()方法關閉。

完整示例

Selector selector = Selector.open();

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;
  Set<SelectionKey> selectedKeys = selector.selectedKeys();
  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

 

 3、總結

NIO容許咱們只用一條線程來管理多個通道(網絡鏈接或文件),隨之而來的代價是解析數據相對於阻塞流來講可能會變得更加的複雜。
若是你須要同時管理成千上萬的連接,這些連接只發送少許數據,例如聊天服務器,用NIO來實現這個服務器是有優點的。或者,若是你須要維持大量的連接,例如P2P網絡,用單線程來管理這些 連接也是有優點的。這種單線程多鏈接的設計能夠用下圖描述:


若是連接數不是不少,可是每一個連接的佔用較大帶寬,每次都要發送大量數據,那麼使用傳統的IO設計服務器多是最好的選擇。下面是經典IO服務設計圖:

相關文章
相關標籤/搜索