java bio,nio,aio及源碼

版權聲明:本文爲博主原創文章,未經博主容許不得轉載。java

目錄(?)[+]編程

NIO

簡介

  • 隨着JavaIO類庫的不斷髮展和改進,基於Java的網絡編程會變得愈來愈簡單。隨着異步IO功能的加強,基於JavaNIO開發的網絡服務器甚至不遜色與C++開發的網絡程序。
  • 記錄一下學習BIO、NIO編程模型以及JDK1.7提供的NIO2.0的使用。

傳統的BIO編程

  • 這個能夠搜索一下socket,就有不少。
  • 經過一個線程來監聽全部的socket鏈接,鏈接成功則新建線程去處理客戶端操做。
  • 問題是伸縮性差,隨着併發訪問量增大,會很好系統資源,可能形成處理失敗。因爲是阻塞時的讀寫,會形成較大的讀寫延遲。
  • 源碼略。

僞異步IO編程

  • 爲了解決傳統的編程模型問題,有人使用線程池或者消息隊列實現N各線程處理M個客戶端的模型。M遠大於N。

模型圖

僞異步IO模型圖

  • Acceptor是一個線程,經過死循環來監聽socket鏈接,若是有鏈接成功,則新建Runnable對象,提交給線程池處理。

源碼分析

  • 跟BIO的代碼差很少,只是在Server端加了線程池,來處理客戶端socket鏈接。並將鏈接封裝到Runnable對象,並交給ThreadPool處理。

弊端

  • 經過以上模型及代碼分析,很容易知道通訊底層仍是使用的socket,讀寫仍是同步阻塞的,所以,以上優化只是減少了過多建立銷燬線程的開銷,並不能從根本上解決阻塞讀寫產生的問題。

NIO編程

簡介

  • NIO(New I/O),較多人喜歡稱做Non-block I/O.
  • NIO新增了SocketChannel和ServerSocketChannel兩種套接字通道。都支持阻塞和非阻塞兩種模式。
  • 相關概念 
    • 緩衝區Buffer 
      • 緩衝區其實是一個數組,封裝了對數據結構化訪問以及維護讀寫位置等信息。
      • 在NIO庫中,全部數據都是用緩衝區處理的,在讀取數據時,直接讀取到緩衝區。寫入數據時,直接寫入寫緩衝區。任什麼時候候訪問NIO中的數據,都是 經過緩衝區進行操做。
      • 最經常使用的的緩衝區是ByteBuffer。大部分Java基本類型都對應一種緩衝區。類圖以下 
        Buffer類圖
    • 通道channel 
      • Channel 是一個通道,能夠經過它讀取和寫入數據。InputStream和OutputStream各自只能在一個方向上操做。
      • Channel是全雙工的,因此它能夠比流更好地映射底層的api。
      • Channel類圖以下: 
        Channel類圖
    • 多路複用器Selector 
      • Selector是NIO的編程基礎。多路複用器提供選擇已經就緒的任務的能力。
      • Selector會不斷輪詢註冊在其上的Channel,若是channel上面有了新的TCP鏈接、讀取或者寫事件,這個channel就是就緒狀態,會被Selector輪詢出來。而後經過SelectionKey集合能夠獲取就緒的Channel集合,進行IO操做。 
        *一個Selector能夠同時輪詢多個Channel,因爲JDK使用了epoll()代替傳統的select實現,因此沒有最大鏈接句柄1024/2048的限制。這意味着只須要一個線程負責Selector的輪詢,就能夠接入成千上萬的客戶端。

