Java NIO之Selector詳細理解

介紹

       Selector一般稱爲選擇器。它是Java NIO核心組件之一,選擇器管理着一個被註冊的通道集合的信息和它們的就緒狀態。通道是和選擇器一起被註冊的,並且使用選擇器來更新通道的就緒狀態。

      

 

可選擇通道(SelectableChannel)

抽象類SelectableChannel提供了實現通道的可選擇性所需要的公共方法。繼承該SelectableChannel的通道(如:socket通道)都是可選擇的,因此可以配置成非阻塞模式。

 

//可以註冊的Channel的抽象

public abstract class SelectableChannel

    extends AbstractInterruptibleChannel

    implements Channel

{
    //返回創建此通道的Provider
    public abstract SelectorProvider provider();


    //返回註冊的SelectionKey(即事件的操作類型)
    public abstract int validOps();


    //判斷當前Channel是否有註冊到任意的Selector
    public abstract boolean isRegistered();


    //獲取該Channel註冊在Selector的SelectionKey(事件類型)
    public abstract SelectionKey keyFor(Selector sel);


     //將Channel註冊到給定的Selector sel上,並指定註冊的事件類型(SelectionKey.OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT等)

    //attachment 爲附屬的resulting key,可以爲null
    //只有非阻塞模型的Channel才能調用register方法
    public abstract SelectionKey register(Selector sel, int ops, Object att)


    //將Channel註冊到給定的Selector sel上,並指定註冊的事件類型(SelectionKey.OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT等)
    public final SelectionKey register(Selector sel, int ops)


    //配置是否是阻塞模式
    public abstract SelectableChannel configureBlocking(boolean block)


    //獲取當前是否是阻塞模式
    public abstract boolean isBlocking();


    //獲取 configureBlocking和register方法同步的鎖
    public abstract Object blockingLock();

}

 

選擇鍵(SelectionKey)

選擇鍵封裝了特定的通道與特定的選擇器的註冊關係。選擇鍵對象被SelectableChannel.register()返回並提供一個表示這種註冊關係的標記。

SelectionKey常用的API如下

     

//選擇鍵封裝了特定的通道與特定的選擇器的註冊關係

public abstract class SelectionKey {

           //返回創建該Key的Channel對象,即使當前key被取消也會返回
           public abstract SelectableChannel channel();
  

           //返回創建該key的Selector
            public abstract Selector selector();
         

             //判斷該key是否可用
             public abstract boolean isValid();


              //請求取消此鍵的通道向其選擇器的註冊。返回時,該密鑰將無效,並將被添加到其選擇器的已取消密鑰集中。在下一次選擇操作期間,該鍵將從所有選擇器的鍵集中刪除。
           public abstract void cancel();


           //  獲取此鍵的 interest 集合。
           public abstract int interestOps();


           //將此鍵的 interest 集合設置爲給定值。
           public abstract SelectionKey interestOps(int ops);


           //獲取該通道已經就緒的操作
           public abstract int readyOps();


           public static final int OP_READ = 1 << 0;  //讀操作事件
           public static final int OP_WRITE = 1 << 2;  //寫操作事件
           public static final int OP_CONNECT = 1 << 3;  //連接事件
           public static final int OP_ACCEPT = 1 << 4;  //接受事件


           //測試此鍵的通道是否已準備好進行讀取。
            public final boolean isReadable()


           //測試此鍵的通道是否已準備好進行寫入。
           public final boolean isWritable()


           // 測試此鍵的通道是否已完成其套接字連接操作。
           public final boolean isConnectable()


            // 測試此鍵的通道是否已準備好接受新的套接字連接。
             public final boolean isAcceptable()


           //將附加對象綁定到SelectionKey上,便於識別給定的通道
           public final Object attach(Object ob)


            //取出綁定在SelectionKey上的附加對象
             public final Object attachment()
}

 

       jdk1.8版本SelectionKey有4中事件,OP_READ(讀事件)、OP_WRITE(寫事件)、OP_CONNECT(連接事件)和OP_ACCEPT(接受事件)。

      

