IO到NIO的前因後果,以及NIO的用法(2)——Selector、Channel

Selector

Selector 一般稱 爲選擇器 ,當然你也可以翻譯爲 多路複用器 。它是Java NIO核心組件中的一個,用於檢查一個或多個NIO Channel(通道)的狀態是否處於可讀、可寫。如此可以實現單線程管理多個channels,也就是可以管理多個網絡鏈接。

Selector——java.channels.Selector

SelectableChannel是一個類,Java通道中最重要的ServerSocketChannelSocketChannelDatagramSocketChannel都是它的間接子類。

SelectableChannel可以是阻塞的。也可以是非阻塞模式。阻塞模式下,通道上的IO操作完成之前都是阻塞的,在非阻塞模式下的IO操作,即使是傳輸少於要求的字節數甚至一字節也沒有,也不會造成阻塞。是否處於阻塞模式,可以通過SelectableChannel類的isBlocking()方法進行判斷。

SelectableChannel和Selector配合使用的基本步驟:

1. 創建一個Selector實例

一般調用Selector.open方法創建一個Selector

Selector selector = Selector.open()

2. 通過SelectableChannel的register方法,把SelectableChannel對象註冊到註冊到一個Selector,從而得到一個選擇鍵SelectionKey對象。

channel.configureBlocking(false); 
SelectionKey selectionKey = channel.register(selector,SelectionKey.OP_WRITE|Selectionkey.OP_READ);

------------------------------------------------------------------------------

Channel必須是非阻塞的

所以FileChannel不適用Selector,因爲FileChannel不能切換爲非阻塞模式,更準確的來說是因爲FileChannel沒有繼承SelectableChannel。SocketChannel可以正常使用。

SelectableChannel抽象類有一個 configureBlocking() 方法用於使通道處於阻塞模式或非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)

注意:

SelectableChannel抽象類configureBlocking() 方法是由 AbstractSelectableChannel抽象類實現的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接繼承了 AbstractSelectableChannel抽象類 

--------------------------------------------------------------------------

register()方法的第二個參數是一個"interest集合",意思是在通過Selector監聽Channel時對什麼事件感興趣。四個不同的類型的事件:

SelectionKey.OP_WRITE:寫操作

SelectionKey.OP_READ:讀操作

SelectionKey.OP_ACCEPT:接受套接字操作

SelectionKey.OP_CONNECT:套接字連接操作

如果你對不止一種事件感興趣,使用或運算符即可,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

興趣集合也就是說該通道需要進行什麼操作,但是沒有準備好,也就是說還沒有到讀寫數據的時候。

通道觸發了一個事件意思是該事件已經就緒。比如某個Channel成功連接到另一個服務器稱爲“ 連接就緒 ”。一個ServerSocketChannel準備好接收新進入的連接稱爲“ 接收就緒 ”。一個有數據可讀的通道可以說是“ 讀就緒 ”。等待寫數據的通道可以說是“ 寫就緒 ”。

3. 從Selector中選擇channel

選擇器維護註冊過的通道的集合,並且這種註冊關係都被封裝在SelectionKey當中.

Selector維護的三種類型SelectionKey集合:

  1. keys所有註冊到Selector的Channel所表示的SelectionKey都會存在於該集合中。keys元素的添加會在Channel註冊到Selector時發生。
  2. selectedKeys:該集合中的每個SelectionKey都是其對應的Channel在上一次操作selector期間被檢查到至少有一種SelectionKey中所感興趣的操作已經準備好被處理。該集合是keys的一個子集。
  3. cancelledKeys:執行了取消操作的SelectionKey會被放入到該集合中。該集合是keys的一個子集。

一旦向Selector註冊了通道,就可以調用select()方法來選擇一些選擇鍵,這些選擇鍵對應的通道已經準備好進行IO操作。

1. abstract int select()
阻塞到至少有一個通道準備好
2. abstract int select(long timeout);
和select一樣,但是最長阻塞時間爲timeout
3. abstract int selectNow()
非阻塞,只要有通道就緒就立刻返回

