Java NIO Selector , SelectionKey , SocketChannel , ServerSocketChannel

一    NIO介紹

1. NIO是非阻塞的

    NIO非堵塞應用通常適用用在I/O讀寫等方面,我們知道,系統運行的性能瓶頸通常在I/O讀寫,包括對端口和文件的操作上,過去,在打開一個I/O通道後,read()將一直等待在端口一邊讀取字節內容,假如沒有內容進來,read()也是傻傻的等,這會影響我們程序繼續做其他事情,那麼改進做法就是開設線程,讓線程去等待,但是這樣做也是相當耗費資源的。

2. 實現原理

    Java NIO非堵塞技術實際是採取Reactor模式,或者說是Observer模式爲我們監察I/O端口,假如有內容進來,會自動通知我們,這樣,我們就不必開啓多個線程死等。

    Selector就是觀察者,觀察 Server 端的ServerSocketChannel  和 Client 端的 SocketChannel ;前提是它們需要先註冊到 同一個Selector,即觀察者中;

----詳細說明

    NIO 有一個主要的類Selector,這個類似一個觀察者,只要我們把需要探知的socketchannel告訴Selector,我們接着做別的事情,當有事件發生時,他會通知我們,傳回一組SelectionKey,我們讀取這些Key,就會獲得我們剛剛註冊過的socketchannel,然後,我們從這個Channel中讀取數據,放心,包準能夠讀到,接着我們可以處理這些數據。Selector內部原理實際是在做一個對所註冊的Channel(SocketChannel)的輪詢訪問,不斷的輪詢(目前就這一個算法),一旦輪詢到一個channel有所註冊的事情發生,比如數據來了,它就會站起來報告,交出一把鑰匙,讓我們通過這把鑰匙來讀取這個channel的內容。

while(!stop) {
            try {
                selector.select(1000);  // 等待客戶端發請求  1000ms
                selector.selectedKeys().stream().forEach( key -> handleSelectionKey(key) );
            }catch(Exception e) {
                e.printStackTrace();
            }
        }

 

3. 小結

 

    從外界看,實現了流暢的I/O讀寫,不堵塞了。Java NIO出現不只是一個技術性能的提高,你會發現網絡上到處在介紹它,因爲它具有里程碑意義,從JDK1.4開始,Java開始提高性能相關的功能,從而使得Java在底層或者並行分佈式計算等操作上已經可以和C或Perl等語言並駕齊驅。

 

下文部分轉載自  https://blog.csdn.net/robinjwong/article/details/41792623

二    Selector , SelectionKey  , SocketChannel , ServerSocketChannel 具體應用和功能

     NIO的通訊過程