Selector選擇器

              Selector常用的API    

 public abstract class Selector implements Closeable {

    //創建一個選擇器
    public static Selector open()


    //獲取當前選擇器是否已打開
    public abstract boolean isOpen()


    //返回創建此通道的提供者
    public abstract SelectorProvider provider()


   //返回此選擇器的鍵集
    public abstract Set<SelectionKey> keys()


   //返回此選擇器已選擇的鍵
    public abstract Set<SelectionKey> selectedKeys()


   //獲取已經I/O準備就緒的鍵集,不會阻塞
    public abstract int selectNow()


    //阻塞直到註冊到Selector上的Channel有事件發生,或者到了超時時間
    //指定最長等待阻塞的時間,單位ms

    public abstract int select(long timeout)



    //阻塞直到註冊在Selector中的Channel 發送可讀寫事件(或其他註冊事件)
    public abstract int select()

    //喚醒調用select()阻塞的線程
    public abstract Selector wakeup()


    //關閉Selector
    public abstract void close()

}

 

open方法

open()方法用於創建一個Selector對象

Selector selector = Selector.open();

 

將Channel註冊到Selector中

我們需要將 Channel 註冊到Selector 中,這樣才能通過 Selector 監控 Channel 中的事件,注:一個Channel要註冊到Selector中,需要先將這個Channel設置成非阻塞模式。

//設置成非阻塞模式

channel.configureBlocking(false);

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

select方法

       select()和select(long timeout),select方法是Selector的核心方法,這兩個方法會阻塞線程。

       select(long timeout):阻塞直到註冊到Selector上的Channel有事件發生,或者到了超時時間指定最長等待阻塞的時間,單位ms

       select():阻塞直到註冊在Selector中的Channel 發送可讀寫事件(或其他註冊事件)

除了正常收到事件或超時還有三種方法可以喚醒在select()中阻塞的線程

        1. wakeup()

         2. close()

         3. interrupt()

 

獲取就緒的Channel

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();


while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    keyIterator.remove();


    //可能有多個註冊事件就緒
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    }


    if (key.isConnectable()) {
        // a connection was established with a remote server.
    }



    if (key.isReadable()) {
        // a channel is ready for reading
    }


    if (key.isWritable()) {
        // a channel is ready for writing
    }

}

注意, 在每次迭代時, 我們都調用 “keyIterator.remove()” 將這個 key 從迭代器中刪除, 因爲 select() 方法僅僅是簡單地將就緒的 IO 操作放到 selectedKeys 集合中, 因此如果我們從 selectedKeys 獲取到一個 key, 但是沒有將它刪除, 那麼下一次 select 時, 這個 key 所對應的 IO 事件還在 selectedKeys 中.

 

 

attach 和 attachment 方法

 public final Object attach(Object ob)  將附加對象綁定到SelectionKey上,便於識別給定的通道

             public final Object attachment()  取出綁定在SelectionKey上的附加對象

 

ServerSocketChannel和Selector使用示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.nio.charset.Charset;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;



public class SelectSocketsThreadPool {
    private static final int MAX_THREADS = 5;  //線程池數量
    public static int PORT_NUMBER = 7786;  //指定默認端口號
    private ThreadPool  pool = new ThreadPool(MAX_THREADS);



    public static void main(String[] args) throws Exception {
            new SelectSocketsThreadPool().go(args);
    }



