網絡編程 - NIO

Reactor模式

Reactor模式稱爲叫反應堆或則反應器。在網絡編程 - BIO中,咱們瞭解到,大量的線程休眠致使資源浪費,在BIO中,經過Reactor來優化,下面舉個簡單的例子:
有個高級遊戲樂園,裏面有5個不一樣遊戲的玩法,玩家進來的時候,有一對一的工做人員給玩家講解每一個玩法。
傳統BIO模式是這樣的:
A玩家進來,工做人員1接待A,工做人員1講解完第一個遊戲玩法,而後A玩家開始玩,此時,工做人員1就在等待A玩家玩完(好比br.readLine())。玩家玩第二個遊戲的時候,工做人員1開始講解第二個遊戲玩法,而後A玩家繼續玩,一直玩到第五個遊戲。
若是還有玩家進來呢?那派出第二個工做人員,第三個工做人員。。。。。。
咱們能夠看到,工做人員在玩家玩的時候,他是處於休息空閒的狀態,並且玩家愈來愈多的時候,工做人員就須要愈來愈多。
Reactor模式:
工做人員1給玩家A講解完了,他就去處理其餘事情,而玩家開始玩遊戲,當玩家準備玩下一個遊戲的時候,他就呼叫工做人員,工做人員就過來說解下一個遊戲玩法。原本一個工做人員只能服務一個玩家,可是經過這個模式,他能夠同時服務多個玩家,在玩家A玩遊戲的時候,他能夠爲其餘玩家提供講解服務。
在Reactor模式中,應用程序並不會調用某個方法直至完成,而是逆置了事件處理流程,具體事件處理程序向反應器註冊一個事件處理器,等到事件來了,具體事件處理程序再處理相關事件。java

NIO三大組件

Buffer

Buffer在NIO中,本質是一塊內存,用於和NIO通道進行交互。咱們能夠把數據從通道讀取中出來,寫入到Buffer,也能夠把Buffer的數據讀到出來,寫到通道中。
在NIO中,java定義了IntBuffer、FloatBuffer、ByteBuffer等,咱們比較經常使用的是ByteBuffer。
image.png編程

主要屬性

Buffer有幾個重要的屬性:position、limit、capacity。segmentfault

  • capacity:指的是內存塊的固定大小,一旦設定,就不能再修改。往內存寫滿數據後,就不能再寫,除非將其清空。
  • position:指的是讀和寫的下一個位置。每次讀或寫的時候,就會加1。從寫模式切換到讀模式的時候(flip方法),position就會重置爲0。
  • limit:在寫模式下,不超過capacity,在讀模式下,position不能大於limit。意思其實很簡單,就是寫的時候,不能超過指定的capacity大小。切換到讀的時候,limit等於寫的長度,讀取的position不能超過寫的數據limit。

下面經過一個簡單的例子深刻了解一下這幾個屬性。數組

public static void main(String[] args) {
    ByteBuffer buffer = ByteBuffer.allocate(8);
    System.out.println("init:" + buffer);
    buffer.put((byte) 'a');
    System.out.println("put-a:" + buffer);
    buffer.put((byte) 'b');
    System.out.println("put-b:" + buffer);
    buffer.put((byte) 'c');
    System.out.println("put-c:" + buffer);
    // 切換到讀模式
    buffer.flip();
    System.out.println("flip:" + buffer);
    buffer.get();
    System.out.println("get-a:" + buffer);
    buffer.get();
    System.out.println("get-b:" + buffer);
    buffer.get();
}

輸出結果以下:
image.png
初始化時,pos指向0,capacity和limit都等於指定大小8。
image.png
put-a時,pos+1,等於1,capacity和limit不變。
image.png
put-b時,pos+1,等於2,capacity和limit不變。
image.png
put-c時,pos+1,等於3,capacity和limit不變。
image.png
flip後,pos把值賦值給limit,並重置爲0,capacity不變。此時,pos等於0,limit等於3,capacity等於8。
image.png
get-a時,pos+1,等於1,capacity和limit不變。
image.png
get-b時,pos+1,等於1,capacity和limit不變。
image.png網絡