NIO服務端序列圖

  • 步驟1 打開ServerSocketChannel,用於監聽客戶端鏈接,它是全部客戶端鏈接的父管道,代碼示例以下:api

    ServerSocketChannel acceptorSvr = ServerSocketChannel.open();
  • 步驟2 綁定監聽端口,並設置非阻塞模式,示例代碼以下:數組

    acceptorSvr.socket().bind(InetAddress.getByName("IP"),port);
    acceptorSvr.configureBlocking(false);
  • 步驟3 建立Reactor線程,建立多路複用器並啓動線程,代碼以下: 
    Selector selector = Selector.open(); 
    new Thread(new ReactorTask()).start();服務器

  • 步驟4 將ServerSocketChannel註冊到Reactor線程的多路複用器Selector上,監聽ACCEPT事件,代碼以下:網絡

    SelectionKey key = accptorSvr.register(selector,SelectionKey.OP_ACCEPT,ioHandle);
  • 步驟5 Selector在線程run方法內輪詢準備就緒的key,代碼以下:數據結構

    int num = selector.select();
    Set selectedKey = selector.selectedKeys();
    Iterator it = selectedKeys.iterator();
    while(it.hasNext()){
        SelectionKey key = it.next();
        //handle IO operation
    }
  • 步驟6 Selector 監聽到有新的客戶端接入請求,處理新的處理請求,完成TCP三次握手,創建物理鏈路,代碼以下:併發

    SocketChannel channel = svrChannel.accept();
  • 步驟7 設置客戶端鏈路爲非阻塞式,示例代碼以下:異步

    channel.configBlocking(false);
    channel.socket().setReuseAddress(true);
  • 步驟8 將新接入的客戶端鏈接註冊到Reactor線程的Selector上,監聽讀寫操做。用來讀取客戶端發送的網絡信息,代碼以下:socket

    SelectionKey key = socketChannel.register(selector,SelectionKey.OP_ACCEPT,ioHandle);
  • 步驟9 異步讀取客戶端請求消息到緩衝區,代碼以下:

    int num = channel.read(buffer);
  • 步驟10 對ByteBuffer進行編解碼。若是有半包消息指針reset,繼續讀取後續的報文,將讀取的信息封裝成Task交給線程池處理。代碼以下:

    //僞代碼
    while(buffer.hasRemaining()){
        buffer.mark();
        Object message = decode(buffer);
        if(message==null){
            buffer.reset();
            break;
        }
        messageList.add(message);
    }
    if(!buffer.hasRemaining())
        buffer.clear();
    else
        buffer.compact();
    if(messageList!=null&& !messageList.isEmpty()){
        for (Object object:messageList) {
            handleTask(object);
        }
    }
  • 步驟11 將POJO編碼成ButeBuffer.調用SocketChannel的異步write()方法將消息異步發送給客戶端,代碼以下:

    socketChannel.write(buffer);
  • 若是緩衝區滿,會致使寫半包。此時,須要註冊監聽寫操做位,循環寫,知道整包消息寫入TCP緩衝區。

源碼分析

  • 見文末源連接

NIO客戶端處理序列圖

  • 步驟1 打開SocketChannel,綁定客戶端本地地址(可選,默認系統會隨機分配一個可用的本地地址),代碼示例以下:

    SocketChannel clientChannel = SocketChannel.open();
  • 步驟2 設置channel爲非阻塞的,同時設置TCP鏈接參數,代碼示例以下: 
    clientChannel.configBlocking(false); 
    socket.setReuseAddress(true);

  • 步驟3 異步鏈接服務端,代碼示例以下:

    boolean connected = clientChannel.connect(new InetSocketAddress("ip",port));
  • 步驟4 判斷是否鏈接成功,若是成功則註冊到Selector,不然從新鏈接,代碼示例以下:

    if(connected){
        clientChannel.register(selector,SelectionKey.OP_READ,ioHandle);
    }else{
        clientChannel.register(selector,SlectionKey.OP_CONNECT,ioHandle);
    }
  • 步驟5 向Reactor線程的Selector註冊OP_CONNECT狀態位,監聽服務端的TCP ACK應答,代碼示例以下:

  • 步驟6 建立ReActor線程,建立Selector並啓動線程,代碼示例以下:

    Selector selector = Selector.open();
    new Thread(new ReactorTask()).start();
  • 步驟7 Selector在線程run方法中無限循環輪詢準備就緒的key,代碼示例以下:

    int num = selector.selector();
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator it = selectionKeys.iterator();
    while(it.hasNext()){
        SelectionKey selectionKey = it.next();
        //處理IO操做        
    }
  • 步驟8 接收connect事件並處理,代碼示例以下:

    if(key.isConnectable()){
        //處理鏈接請求
    }
  • 步驟9 判斷鏈接結果,若是鏈接成功,則註冊讀事件到selector中,代碼示例以下:

    if(channel.finishConnect()){
        //registerRead()
    }
  • 步驟10 註冊讀事件到selector,代碼示例以下:

    clientChannel.register(selector,SelectionKey.OP_READ,ioHandle);
  • 步驟11 異步讀客戶端請求消息到緩衝區,代碼示例以下:

    int readNumber = channel.read(readBuffer);
  • 步驟12 對讀取到的消息進行編解碼,若是有半包消息接收,緩衝區reset,繼續讀取後續的報文,將解碼成功的消息封裝成Task,丟到線程池中處理。代碼示例以下:

    //僞代碼
    while(buffer.hasRemaining()){
        buffer.mark();
        Object message = decode(buffer);
        if(message==null){
            buffer.reset();
            break;
        }
        messageList.add(message);
    }
    if(!buffer.hasRemaining())
        buffer.clear();
    else
        buffer.compact();
    if(messageList!=null&& !messageList.isEmpty()){
        for (Object object:messageList) {
            handleTask(object);
        }
    }
  • 步驟13 將POJO對象編碼成ByteBuffer,調用SocketChannel的異步write接口將消息異步發送到客戶端,代碼示例以下:

    socketChannel.write(buffer);

