Java NIO 學習(六)--Selector

在之前講解的網絡相關的channel,都有講到非阻塞模式,只簡單說明了那些方法在非阻塞模式下的返回情況,並沒有實際的應用;本節要講到的selector就是NIO中非阻塞模式使用的一大優點;

一、概述

selector,選擇器,同過一個選擇器,程序可以通過一個線程處理多個channel,而不需要像之前ServerSocketChannel那樣每接收一個請求都單開一個線程處理通信;selector基於事件驅動的方式處理多個通道I/O;

二、selector使用

1、創建

一個selector的創建,都是通過簡單open靜態方法獲取:

Selector selector = Selector.open();

2、通道註冊

通道要通過selector管理,必須先將通道註冊到一個selector上:

serverChannel.configureBlocking(false);
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);

1.register方法是在SelectableChannel抽象類中定義的,所以只有基礎了SelectableChannel類的通道類型纔可註冊到selector,如ServerSocketChannel、SocketChannel,而且通道必須是設置爲非阻塞模式,所以想FileChannel通道,是不能與selector配合使用的;
2.register方法中的第二個參數,表示這個通道註冊所感興趣的事件,總共有4個取值:

connect-連接事件

accept-連接接收事件

read-讀事件

write-寫事件
如果有多個感興趣事件,入參可以使用 | 操作

3.register方法的返回值是一個SelectionKey對象,後面再詳細講解這個對象;

3、通過selector選擇通道

  1. 向一個selector註冊完一個或多個通道後,就可以通過三個select方法獲取感興趣事件已就緒的通道:

int select() 阻塞直至有一個註冊通道的事件發送

int select(long timeout) 阻塞超時時間爲timeout

int selectNow() 不阻塞,立即返回,如沒有通道事件發生,返回值爲0

select方法返回的int值表示有多少個通道在上一次select後發生了註冊感興趣事件

  1. select方法在阻塞期間,如果有其它線程調用了selector的wakeUp方法,正在阻塞的select方法會立即返回,如果wakeUp方法調用時,selector沒有select方法在阻塞,那麼下次有調用select方法會立即返回;

  2. 調用select方法得知有一個或多個通道就緒後,通過selectedKeys方法獲取已選擇鍵值(select key set)

Set<SelectionKey> selectedKeys = selector.selectedKeys();

註冊通道時register方法也是會返回一個SelectionKey對象,可以認爲該對象包裝了對應的通道,可以通過SelectionKey對象獲取以下內容:

//獲取註冊的感興趣事件集,可以通過
//interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;或者
//key.isConnectable(); 判斷事件類型
int interestOps = key.interestOps();
//獲取已經就緒的事件集,類型判斷同上
int readyOps = key.readyOps();
//獲取附加對象,該對象可以通過key.attach(obj);方法添加
//也可以在通道註冊的時候通過register方法第三個參數帶入
//改附加對象可以是通道實用緩存區、用於判斷通道的標識等
Object attachment = key.attachment();
//獲取這個key所對應的通道對象
SelectableChannel channel2 = key.channel();
//獲取這個key所對應的selector
Selector selector = key.selector();
  1. 獲取到已選擇鍵值(其實是已就緒的通道),就可以遍歷處理這個就緒的集合了,一般方式如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
    SelectionKey selectionKey = iterator.next();
    if (selectionKey.isAcceptable()) {
        //連接請求時間
    } else if (selectionKey.isReadable()) {
        //可讀事件 
    }
    else if (selectionKey.isConnectable()) {
        //連接事件
    }
    else if (selectionKey.isWritable()){
        //可寫事件
    }
    //處理完成後,需要移除
    iterator.remove();
}

通過4個isXXXXable判斷事件類型,並可類型轉換爲對應的通道類型處理IO事件;
每次循環後需要移除處理完的事件,否則下次selectedKeys()還會再次獲取到這事件;

三、實例說明

在ServerSocketChannel與SocketChannel一節的例子中,演示一個簡單的網絡通信:服務端使用主線程接收請求,每成功接收到一個請求後,創建一個獨立的線程處理與客戶端的通信;本節使用selector改造這個演示,只使用一個線程處理請求和通信:

客戶端:

public class ChannelSelector {

    public static void main(String args[]) throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(1234));
        serverChannel.configureBlocking(false);

        Selector selector = Selector.open();
        SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            int select = selector.select();
            if (select > 0) {
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectedKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    // 接收連接請求
                    if (selectionKey.isAcceptable()) {
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey
                                .channel();
                        SocketChannel socketChannel = channel.accept();
                        System.out.println("接收到連接請求:"
                                + socketChannel.getRemoteAddress().toString());
                        socketChannel.configureBlocking(false);
                        //每接收請求,註冊到同一個selector中處理
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
                        // read
                        receiveMessage(selectionKey);
                    }
                    iterator.remove();
                }
            }
        }
    }

    public static void receiveMessage(SelectionKey selectionKey)
            throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        String remoteName = socketChannel.getRemoteAddress().toString();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
        StringBuilder sb = new StringBuilder();
        byte b[];
        try {
            sizeBuffer.clear();
            int read = socketChannel.read(sizeBuffer);
            if (read != -1) {
                sb.setLength(0);
                sizeBuffer.flip();
                int size = sizeBuffer.getInt();
                int readCount = 0;
                b = new byte[1024];
                // 讀取已知長度消息內容
                while (readCount < size) {
                    buffer.clear();
                    read = socketChannel.read(buffer);
                    if (read != -1) {
                        readCount += read;
                        buffer.flip();
                        int index = 0;
                        while (buffer.hasRemaining()) {
                            b[index++] = buffer.get();
                            if (index >= b.length) {
                                index = 0;
                                sb.append(new String(b, "UTF-8"));
                            }
                        }
                        if (index > 0) {
                            sb.append(new String(b, "UTF-8"));
                        }
                    }
                }
                System.out.println(remoteName + ":" + sb.toString());
            }
        } catch (Exception e) {
            System.out.println(remoteName + " 斷線了,連接關閉");
            try {
                //取消這個通道的註冊,關閉資源
                selectionKey.cancel();
                socketChannel.close();
            } catch (IOException ex) {
            }
        }
    }
}

服務端還是與之前一樣:

public class SocketChanneClient {

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

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(1234));
        while (true) {
            Scanner sc = new Scanner(System.in);
            String next = sc.nextLine();

            sendMessage(socketChannel, next);
        }
    }

    public static void sendMessage(SocketChannel socketChannel, String mes) throws IOException {
        if (mes == null || mes.isEmpty()) {
            return;
        }
        byte[] bytes = mes.getBytes("UTF-8");
        int size = bytes.length;
        ByteBuffer buffer = ByteBuffer.allocate(size);
        ByteBuffer sizeBuffer = ByteBuffer.allocate(4);

        sizeBuffer.putInt(size);
        buffer.put(bytes);

        buffer.flip();
        sizeBuffer.flip();
        ByteBuffer dest[] = {sizeBuffer,buffer};
        System.out.println("send message size=" + size + ",content=" + mes);
        while (sizeBuffer.hasRemaining() || buffer.hasRemaining()) {
            socketChannel.write(dest);
        }
    }
}

四、selector非阻塞IO的優點

1、阻塞IO的缺點:

  1. 當客戶端連接多時,需要使用大量線程處理,佔用更多的系統資源;
  2. 多個線程間的切換許多情況下是無意義的,因爲未知阻塞時間;

2、非阻塞IO的優點:

  1. 由一個線程來專門處理所有IO事件,並可分發;
  2. 基於事件驅動機制,當事件就緒時觸發,而不是同步監視事件;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章