主要方法

初始化buffer

allocate方法,在上面例子中,咱們看到了ByteBuffer.allocate(8)的使用。優化

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    // lim也傳capacity,因此兩個剛開始是相等的
    return new HeapByteBuffer(capacity, capacity);
}

HeapByteBuffer(int cap, int lim) {  
    // 這邊pos賦值爲0,字節長度爲cap
    super(-1, 0, lim, cap, new byte[cap], 0);
}

ByteBuffer(int mark, int pos, int lim, int cap, 
             byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

wrap方法,跟allocate方法均可以初始化buffer,不一樣的是能夠指定pos和limit,以及指定字節數組的初始值。this

public static ByteBuffer wrap(byte[] array) {
    return wrap(array, 0, array.length);
}
public static ByteBuffer wrap(byte[] array,
                                    int offset, int length)
{
    try {
        // 傳遞字節數組,pos,偏移量length,用於計算limit
        return new HeapByteBuffer(array, offset, length);
    } catch (IllegalArgumentException x) {
        throw new IndexOutOfBoundsException();
    }
}

寫數據

除了上面例子演示的,put(byte),還有如下這些。
image.png
從源碼中看pos會加1的緣由:spa

public abstract ByteBuffer put(byte b);

public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}
final int nextPutIndex() {                          // package-private
    if (position >= limit)
        throw new BufferOverflowException();
    // 這邊加1
    return position++;
}

也能夠把通道中的數據寫入到buffer:操作系統

// 這邊用read指的是把通道的數據讀取出來,再寫入buffer,read返回的是寫入buffer的數據大小。
channel.read(buf)

flip

從源碼中也能夠看出,把pos的值賦值給limit,並重置爲0。線程

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

讀數據

除了上面例子演示的,get,還有如下這些
image.png
從源碼中看pos會加1的緣由:

public abstract byte get();

public byte get() {
    return hb[ix(nextGetIndex())];
}

final int nextGetIndex() {                          // package-private
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

也能夠把buffer的數據寫入到通道中:

// 把buffer的數據讀取出來,寫入到channel中
channel.write(buf)

標記與重置

調用mark的時候,會把pos的值給mark,調用reset的時候,會把mark的值給pos。在實際過程當中,咱們在讀操做的時候,先調用mark方法標記位置,好比此時爲4,當咱們讀到7的時候,再調用reset方法,此時又從新從4開始讀。

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把pos置爲0,因此就是從頭開始讀寫。
clear方法,把pos置0,並重置limit爲capacity,這個時候進行寫的時候,就是從第一個位置開始寫,若是原先有數據,就是要被覆蓋,至關於清空了整個內存。
compact與clear不同的是,他會把pos和limit之間的數據,移到前面去,並設置pos的值,寫的時候,會重新的位置開始寫。好比pos爲2,limit爲4,他會把2-4之間的值移到0,再把pos設置爲2,這樣沒讀的數據,就不會被覆蓋而消失消失。

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

public abstract ByteBuffer compact();
public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}

public final int remaining() {
    return limit - position;
}

Channel

Channel,通道,操做系統和應用程序之間的數據交互,就是經過通道來的。

  • ServerSocketChannel:用於TCP的服務端
  • SocketChannel:用於TCP的客戶端
  • DatagramChannel:用於UDP

Selector

選擇器,把Channel和須要的事件註冊到Selector上面,讓Selector進行監聽。這些事件包括如下幾種:

// 讀
public static final int OP_READ = 1 << 0;
// 寫
public static final int OP_WRITE = 1 << 2;
// 請求鏈接
public static final int OP_CONNECT = 1 << 3;
// 接收鏈接
public static final int OP_ACCEPT = 1 << 4;

當須要監聽多個事件時,好比OP_ACCEPT和OP_CONNECT能夠這樣寫SelectionKey.OP_ACCEPT | SelectionKey.OP_CONNECT

相關文章
相關標籤/搜索