NIO原理剖析與Netty初步----淺談高性能服務器開發(一)

    除特別註明外,本站全部文章均爲原創,轉載請註明地址 數據庫

    在博主不長的工做經歷中,NIO用的並很少,因爲使用原生的Java NIO編程的複雜性,大多數時候咱們會選擇Netty,mina等開源框架,但理解NIO的原理就不重要了嗎?偏偏相反,理解NIO底層機制是理解這一切的基礎,由此我總結一下當初學習NIO時的筆記,以便後續複習。編程

     如下是我理解的Java原生NIO開發大體流程:服務器

    

       上圖大體描述的是服務端的NIO操做。框架

第一步,綁定一個服務的端口

       這與傳統阻塞IO中的ServerSocket相似,沒什麼好說的socket

第二步,打開通道管理器Selector並在Selector上註冊一個事件

      當註冊的事件發生時,Selector.select()會返回,不然一直阻塞。這一步頗有意思,也是NIO第一個與傳統IO不一樣的地方,NIO經過一個Selector線程能夠管理大量客戶端鏈接,反之傳統IO一個客戶端鏈接進來必須建立一個新的線程爲它服務(固然你可使用鏈接池),咱們知道線程對服務端來講是十分寶貴的資源,一個服務端進程所包含的線程是有   限的;此外,每一個線程會佔用必定的內存空間,過多的線程可能致使內存溢出,這種狀況下你可能會到想對虛擬機進行調優,好比經過修改參數-Xss限制單個線程大小,但這又可   能致使StackOverFlow;另外,線程調度須要切換上下文,對於操做系統,它須要經過TCB(線程控制塊)來對線程進行調度,過多的上下文切換浪費了CPU時間,下降了系統效     率。學習

第三步,輪循訪問Selector,當註冊的事件到達時,方法返回

   下面的代碼能夠看到,方法總體是一個死循環,輪詢訪問Selector,發生某些已經註冊在Selector上的事件時,該方法返回。能夠經過selector.selectedKeys獲取發生的事件,this

該方法返回的是一個泛型集合Set<SelectionKey>,遍歷這個集合與遍歷普通集合沒有什麼不一樣,這裏咱們經過迭代器迭代的緣由是咱們須要刪除已經處理的Key,避免重複處理:spa

public void listen() throws IOException {// 輪詢訪問selector
        while (true) {
            // 當註冊的事件到達時,方法返回;不然,該方法會一直阻塞
            selector.select();
            // 得到selector中選中的項的迭代器,選中的項爲註冊的事件
            Iterator<?> ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                //刪除已選的key,以防重複處理
                ite.remove();
         //這裏能夠寫咱們本身的處理邏輯
                handle(key);
            }
        }
}

   在第二步時,已經在Selector上註冊了Accept事件,當這裏的selector.select()返回時,表明客戶端已經能夠鏈接了,在handle方法裏能夠處理這個事件:操作系統

public void handle(SelectionKey key) throws IOException {
    // 客戶端請求鏈接事件
    if (key.isAcceptable()){
//從Key裏能夠很方便的取到註冊這個事件的Channel

        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        // 得到和客戶端鏈接的通道
        SocketChannel channel = server.accept();
        // 設置成非阻塞
        channel.configureBlocking(false);線程

        logger.info("客戶端已經鏈接!");

        //客戶端鏈接在通道管理器Selector上註冊讀事件

        channel.register(this.selector, SelectionKey.OP_READ);

    }
}

    上面的代碼很簡單,咱們經過key獲取到註冊它的那個Channel,在這裏是ServerSocketChannel,經過server.accept()獲取客戶端鏈接,這裏一樣能夠類比到傳統的阻塞

IO,在阻塞IO中咱們能夠經過ServerSocket.accept獲取到socket,惟一不一樣的是,阻塞IO中的accept方法是阻塞操做,而NIO中是非阻塞的。

    固然,僅僅是鏈接到客戶端並無什麼用處,服務端須要有讀寫數據的能力,好比你能夠用NIO實現一個Http服務器(固然最佳實踐使用Netty等框架)。因此咱們須要在Selector

上註冊讀事件,一樣,當讀事件發生時,執行咱們本身的業務邏輯。下面是修改後的代碼:

public void handle(SelectionKey key) throws IOException {
    if (key.isAcceptable()){
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel channel = server.accept();
        channel.configureBlocking(false);
        logger.info("客戶端已經鏈接!");
        channel.register(this.selector, SelectionKey.OP_READ);
    } else if(key.isReadable()){
        SocketChannel channel = (SocketChannel) key.channel();
        // 建立讀取緩衝
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //讀取到Buffer中
        int read = channel.read(buffer);
        if(read > 0){
            byte[] data = buffer.array();
            String msg = new String(data).trim();
            logger.info("receive msg: {}",msg)
            
            //回寫數據
            ByteBuffer outBuffer = ByteBuffer.wrap("OK".getBytes());
            channel.write(outBuffer);
        }else{
            logger.info("client closed!!!");
            key.cancel();
        }
    }
} 

總結

     本文大體講述了使用NIO進行服務器端開發的大體流程,但代碼顯然仍然存在問題,其一是咱們只使用了一個線程執行全部操做,包括接收客戶端鏈接,讀取數據,返回數據,對於這個簡單的Demo來講已經足夠了,但在實際的服務器開發中,例如你想使用NIO開發本身的HTTP服務器,服務器本地須要作大量操做,包括解析用戶請求,根據請求路由到某一個Action執行業務邏輯,這其中又極可能某些數據從數據庫讀取,渲染模板等操做,十分耗時,這無疑又稱爲系統的瓶頸,再者,使用單一線程不能充分利用多核CPU提供的計算能力。下一篇中會看到,在基於Reactor模型的Netty中,會使用一個Boss線程接收客戶端請求,使用多個Worker線程執行具體的業務邏輯。

相關文章
相關標籤/搜索