select()方法返回的int值表示有多少通道已經就緒,是自上次調用select()方法後有多少通道變成就緒狀態。之前在select()調用時進入就緒的通道不會在本次調用中被記入,而在前一次select()調用進入就緒但現在已經不在處於就緒的通道也不會被記入。例如:首次調用select()方法,如果有一個通道變成就緒狀態,返回了1,若再次調用select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了。

一旦調用select()方法,並且返回值不爲0時,則可以通過調用Selector的selectedKeys()方法來訪問已選擇鍵集合 。如下:

Set<SelectionKey> selectKeys = selector.selectKeys();
Iterator<SelectionKey> keyIterator = selectKeys.iterator();
while(keyIterator.hasNext()){
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()){
            //檢測每一個鍵所對應的通道的就緒事件是否希望處理的事件
        doaccetp(key);//自定義方法
    }else if(key.isConnectable()){
        doconnectable(key);
    }else if(key.isReadable()){
        doreadable(key);
    }else if(key.isWritale()){
        dowritable(key);
    }
    keyIterator.remove();//調用remove()處理已經處理過的鍵。
}

4. 關閉Selector

selector.close();

Selector關閉的時候,相關的所有沒有取消的選擇鍵都會失效,註冊的通道也會消失註冊。

SelectionKey介紹

一個SelectionKey鍵表示了一個特定的通道對象和一個特定的選擇器對象之間的註冊關係。

1. key.attachment();
//返回SelectionKey的attachment,attachment可以在註冊channel的時候指定
2. key.channel()
返回SelectionKey對應的channel
3. key.selector();
返回SelectionKey對應的Selector
4. key.interestOps()
返回代表需要Selector監控的IO操作的bit mask
5. key.readyOps()
返回一個bit mask , 代表相應channel上可以進行的IO操作

我們可以通過以下方法來判斷Selector是否對Channel的某種事件感興趣:key.interestOps()

int interestSet = selectionKey.interestOps(); 
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;

ready 集合是通道已經準備就緒的操作的集合。JAVA中定義以下幾個方法用來檢查這些操作是否就緒.key.readyOps()

//獲得就緒集合
int readySet = selectionKey.readyOps();
//檢查這些操作是否就緒的方法
key.isAcceptable();//是否可讀,是返回 true
boolean isWritable()://是否可寫,是返回 true
boolean isConnectable()://是否可連接,是返回 true
boolean isAcceptable()://是否可接收,是返回 true

可以將一個對象或者更多信息附着到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的Buffer,或是包含聚集數據的某個對象。使用方法如下:

final Object attach(Object obj);
----------------------------------
key.attach(theObject);
Object attachedObj = key.attachment();

Channel介紹

對於Selector進行介紹時,我們直接使用了channel變量,下面我們介紹一下Channel:

Channel主要分爲兩類,文件讀寫FileChannel以及網絡讀寫SelectableChannel(ServerSocketChannel、SocketChannel、DatagramChannel是他的子類)

java.nio.channels

ServerSocketChannel

ServerSocketChannel與ServerSocket一樣都是socket監聽器,其主要區別是前者可以運行在非阻塞模式下運行。

1. 創建ServerSocketChannel

static ServerSocketChannel open();
open方法打開server端Socket的通道,打開的通道默認是不綁定的,需要後續使用bind()方法綁定

2. 綁定地址

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(5678));

abstract ServerSocketChannel bind(SocketAddress address, int backlog);
backlog指定了連接等待隊列的最大長度,如果backlog=0或者負值,將不起作用,採用系統的默認值

3. 接收請求

同ServerSocket一樣,ServerSocketChannel通過accept方法監聽連接請求,並接收請求,當accept返回時,就得到一個對應某個客戶端的SocketChannel對象,accept方法依然是阻塞的。

while(true){
    SocketChannel sc = ServerSocketChannel.accept();
    //do something...
}

