NIO原理解析

NIO中主要包括幾大組件,selector、channel、buffer。selector後面介紹,channel則相似於BIO中的流,可是流的讀取是單向的,例如只能讀,或只能寫,可是channel則是雙向的。數據能夠從channel讀到buffer中,也能夠從buffer中寫入到channel中。網絡

針對於客戶端請求服務端的場景,NIO實現的結構圖以下:dom

                                                                                     禁止盜圖,畫了很久。。。socket

 

 1、channel

 channel主要包括如下幾類性能

  • FileChannel-------------------------->從文件中讀寫數據
  • DatagramChannel------------------>經過UDP讀寫網絡中的數據
  • SocketChannel----------------------->經過TCP讀寫網絡中的數據
  • ServerSocketChannel--------------->監聽新進來的TCP鏈接,並生成一個SocketChannel對象

基本示例:學習

//建立能訪問任意位置的file文件
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
//獲取fileChannel對象
FileChannel inChannel = aFile.getChannel();
//分配48字節大小的byteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
//讀取channel中的數據到buffer中
int bytesRead = inChannel.read(buf);

while (bytesRead != -1) {
    System.out.println("Read " + bytesRead);
    //將buffer從寫模式切換到讀模式
    buf.flip();
    while(buf.hasRemaining()){
        System.out.print((char) buf.get());
    }
  //清空整個緩衝區
    buf.clear();
  //緩衝區已經所有讀完,返回-1退出循環
    bytesRead = inChannel.read(buf);
}

aFile.close();

另外channel不只能夠讀取數據到buffer中,當存在多個channel而且其中有一個channel爲fileChannel時,channel之間能夠互相傳輸數據spa

transferFrom() :能夠將數據從源通道傳輸到FileChannel中線程

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(position, count, fromChannel);

 

transferTo()3d

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);

 

2、Buffer

一、buffer主要包括以下幾類:

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

 二、使用buffer的通常步驟:

  • 寫入數據到Buffer
  • 調用flip()方法
  • 從Buffer中讀取數據
  • 調用clear()方法或者compact()方法------------>clear()方法會清空整個緩衝區,compact()只會清除已經讀過的數據

 三、buffer中的概念

爲了理解Buffer的工做原理,須要熟悉它的三個屬性:rest

  • capacity
  • position
  • limit

position和limit的含義取決於Buffer處在讀模式仍是寫模式。無論Buffer處在什麼模式,capacity的含義老是同樣的。code

capacity

做爲一個內存塊,Buffer有一個固定的大小值,也叫「capacity」.你只能往裏寫capacity個byte、long,char等類型。一旦Buffer滿了,須要將其清空(經過讀數據或者清除數據)才能繼續寫數據往裏寫數據。

position

當你寫數據到Buffer中時,position表示當前的位置。初始的position值爲0.當一個byte、long等數據寫到Buffer後, position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity – 1.

當讀取數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置爲0. 當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。

limit

在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。 寫模式下,limit等於Buffer的capacity。

當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。所以,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到以前寫入的全部數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)

 四、經常使用方法

a、分配大小

//分配48字節大小的byteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
//分配2014字符大小的charBuffer
CharBuffer buf = CharBuffer.allocate(1024);

b、寫數據到buffer中

包括兩種方式:一是從channel中讀數據到buffer中,另一種就是調用buffer的put()方法

//1、channel讀取數據到buffer
int bytesRead = inChannel.read(buf);
//2、調用channel的put()方法
buf.put(127);

c、讀取數據

一樣包括兩種方式:一是寫入數據到channel中,另一種就是調用buffer的get()方法

//1、將數據寫入到channel中
int bytesWritten = inChannel.write(buf);
//2、調用buffer的get()方法讀取數據
byte aByte = buf.get();

d、讀寫模式切換

buffer.flip()

e、清除buffer

一旦讀完Buffer中的數據,須要讓Buffer準備好再次被寫入。能夠經過clear()或compact()方法來完成。

