Java NIO 詳解---NIO中的異步網絡IO

前面的例子都是關於如何通過NIO操作文件讀寫的,我們知道BIO中的Socket、ServerSocket提供了網絡通信的能力,在NIO中也有對應的模塊提供了這種能力,並且具有更加強大的功能—通過異步非阻塞的數據讀寫實現一個線程監聽多個連接的能力。
1)異步IO
所謂的異步IO是一種沒有阻塞讀寫數據的方法。通常情況下,代碼在調用read()方法時程序會阻塞直到又可以讀取的數據;同樣代碼在寫入數據的時候,代碼會阻塞直到數據寫入完成。而異步IO不會有這種阻塞,相反應用程序將註冊自己感興趣的IO事件—可讀數據的到達、寫入數據完成、新的連接的到來,當這些事情發生的時候系統將會通知應用程序;這樣的好處之一就在於可以使用一個線程操操作多個IO而不用像傳統程序那樣同步的輪詢或需要使用多個線程來處理。
2)Selector
Selector(選擇器)是NIO中能夠監聽一到多個通道,並且知道這些通道是否爲讀寫做好準備的組件,這樣一個線程可以通過管理多個Channel,進而管理多個網絡連接。使用一個線程管理多個網絡連接的好處在於可以避免線程間切換的開銷。
下面示範如何以一個Selector管理Channel。首先是Selector的建立:

//通過靜態的open()方法得到一個Selector
Selector selector = Selector.open();

然後是向Selector註冊一個ServerSocketChannel並監聽連接事件

//對於監聽的端口打開一個ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//註冊到Selector的Channel必須設置爲非阻塞模式,否則實現不了異步IO
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(8410);
serverSocket.bind(address);
//第二個參數是表明這個Channel感興趣的事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

與Selector同時使用的Channel必須處於非阻塞模式,這意味着FileChannel不能用於Selector,因爲它不能切換到非阻塞通道;而套接字通道都是可以的。
register的第二個參數表明了該Channel感興趣的事件,具體的事件分爲四個類型 1.Connect 2.Accept 3.Read 4.Write ,具體來說某個channel成功連接到另一個服務器稱爲“連接就緒”。一個server socket channel準備好接收新進入的連接稱爲“接收就緒”。一個有數據可讀的通道可以說是“讀就緒”。等待寫數據的通道可以說是“寫就緒”。這些事件可以用SelectionKey的四個常量來表示:

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

上面的Channel只是註冊了一個事件,但實際上是可以同時註冊多個事件的,比如可以像下面這樣同時註冊”接收就緒”和”讀就緒”兩個事件:

//使用"|"連接同時註冊多個事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_READ);

3) SelectionKey
上面向Selector註冊Channel後返回了一個SelectionKey對象,這個對象包含了一些很有用的信息集:

interest集合
ready集合
Channel
Selector

interest集合即上面Channel註冊時添加的感興趣的事件集合,我們可以通過調用SelectionKey 的interestOps()方法得到一個int數字,然後通過&位操作來確定具體有哪些感興趣的集合:

int interestSet = key.interestOps();
//是否包含ACCEPT事件
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
//是否包含CONNECT事件
boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;

ready集合表明該Selector上已經就緒的事件,可以通過key.readyOps()獲得一個數字,然後通過上面同樣的方式拿到就緒的集合;但是,也可以使用下面這些更加簡潔的方法判斷:

//四個返回boolean值的方法,可以用於判斷目前Selector上有哪些事件已經就緒
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

可以很簡單的拿到這個SelectinKey關聯的Selector和Channel,如下所示:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

3)監聽Selector選擇通道
當向Selector註冊了幾個Channel之後,就可以調用幾個重載的select()方法來檢測是否有通道已經就緒了。具體的來說,Selector的select()方法有以下三種形式:

int select()
int select(long timeout)
int selectNow()

第一個方法會阻塞直到至少有一個通道就緒然後返回;第二個方法和第一個方法類似但不會一直阻塞而是至多會阻塞timeout時間;第三個方法不會阻塞,無論有無就緒的通道都會立即返回,如果沒有就緒的通道會返回0。這些方法返回的int值表明該Selector上就緒通道的數量,準確的來說是自上次調用select()方法後有多少通道變成就緒狀態。如果調用select()方法,因爲有一個通道變成就緒狀態,返回了1,若再次調用select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了。
如果調用select()方法表明至少有一個通道就緒了,那麼就可以通過selector.selectedKeys()方法來獲得具體就緒的通道,這個方法的返回值是Set。如上面所介紹的我們可以很方便的通過SelectionKey找到就緒的事件以及對應的Channel,下面的代碼示例瞭如何遍歷這個Set:

