【NIO】Java NIO之選擇器

leesf發表於2017-04-24

一、前言

  前面已經學習了緩衝和通道,接著學習選擇器。

二、選擇器

  2.1 選擇器基礎

  選擇器管理一個被註冊的通道集合的資訊和它們的就緒狀態,通道和選擇器一起被註冊,並且選擇器可更新通道的就緒狀態,也可將被喚醒的執行緒掛起,直到有通道就緒。

  SelectableChannel 可被註冊到 Selector 物件上,同時可以指定對那個選擇器而言,哪種操作是感興趣的。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言,只能被註冊一次,通道在被註冊到一個選擇器上之前,必須先設定為非阻塞模式,通過呼叫通道的configureBlocking(false)方法即可。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式,而套接字通道都可以。

  選擇鍵封裝了特定的通道與特定的選擇器的註冊關係,選擇鍵物件被SelectableChannel.register( ) 方法返回並提供一個表示這種註冊關係的標記。選擇鍵包含了兩個位元集(以整數的形式進行編碼),指示了該註冊關係所關心的通道操作及通道已經準備好的操作。

  如下程式碼演示了通道與選擇器之間的關係 

Selector selector = Selector.open( );
channel1.register (selector, SelectionKey.OP_READ);
channel2.register (selector, SelectionKey.OP_WRITE);
channel3.register (selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// Wait up to 10 seconds for a channel to become ready
readyCount = selector.select (10000);

  三個通道註冊到了選擇器上,並且感興趣的操作各不相同,select( )方法在將執行緒置於睡眠狀態,直到感興趣的操作中的一個發生或者等待10秒鐘的時間。

  現有的可選操作有讀(read),寫(write),連線(connect)和接受(accept)等四種操作,並非所有的操作都在所有的可選擇通道上被支援。例如SocketChannel 不支援 accept。

  2.2 使用選擇鍵

  一個鍵表示了一個特定的通道物件和一個特定的選擇器物件之間的註冊關係。當鍵被取消時,它將被放在相關的選擇器的已取消的鍵的集合裡。註冊不會立即被取消,但鍵會立即失效。

  通道不會在鍵被取消的時候立即登出。直到下一次操作發生為止,它們仍然會處於被註冊的狀態。

  一個 SelectionKey 物件包含兩個以整數形式進行編碼的位元掩碼:一個用於指示那些通道/選擇器組合體所關心的操作(insterest 集合),另一個表示通道準備好要執行的操作( ready 集合)。可以通過呼叫鍵的 readyOps( )方法來獲取相關的通道的已經就緒的操作,ready集合是interest集合的子集,表示interest集合中從上次呼叫select( )以來已經就緒的那些操作。

  2.3 使用選擇器

  已註冊的鍵的集合:與選擇器關聯的已經註冊的鍵的集合,並非所有註冊過的鍵都仍然有效。這個集合通過keys( )方法返回,可能為空。這個已註冊的鍵的集合不可直接修改。

  已選擇的鍵的集合:已註冊的鍵的集合的子集。該集合的每個成員都是相關的通道被選擇器(在前一個select操作中)判斷為已為就緒狀態,並且包含於鍵的 interest 集合中的操作。

  已取消的鍵的集合:已註冊的鍵的集合的子集,這個集合包含了 cancel( )方法被呼叫過的鍵(這個鍵已經被無效化),但它們還沒有被登出。

  有如下三種方式可以喚醒在 select( )方法中睡眠的執行緒。

  ① wakeup方法,wakeup( )方法將使得選擇器上的第一個還沒有返回的選擇操作立即返回。如果當前沒有在進行中的選擇,那麼下一次對 select( )方法的呼叫將立即返回,後續的選擇操作將正常進行。有時並不想要這種延遲的喚醒行為,而只想喚醒一個睡眠中的執行緒,後續的選擇繼續正常地進行,此時可以通過在呼叫 wakeup( )方法後呼叫 selectNow( )方法解決該問題。

  ② close方法,close( )方法會使得任何一個在select操作中阻塞的執行緒都將被喚醒,如同呼叫wakeup( )方法,與選擇器相關的通道將被登出,而鍵將被取消。

  ③ interrupt方法,如果睡眠中的執行緒的 interrupt( )方法被呼叫,它的返回狀態將被設定。如果被喚醒的執行緒之後將試圖在通道上執行 I/O 操作,通道將立即關閉,然後執行緒將捕捉到一個異常。

  下面示例展示了Selector和通道的基本使用方法  

import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.nio.channels.SelectableChannel;
import java.net.ServerSocket;
import java.net.InetSocketAddress;
import java.util.Iterator;

/**
 * Created by LEESF on 2017/4/24.
 */
public class SelectorDemo {
    public static int PORT_NUMBER = 1234;

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

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

        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverChannel.socket();
        Selector selector = Selector.open();
        serverSocket.bind(new InetSocketAddress(port));
        serverChannel.configureBlocking(false);
        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()) {
                    ServerSocketChannel server =
                            (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    registerChannel(selector, channel,
                            SelectionKey.OP_READ);
                    sayHello(channel);
                }
                if (key.isReadable()) {
                    readDataFromSocket(key);
                }
                it.remove();
            }
        }
    }

    private void registerChannel(Selector selector,
                                   SelectableChannel channel, int ops) throws Exception {
        if (channel == null) {
            return;
        }
        channel.configureBlocking(false);
        channel.register(selector, ops);
    }

    private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    private void readDataFromSocket(SelectionKey key) throws Exception {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int count;
        buffer.clear();
        while ((count = socketChannel.read(buffer)) > 0) {
            buffer.flip();
            while (buffer.hasRemaining()) {
                socketChannel.write(buffer);
            }
            buffer.clear();
        }
        if (count < 0) {
            socketChannel.close();
        }
    }

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

  在多執行緒的場景中,如果需要對任何一個鍵的集合進行更改,不管是直接更改還是其他操作帶來的副作用,都需要首先以相同的順序,在同一物件上進行同步。

  2.4 選擇過程的可擴充套件性

  如下示例使用執行緒池來為通道提供服務。   