客戶端源碼解析

  • 見文末源碼連接

對比

  • NIO編程複雜度比BIO大不少。
  • NIO的優勢 
    • 客戶端發起的鏈接請求是異步的,能夠經過直接在多路複用器註冊OP_CONNECT操做等待後續操做,不須要同步阻塞等待結果可用。
    • SocketChannel的讀寫都是異步的,若是沒有可讀寫的數據它不會同步等待,直接返回,這樣IO通訊線程能夠處理其餘鏈路,不要同步等待這個鏈路可用。
    • 線程模型的優化:因爲JDK的Selector在Linux等主流OS中經過epoll實現,沒有鏈接句柄數的限制,這意味着Selector線程能夠同時處理成千上萬的客戶端鏈接,並且性能不會隨着客戶端的增長而線性降低,所以很是適合作高性能、高併發的網絡服務器。
  • JDK7中升級了NIO類庫,升級後的NIO類庫被稱爲NIO2.0.正式提出了異步文件IO操做,同時提供了與UNIX網絡編程事件驅動IO對應的AIO。

AIO編程

簡介

  • NIO2.0引入了新的異步通道的概念,並提供了異步文件通道和異步套接字通道的實現。
  • 異步通道提供兩種方式獲取結果: 
    • 經過java.util.concurrent.Future類來表示異步操做的結果
    • 在執行異步操做的時候傳入一個java.nio.channels.
  • CompletionHandle接口的實現類做爲操做完成的回調。 
    *NIO2.0的異步套接字通道都是真正的異步阻塞的IO,它對應UNIX網絡編程中的事件驅動IO(AIO),它不須要經過多路複用器對註冊的通道進行輪詢操做便可實現異步讀寫,從而簡化了NIO的編程模型。

源碼分析

  • 見文末源碼連接

4種IO的對比

  • 阻塞與非阻塞。異步與同步 
    • 阻塞與非阻塞指的是當不能進行讀寫時(網卡滿時的寫或者網卡空時的讀),IO操做當即返回仍是阻塞。
    • 同步異步指的是當數據ready時讀寫操做是同步讀仍是異步讀。–以上解釋摘自知乎。
  • 不一樣IO模型因爲線程模型、api等差異很大,用法差別也挺大。幾種IO模型的功能和對好比下表:
IO模型的功能和特性對比
  同步阻塞IO(BIO) 僞異步 非阻塞IO(NIO) 異步IO(AIO)
客戶端個數:IO線程數 1:1 M:N(通常M大於N) M:1 M:0(不須要額外啓動線程,被動回調)
IO類型(阻塞) 阻塞IO 阻塞 非阻塞IO 非阻塞IO
IO類型(同步) 同步 同步 同步 異步
API使用難度 簡單 簡單 複雜 複雜
調試難度 簡單 簡單
可靠性 很是差
吞吐量

總結

  • 本文是筆者閱讀Netty權威指南所做的筆記,感謝李林峯老師。
  • 若是你閱讀了本文,發現了書寫錯誤,請直接評論。感謝!
  • 我將書中的源碼分析部分寫入到代碼中了,若有須要可自行下載。連接:http://download.csdn.net/detail/chenzhao2013/9729887
相關文章
相關標籤/搜索