Set<SelectionKey> selectionKeySet  = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeySet.iterator();
while (iterator.hasNext()){
    SelectionKey selectionKey = iterator.next();
    if(selectionKey.isAcceptable()){
        // a connection was accepted by a ServerSocketChannel.
    }else if(selectionKey.isConnectable()){
        // a connection was established with a remote server.
    }else if(selectionKey.isWritable()){
        // a channel is ready for writing
    }else if(selectionKey.isReadable()){
        // a channel is ready for reading
    }
    iterator.remove();
}

注意末尾的remove()方法,當處理完一個SelectionKey之後,必須手動的將其從Set中移除,Selector本身不會進行這個工作,所以需要我們手動移除避免下一次重複處理。
4)ServerSocketChannel
其實從上面的代碼中我們已經看到了,ServerSocketChannel和ServerSocket所起的作用是一致的,都是用來監聽tcp連接的;值得注意的就是ServerSocketChannel是可以設置爲非阻塞模式的,這時候它的accept()方法在沒有連接進入的情況下總是返回null。下面的代碼示例了ServerSocketChannel的基本用法:

//ServerSocketChannel對象通過靜態方法獲取
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//具體的端口綁定操作還是通過關聯的ServerSocket實現
ServerSocket ss = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(8410);
ss.bind(address);
//ServerSocketChannel可以被設置成非阻塞的模式,這是和Selector配合使用的基礎
serverSocketChannel.configureBlocking(false);
while (true){
    //accept()方法用於監聽進來的連接,如果被設置爲非阻塞模式,那麼當沒有連接時總是返回null
    SocketChannel socketChannel = serverSocketChannel.accept();
    if (socketChannel != null) {
        //do something with socketChannel...
    }
}

5) SocketChannel
Java NIO中的SocketChannel是一個連接到TCP網絡套接字的通道,和Socket是類似的。可以通過以下2種方式創建SocketChannel:
打開一個SocketChannel並連接到互聯網上的某臺服務器。

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",80));

一個新連接到達ServerSocketChannel時,會創建一個SocketChannel。如上面介紹ServerSocketChannel的代碼所示
SocketChannel的數據讀寫和前面介紹的FileChannel沒有什麼不同,都是需要藉助Buffer;值得注意的是SocketChannel是可以工作在非阻塞模式下的,這時候的read()、write()方法都會直接返回,這種模式主要是爲了配合Selector來實現異步非阻塞IO。
最後是一個總的示例:

//測試一個線程同時監聽多個端口並且同時處理多個連接
public static void testSocketNIO() {

    //需要監聽的端口list
    List<Integer> portList = new ArrayList<Integer>();
    portList.add(8410);
    portList.add(8411);
    portList.add(8412);

    //註冊端口監聽事件,並使用異步IO形式使用一個線程監聽多個端口並處理多個連接
    go(portList);


}

public static void go(List<Integer> portList) {
    //進行監聽的選擇器
    Selector selector = null;
    try {
        selector = Selector.open();
    } catch (IOException e) {
        return;
    }
    //每個端口開一個ServerSocketChannel進行監聽
    for (int port : portList) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);//配置成非阻塞模式
            ServerSocket serverSocket = serverSocketChannel.socket();
            InetSocketAddress address = new InetSocketAddress(port);
            serverSocket.bind(address);
            //註冊監聽新連接就緒事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {

        }
    }
    //監聽具體事件
    while (true) {
        int select = 0;
        try {
            select = selector.select();
        } catch (IOException e) {

        }
        if (select == 0) {
            continue;
        }
        //拿到具體就緒的Channel進行處理
        Set<SelectionKey> selectionKeyList = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeyList.iterator();
        while (iterator.hasNext()) {
            SelectionKey selectionKey = iterator.next();
            if (selectionKey.isAcceptable()) { //處理新連接進入事件
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                SocketChannel socketChannel = null;
                try {
                    socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);//新連接同時設置爲非阻塞,同樣使用上面的Selector進行監聽
                    System.out.println("accept new Connection " + socketChannel.getRemoteAddress());
                    socketChannel.register(selector, SelectionKey.OP_READ); //監聽數據傳入事件
                } catch (IOException e) {
                }
            } else if (selectionKey.isReadable()) {
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                ByteBuffer result = ByteBuffer.allocate(1024);
                ByteBuffer byteBuffer = ByteBuffer.allocate(3);
                try {
                    result.clear();
                    while (true) {
                        byteBuffer.clear();
                        int num = socketChannel.read(byteBuffer);
                        if (num == -1) {
                            break;
                        }
                        byteBuffer.flip();
                        result.put(byteBuffer);
                    }
                    result.flip();
                    Charset charset = Charset.forName("UTF-8");
                    CharsetDecoder decoder = charset.newDecoder();
                    System.out.println("receive " + decoder.decode(result).toString() + " from " + socketChannel.getRemoteAddress());
                } catch (IOException e) {

                }
            }
            //處理完這個SelectionKey之後就需要將其移除掉
            iterator.remove();
        }

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