    public void go(String [] args) throws Exception {
        int port = PORT_NUMBER;
        if (args.length > 0){
            port = Integer.parseInt(args[0]);
        }
        System.out.println("Listening on port "+port);



        //初始化ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();

        //該ServerSocketChannel對應的ServerSocket
        ServerSocket serverSocket = serverChannel.socket();

        //初始化一個Selector
        Selector selector = Selector.open();


        serverSocket.bind(new InetSocketAddress(port));  //綁定端口
        serverChannel.configureBlocking(false);  //非阻塞



        //綁定 ACCEPT事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);


        while (true){
            //阻塞直到有新的連接
            int n= selector.select();
            if (n == 0){
                continue;
            }

            Iterator it = selector.selectedKeys().iterator();

            while (it.hasNext()){
                SelectionKey key = (SelectionKey)it.next();
                if (key.isAcceptable()) { //key是ACCEPT
                    System.out.println("accept connection!");

                    //獲取該key對應的Channel
                    System.out.println();
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();

                    //接收到的SocketChannel
                    SocketChannel channel = server.accept();
                    registerChannel(selector,channel,SelectionKey.OP_READ);
                    sayHello(channel);
                }



                if (key.isReadable()){
                    readDataFromSocket(key);
                }


                it.remove();
            }
        }
    }



    private void sayHello(SocketChannel channel) throws IOException {
        buffer.clear();
        buffer.put("Hi there!\r\n".getBytes());
        buffer.flip();
        channel.write(buffer);
    }





    private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);



    //連接上來的SocketChannel,註冊到當前Selector上,註冊的操作爲READ
    protected void registerChannel(Selector selector, SocketChannel channel, int opRead) throws IOException {
        if (channel == null){
           return;
        }


        channel.configureBlocking(false);
        channel.register(selector,opRead);
    }





    //讀取數據
    protected void readDataFromSocket(SelectionKey  key) {
        WorkThread workThread = pool.getWorker();
        if (workThread == null){
            return;
        }


        workThread.serviceChannel(key);
    }







    class ThreadPool {
        List idle = new LinkedList();
        //創建指定數量的線程並添加到空閒隊列idle中
        ThreadPool(int poolSize) {
            for (int i = 0 ; i < poolSize; i++){
                WorkThread  thread = new WorkThread(this);
                thread.setName("Worker" + (i+1));
                thread.start();
                idle.add(thread);
           }
        }





        //從隊列中獲取空閒線程
        WorkThread getWorker(){
            WorkThread workThread = null;
            synchronized (idle){
                if (idle.size() > 0){
                    workThread = (WorkThread)idle.remove(0);
                }
            }
            return workThread;
        }



        //將線程返還到空閒線程
        void returnWorker(WorkThread workThread){
            synchronized (idle){
                idle.add(workThread);
            }
        }
    }





    //線程執行的任務類

    class WorkThread extends Thread {
        private ByteBuffer  buffer = ByteBuffer.allocate(1024);
        private ThreadPool pool;
        private SelectionKey key;


        WorkThread(ThreadPool pool){
            this.pool = pool;
        }


        @Override
        public synchronized void run() {
            System.out.println(this.getName() +" is ready");
            while (true){
                try{
                   this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


                if (key == null){
                   continue;
                }


                System.out.println(this.getName() +" has been awakened");
                try{
                    drainChannel(key);  //處理就緒的key
                }catch (Exception e){
                    System.out.println(" caught "+ e + "closing channel");

                    try {
                        key.channel().close();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }


                key = null;
                this.pool.returnWorker(this);  //線程處理完任務放回空閒隊列
            }
        }



        synchronized void serviceChannel(SelectionKey key){
            this.key = key;
            key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
            this.notify();
        }



        //處理就緒的key
        private void drainChannel(SelectionKey key) throws IOException {
            SocketChannel channel = (SocketChannel)key.channel();
            int count;
            buffer.clear();
            while ((count = channel.read(buffer)) > 0 ){
                System.out.println(new String(buffer.array(),0,count));
                buffer.flip();
                while (buffer.hasRemaining()){
                    channel.write(buffer);
                }

                buffer.clear();
            }


            if (count < 0){
                channel.close();
                return;
            }


            key.interestOps(key.interestOps() | SelectionKey.OP_READ);
            key.selector().wakeup();
        }



        private String decode(ByteBuffer bb) {
            Charset charset = Charset.forName("ASCII");
            return charset.decode(bb).toString();
        }

    }

}

 

感謝:

《Java NIO》

https://blog.csdn.net/u014634338/article/details/82865622

http://tutorials.jenkov.com/java-nio/selectors.html

 

 

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