Java NIO Selector

【正文】netty死磕1.4:  html

Java NIO Selector 一文全解 java


1.1. Selector入門

1.1.1. Selector的和Channel的關係

Java NIO的核心組件包括:編程

(1)Channel(通道)服務器

(2)Buffer(緩衝區)網絡

(3)Selector(選擇器)異步

其中Channel和Buffer比較好理解 ,聯繫也比較密切,他們的關係簡單來講就是:數據老是從通道中讀到buffer緩衝區內,或者從buffer寫入到通道中。socket

選擇器和他們的關係又是什麼?ide

選擇器(Selector) 是 Channel(通道)的多路複用器,Selector 能夠同時監控多個 通道的 IO(輸入輸出) 情況。性能

Selector的做用是什麼?學習

選擇器提供選擇執行已經就緒的任務的能力。從底層來看,Selector提供了詢問通道是否已經準備好執行每一個I/O操做的能力。Selector 容許單線程處理多個Channel。僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道,這樣會大量的減小線程之間上下文切換的開銷。

1.1.2. 可選擇通道(SelectableChannel)

並非全部的Channel,都是能夠被Selector 複用的。比方說,FileChannel就不能被選擇器複用。爲何呢?

判斷一個Channel 能被Selector 複用,有一個前提:判斷他是否繼承了一個抽象類SelectableChannel。若是繼承了SelectableChannel,則能夠被複用,不然不能。

SelectableChannel類是何方神聖?

SelectableChannel類提供了實現通道的可選擇性所須要的公共方法。它是全部支持就緒檢查的通道類的父類。全部socket通道,都繼承了SelectableChannel類都是可選擇的,包括從管道(Pipe)對象的中得到的通道。而FileChannel類,沒有繼承SelectableChannel,所以是否是可選通道。

通道和選擇器註冊以後,他們是綁定的關係嗎?

答案是否是。不是一對一的關係。一個通道能夠被註冊到多個選擇器上,但對每一個選擇器而言只能被註冊一次。

通道和選擇器之間的關係,使用註冊的方式完成。SelectableChannel能夠被註冊到Selector對象上,在註冊的時候,須要指定通道的哪些操做,是Selector感興趣的。

wps3749.tmp

1.1.3. Channel註冊到Selector

使用Channel.register(Selector sel,int ops)方法,將一個通道註冊到一個選擇器時。第一個參數,指定通道要註冊的選擇器是誰。第二個參數指定選擇器須要查詢的通道操做。

能夠供選擇器查詢的通道操做,從類型來分,包括如下四種:

(1)可讀 : SelectionKey.OP_READ

(2)可寫 : SelectionKey.OP_WRITE

(3)鏈接 : SelectionKey.OP_CONNECT

(4)接收 : SelectionKey.OP_ACCEPT

若是Selector對通道的多操做類型感興趣,能夠用「位或」操做符來實現:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

注意,操做一詞,是一個是使用很是氾濫,也是一個容易混淆的詞。特別提醒的是,選擇器查詢的不是通道的操做,而是通道的某個操做的一種就緒狀態。

什麼是操做的就緒狀態?

一旦通道具有完成某個操做的條件,表示該通道的某個操做已經就緒,就能夠被Selector查詢到,程序能夠對通道進行對應的操做。比方說,某個SocketChannel通道能夠鏈接到一個服務器,則處於「鏈接就緒」(OP_CONNECT)。再比方說,一個ServerSocketChannel服務器通道準備好接收新進入的鏈接,則處於「接收就緒」(OP_ACCEPT)狀態。還比方說,一個有數據可讀的通道,能夠說是「讀就緒」(OP_READ)。一個等待寫數據的通道能夠說是「寫就緒」(OP_WRITE)。

1.1.4. 選擇鍵(SelectionKey)

