Java NIO使用及原理分析 (四)

轉載自:李會軍•寧靜致遠

在上一篇文章中介紹了關於緩衝區的一些細節內容,現在終於可以進入NIO中最有意思的部分非阻塞I/O。通常在進行同步I/O操作時,如果讀取數據,代碼會阻塞直至有 可供讀取的數據。同樣,寫入調用將會阻塞直至數據能夠寫入。傳統的Server/Client模式會基於TPR(Thread per Request),服務器會爲每個客戶端請求建立一個線程,由該線程單獨負責處理一個客戶請求。這種模式帶來的一個問題就是線程數量的劇增,大量的線程會增大服務器的開銷。大多數的實現爲了避免這個問題,都採用了線程池模型,並設置線程池線程的最大數量,這由帶來了新的問題,如果線程池中有200個線程,而有200個用戶都在進行大文件下載,會導致第201個用戶的請求無法及時處理,即便第201個用戶只想請求一個幾KB大小的頁面。傳統的 Server/Client模式如下圖所示:

NIO中非阻塞I/O採用了基於Reactor模式的工作方式,I/O調用不會被阻塞,相反是註冊感興趣的特定I/O事件,如可讀數據到達,新的套接字連接等等,在發生特定事件時,系統再通知我們。NIO中實現非阻塞I/O的核心對象就是Selector,Selector就是註冊各種I/O事件地 方,而且當那些事件發生時,就是這個對象告訴我們所發生的事件,如下圖所示:

從圖中可以看出,當有讀或寫等任何註冊的事件發生時,可以從Selector中獲得相應的SelectionKey,同時從 SelectionKey中可以找到發生的事件和該事件所發生的具體的SelectableChannel,以獲得客戶端發送過來的數據。關於 SelectableChannel的可以參考Java NIO使用及原理分析(一)

使用NIO中非阻塞I/O編寫服務器處理程序,大體上可以分爲下面三個步驟:

1. 向Selector對象註冊感興趣的事件
2. 從Selector中獲取感興趣的事件
3. 根據不同的事件進行相應的處理

接下來我們用一個簡單的示例來說明整個過程。首先是向Selector對象註冊感興趣的事件:

/*
 * 註冊事件
 * */
protected Selector getSelector() throws IOException {
    // 創建Selector對象
    Selector sel = Selector.open();
    
    // 創建可選擇通道,並配置爲非阻塞模式
    ServerSocketChannel server = ServerSocketChannel.open();
    server.configureBlocking(false);
    
    // 綁定通道到指定端口
    ServerSocket socket = server.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    socket.bind(address);
    
    // 向Selector中註冊感興趣的事件
    server.register(sel, SelectionKey.OP_ACCEPT); 
    return sel;
}

創建了ServerSocketChannel對象,並調用configureBlocking()方法,配置爲非阻塞模式,接下來的三行代碼把該通道綁定到指定端口,最後向Selector中註冊事件,此處指定的是參數是OP_ACCEPT,即指定我們想要監聽accept事件,也就是新的連接發 生時所產生的事件,對於ServerSocketChannel通道來說,我們唯一可以指定的參數就是OP_ACCEPT。

從Selector中獲取感興趣的事件,即開始監聽,進入內部循環:

/*
 * 開始監聽
 * */ 
public void listen() { 
    System.out.println("listen on " + port);
    try { 
        while(true) { 
            // 該調用會阻塞,直到至少有一個事件發生
            selector.select(); 
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();
            while (iter.hasNext()) { 
                SelectionKey key = (SelectionKey) iter.next(); 
                iter.remove(); 
                process(key); 
            } 
        } 
    } catch (IOException e) { 
        e.printStackTrace();
    } 
}

在非阻塞I/O中,內部循環模式基本都是遵循這種方式。首先調用select()方法,該方法會阻塞,直到至少有一個事件發生,然後再使用selectedKeys()方法獲取發生事件的SelectionKey,再使用迭代器進行循環。

最後一步就是根據不同的事件,編寫相應的處理代碼:

/*
 * 根據不同的事件做處理
 * */
protected void process(SelectionKey key) throws IOException{
    // 接收請求
    if (key.isAcceptable()) {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel channel = server.accept();
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_READ);
    }
    // 讀信息
    else if (key.isReadable()) {
        SocketChannel channel = (SocketChannel) key.channel(); 
        int count = channel.read(buffer); 
        if (count > 0) { 
            buffer.flip(); 
            CharBuffer charBuffer = decoder.decode(buffer); 
            name = charBuffer.toString(); 
            SelectionKey sKey = channel.register(selector, SelectionKey.OP_WRITE); 
            sKey.attach(name); 
        } else { 
            channel.close(); 
        } 
        buffer.clear(); 
    }
    // 寫事件
    else if (key.isWritable()) {
        SocketChannel channel = (SocketChannel) key.channel(); 
        String name = (String) key.attachment(); 
        
        ByteBuffer block = encoder.encode(CharBuffer.wrap("Hello " + name)); 
        if(block != null)
        {
            channel.write(block);
        }
        else
        {
            channel.close();
        }

     }
}

此處分別判斷是接受請求、讀數據還是寫事件,分別作不同的處理。

到這裏關於Java NIO使用及原理分析的四篇文章就全部完成了。Java NIO提供了通道、緩衝區、選擇器這樣一組抽象概念,極大的簡化了我們編寫高性能併發型服務器程序,後面有機會我會繼續談談使用Java NIO的一些想法。

(完)

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