NIO中和選擇器Selector

NIO中和選擇器Selector

在上一篇的JAVA中NIO再深刻咱們學會了如何使用Buffer ,而在Java中IO和NIO中咱們略微瞭解到Channel的概念,咱們知道了Channel就像礦洞裏的鐵軌同樣,Buffer就像鐵軌上的礦車,對於數據真正的操做都是對於Buffer的操做。而在NIO中還有一個很是重要的概念就是Selector,它就像礦洞裏的調度系統同樣。segmentfault

爲何要有Selector

要理解爲何要有Selector?這個問題,咱們首先得知道在UNIX系統中有五種I/O模型:同步阻塞I/O、同步非阻塞I/O、I/O多路複用、信號驅動I/O和異步I/O。這個幾個I/O模型都是什麼意思呢,大概比喻一下。服務器

  • 阻塞式I/O模型:一我的在釣魚,當沒魚上鉤時,就坐在岸邊一直等。
  • 非阻塞式I/O模型:邊釣魚邊玩手機,隔會再看看有沒有魚上鉤,有的話就迅速拉桿。
  • I/O複用模型:放了一堆魚竿,在岸邊一直守着這堆魚竿,沒魚上鉤就玩手機。
  • 信號驅動式I/O模型:魚竿上繫了個鈴鐺,當鈴鐺響,就知道魚上鉤,而後能夠專心玩手機。
  • 異步I/O模型:僱傭一我的來給我釣魚,釣上來之後給我送到住處,我該幹嗎幹嗎去。

阻塞與非阻塞是指應用程序在發起I/O操做時,是當即返回仍是等待。而同步和異步是指應用程序在於內核通訊時,數據從內核空間到應用空間的拷貝,是由內核發起仍是由應用程序來觸發。網絡

而所謂的I/O就是計算機內存與外部設備之間數據拷貝的過程,咱們知道CPU訪問內存的速度遠遠高於外部設備,所以CPU一般就是先將外部設備的數據讀取到內存中,而後再進行處理。而後此時有個場景,當那你的用戶程序經過CPU向外部設備發送了一個讀的指令,數據從外部設備到內存中是須要一段時間的,那麼此時CPU是休息呢?仍是讓給別人?仍是不斷的詢問,到了嗎?到了嗎?到了嗎……?這個就是I/O模型所要解決的問題。異步

而咱們的NIO模擬的I/O模型就是I/O複用模型。經過只阻塞Selector 這一個線程,經過Selector 不斷的查詢Channel中的狀態,從而達到了一個線程控制Selector ,而一個Selector 控制多個Channel的目的。用圖表示就是這樣。socket

Selector使用

從圖上面咱們就能夠猜出來大概的Selector 該如何來使用post

建立Selector

經過調用Selector.open()方法來建立一個Selector。線程

Selector selector = Selector.open();

建立所須要的Channel

咱們知道NIO中的Channel分爲四種類型code

  • FileChannel:文件通道
  • DatagramChannel:經過UDP讀取網絡中的數據
  • SocketChannel:經過TCP讀取網絡中的數據
  • ServerSocketChannel:能夠監聽進來的鏈接,對於每一個進來的鏈接都會建立一個SocketChannel

在這四個通道中有一個不能和Selector 配合使用,由於從圖中能夠看出,咱們的Selector 是不斷的輪詢註冊在Selector 中的每一個通道的狀態,不能阻塞在其中一個通道,即每一個通道必須是非阻塞狀態的,可是FileChannel的通道是阻塞狀態且不能更改,因此FileChannel不能和Selector 配合使用。server

ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(new InetSocketAddress(8080));
//設置爲非阻塞模式
socketChannel.configureBlocking(false);

將建立好的Channel註冊到Selector上

爲了便於Selector管理Channel,咱們將Channel註冊到Selector上。blog

//將Channel註冊到Selector上
SelectionKey selectionKey = socketChannel.register(selector,SelectionKey.OP_READ);

咱們能夠看到第一個參數就是咱們本身的Selector,而第二個參數就是選擇要監聽的事件類型,一共有四種

  • SelectionKey.OP_CONNECT:鏈接繼續事件,表示服務器監聽到了客戶鏈接,服務器能夠接收這個鏈接了
  • SelectionKey.OP_ACCEPT:鏈接就緒事件,服務端收到客戶端的一個鏈接請求會觸發
  • SelectionKey.OP_READ:讀就緒事件,表示通道中已經有可讀的數據了,能夠執行讀操做
  • SelectionKey.OP_WRITE:寫就緒事件,表示已經能夠向通道寫數據了

ServerSocketChannel 的有效事件是OP_ACCEPTSocketChannel 的有效事件是OP_CONNECTOP_READOP_WRITE

Selector循環遍歷各個Channel

在上一步咱們已經將所須要的Channel註冊到了Selector 中,那麼咱們如今能夠調用Selector.select()方法進行遍歷獲得已經準備好的Channel

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

注意此時咱們在遍歷完成後,完成相應操做後都要調用keyIterator.remove();方法將其移除掉,由於select()方法只是獲得了全部已經準備好的Channel的key值集合,若是不刪除的話,那麼下次遍歷依然仍是會調用相應的事件。

完整的例子

作一個簡單的服務器監聽的程序。監聽本機的8080端口,打印出發送過來的數據。

public class TestNIO {

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.socket().bind(new InetSocketAddress(8080));
        //設置爲非阻塞模式
        socketChannel.configureBlocking(false);
        //將Channel註冊到Selector上
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            int readyChannel = selector.select();
            if (readyChannel == 0){
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                keyIterator.remove();
                if (key.isAcceptable()){
                    System.out.println("isAcceptable");
                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(key.selector(),SelectionKey.OP_READ);
                }
                else if (key.isConnectable()){
                    System.out.println("isConnectable");
                }
                else if (key.isReadable()){
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    clientChannel.read(byteBuffer);
                    System.out.println(new String(byteBuffer.array()));
                }else if (key.isWritable()){
                    System.out.println("isWritable");
                }
            }
        }
    }
}

此時能夠經過在控制檯用命令telnet localhost 8080便可與服務器鏈接。

參考文章

相關文章
相關標籤/搜索