4. 設置非阻塞模式

在ServerSocketChannel的父類AbstractSelectableChannel中,定義了設置非阻塞模式的方法
final SelectableChannel configureCBlocking(boolean block)
true--阻塞
false--非阻塞

4. 關閉ServerSocketChannel

ssc.close()

SocketChannel

SocketChannel以非阻塞的方式讀取Socket,使用一個線程就可以和多個連接進行通信,通過把多個SocketChannel註冊到Selector,之後在循環中使用Selector的select方法,一旦有事件發生,就會得到通知,進行相應的處理。

1. 創建SocketChannel

1. static SocketChannel open();
//不帶參數的open方法,用於後續使用connect方法進行連接遠程主機

舉例:

SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("http://www.foo.com",80));

2. static SocketChannel open(SocketAddress remote);
3. adstract SocketChannel accept();//ServerSocketChannel的方法,返回SocketChannel

2. 連接/終止連接SocketChannel

連接:

connect(SocketAddress remote);

可以通過isConnected()方法判斷是否已經連接。

終止連接:

finishConnect();

3. 關閉SocketChannel

sc.close()

4. 讀取SocketChannel

1. abstract int read(ByteBuffer dst)
從SocketChannel讀取的數據要寫入Buffer,之後程序通過讀取Buffer來獲得數據

example:
    ByteBuffer bB = ByteBuffer.allocate(100);
    int count = socketChannel.read(bB);
    從通道中讀到的字節,會放到緩衝區但當前位置position開始的位置。

2. final long read(ByteBuffer[] dsts);
從通道讀取字節序列,存到dsts給定的一組緩衝區中。
3. abstract long read(ByteBuffer dsts, int offset, int length);
從通道中讀取的字節,從dsts緩衝區數組的offset開始的length個緩衝區中。

5. 寫入SocketChannel

1. abstract int write(ByteBuffer src);
從字節緩衝區src中讀取一系列字節寫入通道中。
2. final long write(ByteBuffer[] srcs);
從字節緩衝區數組srcs依次讀取每個緩衝區,將讀取的一系列字節寫入通道中。
3. abstract long write (ByteBuffer[] srcs, int offset, int length);
從字節緩衝區數組srcs一次讀取每個緩衝區,將讀取的一系列字節寫入通道中,從offset的開始讀。

6. 設置非阻塞模型

sc.configureBlocking(false);

DatagramChannel

DatagramChannel類支持以非阻塞方式發送和接收UDP數據報。它也是SelectableChannel的子類。同DatagramSocket類似,UDP不是面向連接的。

1. 創建DatagramChannel

static DatagramChannel open()

DatagramChannel dc = DatagramChannel.open()

2. 綁定地址

把通道的套接字綁定到本地地址

abstract DatagramChannel bind(SocketAddress local)

dc.bind(new InetSocketAddress(5678))

3. 接收數據報

ByteBuffer bB = ByteBuffer.allocateDirect(100);
dc.receiver(buf);

如果DatagramChannel工作在阻塞模式,那麼receive方法會一直等到有可以讀取的數據報才返回。

如果DatagramChannel工作在非阻塞模式,那麼receive方法如果沒有接收到數據報,會立即返回null。

4. 發送數據報

abstract int send(ByteBuffer src, SocketAddress target);
src是要發送的內容,具體指的是src中剩餘的數據,target指的是數據報的目的地址。
返回值表示的是發送的字節數。

ByteBuffer bB = ByteBuffer.allocateDirect(100);
buf.put(msg.getBytes());
buf.flip();
int count = dc.send(bB, new InetSocketAddress("www.foo.com",5678));

同UDP的DatagramSocket類似,DatagramChannel不會接收到關於發送和接收數據報的通知。

ByteBuffer對象在通過put方法存入數據後,需要調用flip()方法重置position的位置,以便把剛存入的數據重新讀出。需要重複讀取ByteBuffer時,相應地,可以調用rewind()方法。

5. 管理固定連接

