1. 程式人生 > >Selector (死磕4)

Selector (死磕4)

【正文】JAVA NIO 死磕4: 

NIO Selector 死磕


1.1. Selector入門

1.1.1. Selector的和Channel的關係

Java NIO的核心元件包括:

(1)Channel(通道)

(2)Buffer(緩衝區)

(3)Selector(選擇器)

其中Channel和Buffer比較好理解 ,聯絡也比較密切,他們的關係簡單來說就是:資料總是從通道中讀到buffer緩衝區內,或者從buffer寫入到通道中。

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

選擇器(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()來例項化的,如下:

  // 1、獲取Selector選擇器

            Selector selector = Selector.open();

Selector的類方法open()內部是向SPI發出請求,通過預設的SelectorProvider物件獲取一個新的例項。

1.2.2. 將Channel註冊到Selector

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

            // 2、獲取通道

            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            // 3.設定為非阻塞

            serverSocketChannel.configureBlocking(false);

            // 4、繫結連線

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

            // 5、將通道註冊到選擇器上,並制定監聽事件為:“接收”事件

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


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

            // 3、分配指定大小的緩衝區
            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
        {

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

            // 2、獲取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 3.設定為非阻塞
            serverSocketChannel.configureBlocking(false);
            // 4、繫結連線
            serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

            // 5、將通道註冊到選擇器上,並註冊的操作為:“接收”操作
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            // 6、採用輪詢的方式,查詢獲取“準備就緒”的註冊過的操作
            while (selector.select() > 0)
            {
                // 7、獲取當前選擇器中所有註冊的選擇鍵(“已經準備就緒的操作”)
                Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                while (selectedKeys.hasNext())
                {
                    // 8、獲取“準備就緒”的時間
                    SelectionKey selectedKey = selectedKeys.next();

                    // 9、判斷key是具體的什麼事件
                    if (selectedKey.isAcceptable())
                    {
                        // 10、若接受的事件是“接收就緒” 操作,就獲取客戶端連線
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 11、切換為非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 12、將該通道註冊到selector選擇器上
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    else if (selectedKey.isReadable())
                    {
                        // 13、獲取該選擇器上的“讀就緒”狀態的通道
                        SocketChannel socketChannel = (SocketChannel) selectedKey.channel();

                        // 14、讀取資料
                        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();
                    }

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

            // 7、關閉連線
            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群檔案共享。


瘋狂創客圈:如果說Java是一個武林,這裡的聚集一群武痴, 交流程式設計體驗心得
QQ群連結:
瘋狂創客圈QQ群


無程式設計不創客,無案例不學習。 一定記得去跑一跑案例哦


JAVA NIO 死磕全目錄


1. JAVA NIO簡介
1.1. NIO 和 OIO 的對比
1.2. 阻塞和非阻塞
1.3. Channel
1.4. selector
1.5. Java NIO Buffer
2. Java NIO Buffer
2.1. Buffer型別的標記屬性
2.1.1. capacity
2.1.2. position
2.1.3. limit
2.1.4. 總結:
2.2. Buffer 型別
2.3. Buffer中的方法
2.3.1. 獲取allocate()方法
2.3.2. 寫put()方法
2.3.3. 讀切換flip()方法
2.3.4. 讀get() 方法
2.3.5. 倒帶rewind()方法
2.3.6. mark( )和reset( )
2.3.7. clear()清空
2.4. Buffer 的使用
2.4.1. 使用的基本步驟
2.4.2. 完整的例項程式碼
3. Java NIO Channel
3.1. Java NIO Channel的特點
3.2. Channel型別
3.3. FileChannel
3.4. SocketChannel
3.4.1. 監聽連線
3.4.2. 非阻塞模式
3.5. DatagramChannel
4. NIO Selector
4.1. Selector入門
4.1.1. Selector的和Channel的關係
4.1.2. 可選擇通道(SelectableChannel)
4.1.3. Channel註冊到Selector
4.1.4. 選擇鍵(SelectionKey)
4.2. Selector的使用流程
4.2.1. 建立Selector
4.2.2. 將Channel註冊到Selector
4.2.3. 輪詢查詢就緒操作
4.3. 一個NIO 程式設計的簡單例項