import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.SelectionKey;
import java.util.List;
import java.util.LinkedList;
import java.io.IOException;

/**
 * Created by LEESF on 2017/4/24.
 */
public class SelectSocketsThreadPool extends SelectorDemo{
    private static final int MAX_THREADS = 5;
    private ThreadPool pool = new ThreadPool(MAX_THREADS);
    public static void main(String[] argv) throws Exception {
        new SelectSocketsThreadPool().go(argv);
    }

    protected void readDataFromSocket(SelectionKey key) throws Exception {
        WorkerThread worker = pool.getWorker();
        if (worker == null) {
            return;
        }
        worker.serviceChannel(key);
    }
    private class ThreadPool {
        List idle = new LinkedList();
        ThreadPool(int poolSize) {
            for (int i = 0; i < poolSize; i++) {
                WorkerThread thread = new WorkerThread(this);
                thread.setName("Worker" + (i + 1));
                thread.start();
                idle.add(thread);
            }
        }

        WorkerThread getWorker() {
            WorkerThread worker = null;
            synchronized (idle) {
                if (idle.size() > 0) {
                    worker = (WorkerThread) idle.remove(0);
                }
            }
            return (worker);
        }

        void returnWorker(WorkerThread worker) {
            synchronized (idle) {
                idle.add(worker);
            }
        }
    }

    private class WorkerThread extends Thread {
        private ByteBuffer buffer = ByteBuffer.allocate(1024);
        private ThreadPool pool;
        private SelectionKey key;
        WorkerThread(ThreadPool pool) {
            this.pool = pool;
        }
        public synchronized void run() {
            System.out.println(this.getName() + " is ready");
            while (true) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    this.interrupted();
                }
                if (key == null) {
                    continue; // just in case
                }
                System.out.println(this.getName() + " has been awakened");
                try {
                    drainChannel(key);
                } catch (Exception e) {
                    System.out.println("Caught '" + e
                            + "' closing channel");
                    try {
                        key.channel().close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                    key.selector().wakeup();
                }
                key = null;
                this.pool.returnWorker(this);
            }
        }