abstract DatagramChannel connect(SocketAddress remote);
//與特定的主機remote建立固定連接
abstract DatagramChannel disconnect()
//解除固定連接關係
abstarct boolean isConnected()
//只有當DatagramChannel打開並且有固定連接的時候才返回true

6. 讀寫數據

read和receive方法一樣,都能接收數據報。使用read方法時,通道必須已經與某個遠程主機有固定連接。

abstarct int read(BYteBuffer dst)
final long read(ByteBuffer[] dsts)
abstarct long read(ByteBuffer[] dsts, int offset, int length)

abstarct int write(ByteBuffer[] srcs);
final long write(ByteBuffer[] srcs);
abstract long write(ByteBuffer[] srcs, int offset, int length)

FileChannel

暫不做介紹

模板代碼:

有了模板代碼我們在編寫程序時,大多數時間都是在模板代碼中添加相應的業務代碼

服務端:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress("localhost",8080));
ssc.configureBlocking(false);

Selector selector = Selector.open();
ssc.reister(selector, SelectionKey.OP_ACCEPT);

while(true){
    int readyNum = selector.select();
    if(readyNum == 0){
        continue;
    }
    
    Set<SelectionKey> selectionKeys = seletor.selectedKeys();
    Iterator<SelectionKey> it = selectedKeys.iterator();
    
    while(it.hasNext()){
        SelectionKey key = it.next();
        if(key.isAcceptable()){
            //創建連接,並且把連接註冊到selector
            SocketChannel socketChannel = ssc.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        }else if(key.isConnectable()){
            
        }else if(key.isWritable()){
            writeBuffer.rewind();
            SocketChannel socketChannel = (SocketChannel)key.channel();
            socketChannel.write(writeBuffer);
            key.interestOps(SelectionKey.OP_READ);
        }else if(key.isReadable()){
            SocketChannel socketChannel = (SocketChannel)key.channel();
            readBuffer.clear();
            socketChannel.read(readCBuffer);
            readBuffer.flip();
            System.out.println(new String(readBuffer.array()));
            key.interestOps(SelectionKey.OP_WRITE);
        }
        it.remove();
    }
}

舉例:客戶端和服務端交互

服務端:

try{
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.bind(new InetSocketAddress("127.0.0.1",5678));
    ssc.configureBlocking(false);
    
    Selector selector = Selector.open();
    ssc.register(selector, SelectionKey.OP_ACCEPT);
    
    ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    
    writeBuffer.put("recevive:".getBytes());
    writeBuffer.flip();
    
    while(true){
        int nReady = selector.select();
        if(nReady == 0){
            continue;
        }
        Set<SelectionKey> keys = selector.selectiedKeys();
        Iterator<SelectionKey> it = keys.iterator();
        while(it.hasNext()){
            SelectionKey key = it.next();
            if(key.isAcceptable()){
                //創建新的連接
                SocketChannel socketChannel = ssc.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SeletionKey.OP_READ);
            }else if(key.isReadable()){
                SocketChannel socketChannel = (SocketChannel)key.channel();
                readBuffer.clear();
                socketChannel.read(readCBuffer);
                
                readBuffer.flip();
                System.out.println(new String(readBuffer.array()));
                key.interestOps(SelectionKey.OP_WRITE);
             }else if(key.isWritable()){
                writeBuffer.rewind();
                SocketChannel socketChannel = (SocketChannel)key.channel();
                socketChannel.write(writeBuffer);
                key.interestOps(SelectionKey.OP_READ);
            }
        }
    }
    
}catch(Exception e){
    e.printStatckTrace()
}

客戶端:

try{
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("127.0.0.1",5678));
    ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    
    writeBuffer.put("hello".getBytes());
    writeBuffer.flip();
    
    while(true){
        writeBuffer.rewind();
        socketChannel.write(writeBuffer);
        
        readBuffer.clear();
        socketChannel.read(readBuffer);
    }
}catch(Exception e){
    e.printStackTrace();
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章