若是調用的是clear()方法,position將被設回0,limit被設置成 capacity的值。換句話說,Buffer 被清空了。Buffer中的數據並未清除,只是這些標記告訴咱們能夠從哪裏開始往Buffer裏寫數據。

若是Buffer中有一些未讀的數據,調用clear()方法,數據將「被遺忘」,意味着再也不有任何標記會告訴你哪些數據被讀過,哪些尚未。

若是Buffer中仍有未讀的數據,且後續還須要這些數據,可是此時想要先先寫些數據,那麼使用compact()方法。

compact()方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法同樣,設置成capacity。如今Buffer準備好寫數據了,可是不會覆蓋未讀的數據。

 

另外NIO支持scatter/gather,說白了,就是一個channel能夠讀取數據到多個buffer中去,或者多個buffer能夠寫入到channel中。當第一個buffer寫滿以後,會緊接着讀取到第二個buffer中去,以下圖:

channel-->buffer

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

buffer-->channel

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

 

3、selector

爲啥使用selector?在傳統的BIO當中,監聽每一個客戶端的請求都須要一個線程去處理,線程數的上升會涉及到大量的上下文切換的操做,這也是很是浪費性能的。NIO中基於事件驅動的理念,使用selector監聽各類事件的發生,能夠實現只開啓一個線程

就能夠管理全部的請求,固然實際狀況合理的增長線程數能夠大大提升性能。

注意:與Selector一塊兒使用時,Channel必須處於非阻塞模式下。這意味着不能將FileChannel與Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式。

 一、註冊channel到selector中

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

selectionKey用來描述事件,包括事件類型,以及對應的selector與channel等等。

第二個入參爲事件類型,主要包括四種:

  • Connect
  • Accept
  • Read
  • Write

分別用常量表示爲:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

固然selector監聽channel時,能夠對多個事件感興趣,寫法以下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

可能對selector、channel、事件三者的關係有點亂,用故事來總結一下:selector是父母,channel是孩子,父母監督孩子學習,孩子有不少同窗,有些同窗學習好,有些同窗學習差,父母歡迎學習好的學生來家裏玩,排斥成績差的。那麼特定的事件就能夠理解

成那些成績差的同窗來家中,父母監聽到了,開始行動,將他們趕走。

 

二、selectionKey

當channel註冊到selector中時,會返回一個selectionKey對象,能夠理解成事件的描述或是對註冊的描述,主要包括這幾個部分:

a、interest集合----------------------->感興趣的事件集合。

int interestSet = selectionKey.interestOps();
//判斷事件是否在集合中
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;

b、ready集合-------------------------->通道已經準備就緒的操做的集合

int readySet = selectionKey.readyOps();
//事件是否在已準備就緒的集合中,selectionKey提供了以下方法
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

c、Channel

Channel  channel  = selectionKey.channel();

d、Selector

Selector selector = selectionKey.selector();

e、附加的對象(可選)

用戶也能夠將buffer等其餘對象加到selectionKey上,方便後續操做

添加對象:

//添加對象到selectionKey有兩種方式
selectionKey.attach(theObject);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
//獲取此附加對象
Object attachedObj = selectionKey.attachment();

 

三、select()

當把channel註冊到selector中去以後,能夠經過select()方法來監聽對應channel的特定事件。主要有三種select方法:

  • int select()-------------------------------------->阻塞到被監聽的channel至少有一個事件發生。
  • int select(long timeout)---------------------->在timeout(ms)的時間內阻塞,直到被監聽的channel至少有一個事件發生。
  • int selectNow()--------------------------------->非阻塞,無論有沒有事件發生,都立馬返回。

四、selectedKeys()

Set selectedKeys = selector.selectedKeys();

完整的示例:

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 selectedKeys = selector.selectedKeys();
    Iterator 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();
  }
}

 

4、DatagramChannel

前面的示例都是基於TCP鏈接,如今講述一下UDP的示例.

DatagramChannel是一個能收發UDP包的通道。由於UDP是無鏈接的網絡協議,因此不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。

一、打開DatagramChannel

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("jenkov.com", 80));
相關文章
相關標籤/搜索