        synchronized void serviceChannel(SelectionKey key) {
            this.key = key;
            key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
            this.notify();
        }
        void drainChannel(SelectionKey key) throws Exception {
            SocketChannel channel = (SocketChannel) key.channel();
            int count;
            buffer.clear();
            while ((count = channel.read(buffer)) > 0) {
                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();
        }
    }
}

   下面示例使用Selector完成客戶端與服務端的通訊,其中SelectorServerSocketChannel為服務端,SelectorSocketChannel為客戶端,先啟動服務端,然後啟動客戶端,連線成功後,客戶端傳送資訊至服務端,服務端收到資訊後,反饋資訊給客戶端。

  SelectorServerSocketChannel 

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;

/**
 * Created by LEESF on 2017/4/24.
 */
public class SelectorServerSocketChannel {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocketChannel.configureBlocking(false);
        serverSocket.bind(new InetSocketAddress("localhost", 1234));

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            selector.select();
            Iterator it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel server =
                            (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Connected: " + channel.socket().getRemoteSocketAddress());
                }
                if (key.isReadable()) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    socketChannel.read(byteBuffer);
                    byteBuffer.flip();
                    System.out.println("server received message: " + getString(byteBuffer));
                    byteBuffer.clear();
                    String message = "server sending message " + System.currentTimeMillis();
                    System.out.println("server sending message: " + message);
                    byteBuffer.put(message.getBytes());
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                }

                it.remove();
            }
        }
    }

    private static String getString(ByteBuffer buffer) {
        Charset charset;
        CharsetDecoder decoder;
        CharBuffer charBuffer;
        try {
            charset = Charset.forName("UTF-8");
            decoder = charset.newDecoder();
            charBuffer = decoder.decode(buffer.asReadOnlyBuffer());
            return charBuffer.toString();
        } catch (Exception ex) {
            ex.printStackTrace();
            return "";
        }
    }
}

  SelectorSocketChannel客戶端 

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.channels.Selector;
import java.util.Iterator;

/**
 * Created by LEESF on 2017/4/24.
 */
public class SelectorSocketChannel {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        socketChannel.connect(new InetSocketAddress("localhost",1234));
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

        while (true) {
            selector.select();
            Iterator it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                it.remove();
                if (key.isConnectable()) {
                    if (socketChannel.isConnectionPending()) {
                        if (socketChannel.finishConnect()) {
                            key.interestOps(SelectionKey.OP_READ);
                            sendMessage(socketChannel);
                        } else {
                            key.cancel();
                        }
                    }
                }
                if(key.isReadable()) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
                    while (true) {
                        byteBuffer.clear();
                        int count = socketChannel.read(byteBuffer);
                        if (count > 0) {
                            byteBuffer.flip();
                            System.out.println("client receive message: " + getString(byteBuffer));
                            break;
                        }
                    }
                }
            }
        }
    }

    private static void sendMessage(SocketChannel socketChannel) throws Exception {
        String message = "client sending message " + System.currentTimeMillis();
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        byteBuffer.clear();
        System.out.println("client sending message: " + message);
        byteBuffer.put(message.getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
    }

    private static String getString(ByteBuffer buffer) {
        Charset charset;
        CharsetDecoder decoder;
        CharBuffer charBuffer;
        try {
            charset = Charset.forName("UTF-8");
            decoder = charset.newDecoder();
            charBuffer = decoder.decode(buffer.asReadOnlyBuffer());
            return charBuffer.toString();
        } catch (Exception ex) {
            ex.printStackTrace();
            return "";
        }
    }
}

  客戶端輸出結果:  

client sending message: client sending message 1493032984099
client receive message: server sending message 1493032984101

  服務端輸出結果: 

Connected: /127.0.0.1:49859
server received message: client sending message 1493032984099
server sending message: server sending message 1493032984101

三、總結

  本篇博文講解了選擇器的基礎知識點,使用選擇器可以大幅度的提升系統的效能,使得開發更為便捷,至此,整個NIO的內容就學到這裡,之後會學習Netty,同時,所有原始碼已經上傳至github,歡迎star,也謝謝各位園友的觀看~ 

相關文章