1.    Selector  (選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否爲諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接

    僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。

 

  1. java.nio.channels

  2. public abstract class Selector extends Object implements Closeable

 

1.1    Selector 的創建

通過調用Selector.open()方法創建一個Selector;Selector selector = Selector.open();

    isOpen() —— 判斷Selector是否處於打開狀態。Selector對象創建後就處於打開狀態了。

    close() —— 當調用了Selector對象的close()方法,就進入關閉狀態.。用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey實例無效。通道本身並不會關閉

 

1.2    ServerChanel  向 Selector 中註冊

爲了將Channel和Selector配合使用,必須將channel註冊到selector上。

通過SelectableChannel。register()方法來實現。


 
  1. channel.configureBlocking(false);

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

    與Selector一起使用時,Channel必須處於非阻塞模式下。這意味着FIleChannel與Selector不能一起使用。

    注意register()方法的第二個參數,這是一個”interest集合“,意思是在通過Selector監聽Channel時對什麼事件感興趣。

可以監聽四種不同類型的事件:

  • Connect
  • Accept
  • Read
  • Write

    通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱爲”連接就緒“。一個server socket channel準備號接收新進入的連接稱爲”接收就緒“。一個有數據可讀的通道可以說是”讀就緒“。等代寫數據的通道可以說是”寫就緒“。

    這四種事件用SelectionKey的四個常量來表示:

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

 

 

2. register()返回值 —— SelectionKey,  Selector中的SelectionKey集合

    只要ServerSocketChannel及SocketChannel向Selector註冊了特定的事件,Selector就會監控這些事件是否發生

    SelectableChannel的register()方法返回一個SelectionKey對象,該對象是用於跟蹤這些被註冊事件的句柄

一個Selector對象會包含3種類型的SelectionKey集合:

  • all-keys集合 —— 當前所有向Selector註冊的SelectionKey的集合,Selector的keys()方法返回該集合
  • selected-keys集合 —— 相關事件已經被Selector捕獲的SelectionKey的集合,Selector的selectedKeys()方法返回該集合
  • cancelled-keys集合 —— 已經被取消的SelectionKey的集合,Selector沒有提供訪問這種集合的方法
  •  

    當register()方法執行時,新建一個SelectioKey,並把它加入Selector的all-keys集合中。

----selectionKey手動關閉 remove() 或cancel()

如果關閉了與SelectionKey對象關聯的Channel對象,或者調用了SelectionKey對象的cancel方法,這個SelectionKey對象就會被加入到cancelled-keys集合中,表示這個SelectionKey對象已經被取消。

    在執行Selector的select()方法時,如果與SelectionKey相關的事件發生了,這個SelectionKey就被加入到selected-keys集合中,程序直接調用selected-keys集合的remove()方法,或者調用它的iterator的remove()方法,都可以從selected-keys集合中刪除一個SelectionKey對象。

 

3.    SelectionKey——SelectableChannel 在 Selector 中的註冊的標記/句柄。

register()方法返回一個SelectinKey對象,這個對象包含一些你感興趣的屬性:

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的對象

通過調用某個SelectionKey的cancel()方法,關閉其通道,或者通過關閉其選擇器來取消該Key之前,它一直保持有效。

取消某個Key之後不會立即從Selector中移除它,相反,會將該Key添加到Selector的已取消key set,以便在下一次進行選擇操作的時候移除它。

  • interest集合 —— 感興趣的事件集合,可以通過SelectionKey讀寫interest集合,

 
  1. int interestSet = selectionKey.interestOps();

  2. boolean isInterestedInAccept = (interestSet & Selection.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

  3. boolean isInterestedInConnect = interestSet & SelectioKey.OP_CONNECT;

  4. boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;

  5. boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

  • ready集合 —— 是通道已經準備就緒的操作的集合,在一個選擇後,你會是首先訪問這個ready set,
int readySet = selectionKey.readyOps();

可以向檢測interet集合那樣的方法,來檢測channel中什麼事件或操作已經就緒,也可以使用一下四個方法,

selectionKey.isAcceptable();

selectionKey.isConnectable();

selectionKey.isReadable();

selectionKey.isWritable();

​​----Selector 內容

  • 從SelectionKey中獲取Channel和Selector:
  1. Channel channel = selectionKey.channel();

  2. Selector selector = selectionKey.selector();

  • 附加的對象 —— 可以將一個對象或者更多的信息附着到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的Buffer,或是包含聚集數據的某個對象,
  1. selectionKey.attach(theObject);

  2. Object attachedObj = selectionKey.attachment();

 

4.    通過Selector選擇就緒的通道

一旦向Selector註冊了一個或多個通道,就可以調用幾個重載的select()方法。

這些方法返回你所感興趣的事件(連接,接受,讀或寫)已經準備就緒的那些通道。換句話說,如果你對”讀就緒“的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。

  • select() —— 阻塞到至少有一個通道在你註冊的事件上就緒了
  • select(long timeout) —— 和select()一樣,除了最長會阻塞timeout毫秒
  • selectNow() —— 不會阻塞,不管什麼通道就緒都立刻返回;此方法執行非阻塞的選擇操作,如果自從上一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回0
  • select()方法返回的Int值表示多少通道就緒。

一旦調用了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過調用selector的selectorKeys()方法,訪問”已選擇鍵集“中的就緒通道

Set selectedKeys = selector.selectedKeys();

可以遍歷這個已選擇的集合來訪問就緒的通道


 
  1. 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 eatablished 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();
    }

     

這個循環遍歷已選擇集中的每個鍵,並檢測各個鍵所對象的通道的就緒事件。

注意每次迭代末尾的remove()調用,Selector不會自己從已選擇集中移除SelectioKey實例,必須在處理完通道時自己移除。

 

5.    Selector的wakeUp()方法

    某個線程調用select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其他線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。

 

-------個人實現的一個 案例代碼

 

import org.apache.commons.lang.StringUtils;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.logging.Logger;

class MyTimeServer implements Runnable {

    private static Logger logger = Logger.getLogger("MyTimeServer");

    private int port;
    private volatile boolean stop;
    private Selector selector;
    private ServerSocketChannel servChannel;

    public MyTimeServer(int port) {
        this.port = port;
    }

    private void startTimeServer(){
        try{
            selector = Selector.open();
            servChannel = ServerSocketChannel.open();
            servChannel.configureBlocking(false);
            servChannel.socket().bind(new InetSocketAddress(this.port),1024);
            servChannel.register(selector, SelectionKey.OP_ACCEPT);
            this.stop = false;
            logger.info("TimeServer starts in port : " + port);
        }catch(IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stopTimeServer() {
        this.stop = true;
        try{
            selector.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public void run() {

        startTimeServer();

        while(!stop) {
            try {
                selector.selectedKeys().stream().forEach( selectKey -> handleSelectionKey(selectKey) );
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
        //多路複用器關閉以後,所有註冊在上面的Channel和Pipe等資源都會自動去註冊並關閉,所以不需要重複釋放資源
        if(selector != null) {
            stopTimeServer();
        }
    }

    private void handleSelectionKey(SelectionKey selectKey){
        try{
            handleRequest(selectKey);
            if(selectKey != null) {
                selectKey.cancel();
                if(selectKey.channel() != null) {
                    selectKey.channel().close();  //處理client的key後 關閉該連接
                }
            }
        }catch(Exception e) {
            e.printStackTrace();
        }
    }

    private void handleRequest(SelectionKey selectKey) throws Exception {
        if(selectKey.isValid()) {
            if(selectKey.isAcceptable()) {
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectKey.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector , SelectionKey.OP_READ);
            }
            if(selectKey.isReadable()) {
                SocketChannel socketChannel = (SocketChannel)selectKey.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024); //預分配1024
                int readBytes = socketChannel.read(readBuffer);
                if(readBytes > 0) {
                    byte[] readbytes = new byte[readBytes];
                    readBuffer.position(0); // 重置 byteBuffer 的位置 position 變量
                    readBuffer.get(readbytes , 0 , readBytes);
                    handleReadContext(socketChannel,new String(readbytes , "UTF-8"));
                } else {
                    selectKey.cancel();
                    socketChannel.close();
                }
            }
        }
    }
    private void handleReadContext(SocketChannel socketChannel , String requestBody) {
        if(StringUtils.isNotBlank(requestBody) && requestBody.equals("time")) {
            logger.info("The timeServer receive request :  " + requestBody);
            String response = "[success] msg : " + new Date(System.currentTimeMillis()).toString();
            try {
                reponse(socketChannel, response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
            try {
                reponse(socketChannel, "[success] msg : nothing to say");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void reponse(SocketChannel socketChannel, String response) throws Exception {
        if(response != null && response.length() > 0) {
            byte[] reponesBytes = response.getBytes();
            ByteBuffer writeBuffer  = ByteBuffer.allocate(reponesBytes.length);
            writeBuffer.put(reponesBytes);
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            writeBuffer.clear();
        }
    }
}

 

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.logging.Logger;

class MyTimeServer implements Runnable {

    private static Logger logger = Logger.getLogger("MyTimeServer");

    private int port;
    private volatile boolean stop;
    private Selector selector;
    private ServerSocketChannel servChannel;

    public MyTimeServer(int port) {
        this.port = port;
    }

    private void startTimeServer(){
        try{
            selector = Selector.open();
            servChannel = ServerSocketChannel.open();
            servChannel.configureBlocking(false);
            servChannel.socket().bind(new InetSocketAddress(this.port),1024);
            servChannel.register(selector, SelectionKey.OP_ACCEPT);
            this.stop = false;
            logger.info("TimeServer starts in port : " + port);
        }catch(IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stopTimeServer() {
        this.stop = true;
        try{
            selector.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public void run() {

        startTimeServer();

        while(!stop) {
            try {
                selector.select(1000);  // 等待客戶端發請求  1000ms
                selector.selectedKeys().stream().forEach( key -> handleSelectionKey(key) );
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
        //多路複用器關閉以後,所有註冊在上面的Channel和Pipe等資源都會自動去註冊並關閉,所以不需要重複釋放資源
        if(selector != null) {
            stopTimeServer();
        }
    }

    private void handleSelectionKey(SelectionKey key){
        try{
            handleRequest(key);
            if(key != null) {
                key.cancel();
                if(key.channel() != null) {
                    key.channel().close();  //處理client的key後 關閉該連接
                }
            }
        }catch(Exception e) {
            e.printStackTrace();
        }
    }

    private void handleRequest(SelectionKey key) throws Exception {
        if(key.isValid()) {
            if(key.isAcceptable()) {
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector , SelectionKey.OP_READ);
            }
            if(key.isReadable()) {
                SocketChannel socketChannel = (SocketChannel)key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if(readBytes > 0) {
                    byte[] readbytes = new byte[readBytes];
                    readBuffer.position(0); // 重置 byteBuffer 的位置 position 變量
                    readBuffer.get(readbytes , 0 , readBytes);
                    String body = new String(readbytes, "UTF-8");
                    logger.info("The timeServer receive request :  " + body);
                    String response = "[success] msg : " + new Date(System.currentTimeMillis()).toString();
                    reponse(socketChannel, response);
                } else {
                    key.cancel();
                    socketChannel.close();
                }
            }
        }
    }

    private void reponse(SocketChannel socketChannel, String response) throws Exception {
        if(response != null && response.length() > 0) {
            byte[] reponesBytes = response.getBytes();
            ByteBuffer writeBuffer  = ByteBuffer.allocate(reponesBytes.length);
            writeBuffer.put(reponesBytes);
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            writeBuffer.clear();
        }
    }
}

 

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