Channel和Selector的關係肯定好後,而且一旦通道處於某種就緒的狀態,就能夠被選擇器查詢到。這個工做,使用選擇器Selector的select()方法完成。select方法的做用,對感興趣的通道操做,進行就緒狀態的查詢。

Selector能夠不斷的查詢Channel中發生的操做的就緒狀態。而且挑選感興趣的操做就緒狀態。一旦通道有操做的就緒狀態達成,而且是Selector感興趣的操做,就會被Selector選中,放入選擇鍵集合中。

一個選擇鍵,首先是包含了註冊在Selector的通道操做的類型,比方說SelectionKey.OP_READ。也包含了特定的通道與特定的選擇器之間的註冊關係。

開發應用程序是,選擇鍵是編程的關鍵。NIO的編程,就是根據對應的選擇鍵,進行不一樣的業務邏輯處理。

選擇鍵的概念,有點兒像事件的概念。

選擇鍵和事件的關係是什麼?

一個選擇鍵有點兒像監聽器模式裏邊的一個事件,可是又不是。因爲Selector不是事件觸發的模式,而是主動去查詢的模式,因此不叫事件Event,而是叫SelectionKey選擇鍵。

1.2. Selector的使用流程

1.2.1. 建立Selector

Selector對象是經過調用靜態工廠方法open()來實例化的,以下:

  // 一、獲取Selector選擇器

            Selector selector = Selector.open();

Selector的類方法open()內部是向SPI發出請求,經過默認的SelectorProvider對象獲取一個新的實例。

1.2.2. 將Channel註冊到Selector

要實現Selector管理Channel,須要將channel註冊到相應的Selector上,以下:

            // 二、獲取通道

            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            // 3.設置爲非阻塞

            serverSocketChannel.configureBlocking(false);

            // 四、綁定鏈接

            serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

            // 五、將通道註冊到選擇器上,並制定監聽事件爲:「接收」事件

            serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

上面經過調用通道的register()方法會將它註冊到一個選擇器上。

首先須要注意的是:

與Selector一塊兒使用時,Channel必須處於非阻塞模式下,不然將拋出異常IllegalBlockingModeException。這意味着,FileChannel不能與Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式,而套接字相關的全部的通道均可以。

另外,還須要注意的是:

一個通道,並無必定要支持全部的四種操做。好比服務器通道ServerSocketChannel支持Accept 接受操做,而SocketChannel客戶端通道則不支持。能夠經過通道上的validOps()方法,來獲取特定通道下全部支持的操做集合。

1.2.3. 輪詢查詢就緒操做

萬事俱備,能夠開幹。下一步是查詢就緒的操做。

經過Selector的select()方法,能夠查詢出已經就緒的通道操做,這些就緒的狀態集合,包存在一個元素是SelectionKey對象的Set集合中。

下面是Selector幾個重載的查詢select()方法:

(1)select():阻塞到至少有一個通道在你註冊的事件上就緒了。

(2)select(long timeout):和select()同樣,但最長阻塞事件爲timeout毫秒。

(3)selectNow():非阻塞,只要有通道就緒就馬上返回。

select()方法返回的int值,表示有多少通道已經就緒,更準確的說,是自前一次select方法以來到這一次select方法之間的時間段上,有多少通道變成就緒狀態。

一旦調用select()方法,而且返回值不爲0時,下一步工幹啥?

經過調用Selector的selectedKeys()方法來訪問已選擇鍵集合,而後迭代集合的每個選擇鍵元素,根據就緒操做的類型,完成對應的操做:

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

}

處理完成後,直接將選擇鍵,從這個集合中移除,防止下一次循環的時候,被重複的處理。鍵能夠但不能添加。試圖向已選擇的鍵的集合中添加元素將拋出java.lang.UnsupportedOperationException。

1.3. 一個NIO 編程的簡單實例

package com.crazymakercircle.iodemo.base;

import com.crazymakercircle.config.SystemConfig;

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.Iterator;

public class SelectorDemo
{

    static class Client
    {
        /**
         * 客戶端
         */
        public static void testClient() throws IOException
        {
            InetSocketAddress address= new InetSocketAddress(SystemConfig.SOCKET_SERVER_IP, SystemConfig.SOCKET_SERVER_PORT);


            // 一、獲取通道(channel)
            SocketChannel socketChannel =  SocketChannel.open(address);
            // 二、切換成非阻塞模式
            socketChannel.configureBlocking(false);

            // 三、分配指定大小的緩衝區
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put("hello world  ".getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);

            socketChannel.close();
        }

        public static void main(String[] args) throws IOException
        {
            testClient();
        }
    }
    static class Server
    {

        public static void testServer() throws IOException
        {

            // 一、獲取Selector選擇器
            Selector selector = Selector.open();

            // 二、獲取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 3.設置爲非阻塞
            serverSocketChannel.configureBlocking(false);
            // 四、綁定鏈接
            serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

            // 五、將通道註冊到選擇器上,並註冊的操做爲:「接收」操做
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            // 六、採用輪詢的方式,查詢獲取「準備就緒」的註冊過的操做
            while (selector.select() > 0)
            {
                // 七、獲取當前選擇器中全部註冊的選擇鍵(「已經準備就緒的操做」)
                Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                while (selectedKeys.hasNext())
                {
                    // 八、獲取「準備就緒」的時間
                    SelectionKey selectedKey = selectedKeys.next();

                    // 九、判斷key是具體的什麼事件
                    if (selectedKey.isAcceptable())
                    {
                        // 十、若接受的事件是「接收就緒」 操做,就獲取客戶端鏈接
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 十一、切換爲非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 十二、將該通道註冊到selector選擇器上
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    else if (selectedKey.isReadable())
                    {
                        // 1三、獲取該選擇器上的「讀就緒」狀態的通道
                        SocketChannel socketChannel = (SocketChannel) selectedKey.channel();

                        // 1四、讀取數據
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int length = 0;
                        while ((length = socketChannel.read(byteBuffer)) != -1)
                        {
                            byteBuffer.flip();
                            System.out.println(new String(byteBuffer.array(), 0, length));
                            byteBuffer.clear();
                        }
                        socketChannel.close();
                    }

                    // 1五、移除選擇鍵
                    selectedKeys.remove();
                }
            }

            // 七、關閉鏈接
            serverSocketChannel.close();
        }

        public static void main(String[] args) throws IOException
        {
            testServer();
        }
    }
}

2. NIO編程小結

NIO編程的難度比同步阻塞BIO大不少。

請注意以上的代碼中並無考慮「半包讀」和「半包寫」,若是加上這些,代碼將會更加複雜。

(1)客戶端發起的鏈接操做是異步的,能夠經過在多路複用器註冊OP_CONNECT等待後續結果,不須要像以前的客戶端那樣被同步阻塞。

(2)SocketChannel的讀寫操做都是異步的,若是沒有可讀寫的數據它不會同步等待,直接返回,這樣I/O通訊線程就能夠處理其餘的鏈路,不須要同步等待這個鏈路可用。

(3)線程模型的優化:因爲JDK的Selector在Linux等主流操做系統上經過epoll實現,它沒有鏈接句柄數的限制(只受限於操做系統的最大句柄數或者對單個進程的句柄限制),這意味着一個Selector線程能夠同時處理成千上萬個客戶端鏈接,並且性能不會隨着客戶端的增長而線性降低。所以,它很是適合作高性能、高負載的網絡服務器。



源碼:


代碼工程:  JavaNioDemo.zip

下載地址:在瘋狂創客圈QQ羣文件共享。



無編程不創客,無案例不學習。瘋狂創客圈,一大波高手正在交流、學習中!

瘋狂創客圈 Netty 死磕系列 10多篇深度文章博客園 總入口】  QQ羣:104131248

相關文章
相關標籤/搜索