Java NIO淺析

準備知識

同步、異步、阻塞、非阻塞

同步和異步說的是服務端消息的通知機制,阻塞和非阻塞說的是客戶端線程的狀態。
已客戶端一次網絡請求爲例作簡單說明:html

  • 同步
    同步是指一次請求沒有獲得結果以前就不返回。java

  • 異步
    請求不會馬上獲得最終結果,服務器處理完成再異步通知客戶端。react

  • 阻塞
    請求結果返回以前,當前線程被掛起。在此期間不能作任何其餘的事情。linux

  • 非阻塞
    請求當即返回,後續由客戶端時不時的詢問服務器結果或者服務器異步回調。編程

同步IO、異步IO、阻塞IO、非阻塞IO

一般來講,IO操做包括:對硬盤的讀寫、對socket的讀寫以及外設的讀寫。
已一個IO讀取過程爲例作簡要說明(如圖):服務器

  1. DMA把數據讀取到內核空間的緩衝區(讀就緒)網絡

  2. 內核將數據拷貝到用戶空間。多線程

io%E5%8E%9F%E7%90%86.png

內核空間是用戶代碼沒法控制的,因此用戶空間在讀取以前,首先會判斷是否已經讀就緒。架構

  • 同步IO
    當用戶發出IO請求操做以後,內核會去查看要讀取的數據是否就緒,若是數據沒有就緒,就一直等待。須要經過用戶線程或者內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶空間。併發

  • 異步IO
    只有IO請求操做的發出是由用戶線程來進行的,IO操做的兩個階段都是由內核自動完成,而後發送通知告知用戶線程IO操做已經完成。也就是說在異步IO中,不會對用戶線程產生任何阻塞。

  • 阻塞IO
    當用戶線程發起一個IO請求操做(以讀請求操做爲例),內核查看要讀取的數據還沒就緒,當前線程被掛起,阻塞等待結果返回。

  • 非阻塞IO
    若是數據沒有就緒,則會返回一個標誌信息告知用戶線程當前要讀的數據沒有就緒。當前線程在拿到這次請求結果的過程當中,能夠作其它事情。

JAVA中的BIO、NIO、AIO

  • BIO
    同步阻塞,傳統io方式。
    適用於鏈接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中。

  • NIO
    同步非阻塞,jdk4開始支持。
    適用於鏈接數目多且鏈接比較短(輕操做)的架構,好比聊天服務器。

  • AIO
    異步非阻塞,jdk7開始支持。
    適用於鏈接數目多且鏈接比較長(重操做)的架構。

形象的理解NIO和AIO:
若是把內核比做快遞,NIO就是你要本身時不時到官網查下快遞是否已經到了你所在城市,而後本身去取快遞;AIO就是快遞員送貨上門了。

Linux下五種IO模型

  • 阻塞I/O(blocking I/O)

  • 非阻塞I/O (nonblocking I/O)

  • I/O複用(select 和poll) (I/O multiplexing)

  • 信號驅動I/O (signal driven I/O (SIGIO))

  • 異步I/O (asynchronous I/O (the POSIX aio_functions))

IO複用模型(IO多路複用)

簡言之,就是經過單個線程(進程)來管理多IO流。如圖:

io%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8.png

IO多路複用避免阻塞在IO上,本來爲多進程或多線程來接收多個鏈接的消息變爲單進程或單線程保存多個socket的狀態後輪詢處理。只有當某個socket讀寫就緒後,才真正調用實際的IO讀寫操做。這樣能夠避免線程切換帶來的開銷。

實現IO多路複用須要函數來支持,就是你說的linux下的select、poll、epoll以及win下 iocp和BSD的kqueue。這幾個函數也會使進程阻塞,可是和阻塞I/O所不一樣的是,它能夠同時阻塞多個I/O操做。並且能夠同時對多個讀操做,多個寫操做的I/O準備狀態進行檢測。

IO多路複用爲什麼比非阻塞IO模型的效率高是由於在非阻塞IO中,不斷地詢問socket狀態是經過用戶線程去進行的,而在IO多路複用中,輪詢每一個socket狀態是內核在進行的,這個效率要比用戶線程要高的多。

io%E5%A4%8D%E7%94%A8%E6%A8%A1%E5%9E%8B.png

五種IO模型以及select、poll、epoll的詳細介紹推薦你們看這篇文章
socket阻塞與非阻塞,同步與異步、I/O模型

理解Reactor和Proactor模式

在Reactor模式中,會先對每一個client註冊感興趣的事件,而後有一個線程專門去輪詢每一個client是否有事件發生,當有事件發生時(讀寫就緒),便順序處理每一個事件,當全部事件處理完以後,便再轉去繼續輪詢,如圖所示:

reactor%E6%A8%A1%E5%BC%8F.png

從這裏能夠看出,多路複用IO就是採用Reactor模式。注意,上面的圖中展現的是順序處理每一個事件,固然爲了提升事件處理速度,能夠經過多線程或者線程池的方式來處理事件。
在Proactor模式中,當檢測到有事件發生時,會新起一個異步操做,而後交由內核線程去處理,當內核線程完成IO操做以後,發送一個通知告知操做已完成,能夠得知,異步IO模型採用的就是Proactor模式。

這部分摘選自:Java NIO:淺析I/O模型

Java NIO介紹

Channels and Buffers(通道和緩衝區)
標準的IO基於字節流和字符流進行操做的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操做,數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。

Non-blocking IO(非阻塞IO)
Java NIO可讓你非阻塞的使用IO,例如:當線程從通道讀取數據到緩衝區時,線程仍是能夠進行其餘事情。當數據被寫入到緩衝區時,線程能夠繼續處理它。從緩衝區寫入通道也相似。

Selectors(選擇器)
選擇器用於監聽多個通道的事件(好比:鏈接打開,數據到達)。所以,單個的線程能夠監聽多個數據通道。

NIO與IO區別

  IO                 NIO
面向流         面向緩衝
阻塞IO          非阻塞IO
  無                選擇器

Channel

Java NIO的通道相似流,但又有些不一樣:

  • 既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向的。

  • 通道能夠異步地讀寫。

  • 通道中的數據老是要先讀到一個Buffer,或者老是要從一個Buffer中寫入。

Channel的實現

  • FileChannel (從文件中讀寫數據)

  • DatagramChannel (經過UDP讀寫網絡中的數據)

  • SocketChannel (經過TCP讀寫網絡中的數據)

  • ServerSocketChannel (能夠監聽新進來的TCP鏈接,像Web服務器那樣)

Buffer

Java NIO中的Buffer用於和NIO通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。

Buffer的基本用法

使用Buffer讀寫數據通常遵循如下四個步驟:

  1. 分配指定大小的buffer空間

  2. 寫入數據到Buffer

  3. 調用flip()方法

  4. 從Buffer中讀取數據

  5. 調用clear()方法或者compact()方法

當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,須要經過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到buffer的全部數據。

一旦讀完了全部的數據,就須要清空緩衝區,讓它能夠再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
注:Buffer中的數據並未清除,只是這些標記告訴咱們能夠從哪裏開始往Buffer裏寫數據。

Buffer的類型

  • ByteBuffer

  • MappedByteBuffer

  • CharBuffer

  • DoubleBuffer

  • FloatBuffer

  • IntBuffer

  • LongBuffer

  • ShortBuffer

Selector

Selector(選擇器)是Java NIO中可以檢測一到多個通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。

nio.png

爲何使用Selector?

僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。

可是,須要記住,現代的操做系統和CPU在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個CPU有多個內核,不使用多任務多是在浪費CPU能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用Selector可以處理多個通道就足夠了。

NIO如何實現非阻塞?

服務器上全部Channel須要向Selector註冊,而Selector則負責監視這些Socket的IO狀態(觀察者),當其中任意一個或者多個Channel具備可用的IO操做時,該Selector的select()方法將會返回大於0的整數,該整數值就表示該Selector上有多少個Channel具備可用的IO操做,並提供了selectedKeys()方法來返回這些Channel對應的SelectionKey集合(一個SelectionKey對應一個就緒的通道)。正是經過Selector,使得服務器端只須要不斷地調用Selector實例的select()方法便可知道當前全部Channel是否有須要處理的IO操做。
注:java NIO就是多路複用IO,jdk7以後底層是epoll模型。

一個簡單的demo

/**
 * NioServer
 * Date: 6/27/2016
 * Time: 8:06 PM
 *
 * @author xiaodong.fan
 */
public class NioServer {

  public static void main(String[] args) throws Exception {
    // 一、初始化一個ServerSocketChannel
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9999);
    serverSocketChannel.configureBlocking(false);// 設置爲非阻塞模式,後續的accept()方法會馬上返回
    serverSocketChannel.socket().bind(inetSocketAddress, 1024);// 監聽本地9999端口的請求,第二個參數限制能夠創建的最大鏈接數
    Selector selector = Selector.open();
    /**
     * 將通道註冊到一個選擇器上(非阻塞模式與選擇器搭配會工做的更好)
     * 注意register()方法的第二個參數。這是一個「interest集合」,意思是在經過Selector監聽Channel時對什麼事件感興趣。
     * 能夠監聽四種不一樣類型的事件:OP_CONNECT,OP_ACCEPT,OP_READ,OP_WRITE
     * 若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來:SelectionKey.OP_READ | SelectionKey.OP_WRITE
     */
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

    // 二、監聽鏈接請求並處理
    while (true) {
      int connects = selector.select(2000);// 每次最多阻塞2秒

      if (connects == 0) {
        System.out.println("沒有請求...");
        continue;
      } else {
        System.out.println("請求來了...");
      }

      // 獲取監聽到有鏈接請求的channel對應的selectionKey
      Set<SelectionKey> selectedKeys = selector.selectedKeys();
      // 遍歷selectionKey來訪問就緒的通道
      Iterator<SelectionKey> selectedKeyIterator = selectedKeys.iterator();
      while (selectedKeyIterator.hasNext()) {
        SelectionKey selectionKey = selectedKeyIterator.next();
        if (selectionKey.isValid()) {

          if (selectionKey.isAcceptable()) {// 接收就緒
            ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
            // 返回一個包含新進來的鏈接SocketChannel,由於前面設置的非阻塞模式,這裏會當即返回。
            SocketChannel socketChannel = channel.accept();
            if (socketChannel == null) {
              return;
            }
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("鏈接創建完成");
            doWrite(socketChannel, "connection is established");// 鏈接創建完成,給客戶端發消息

          } else if (selectionKey.isReadable()) {// 讀就緒

            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            ByteBuffer readBuffer = ByteBuffer.allocate(10);
            while ((socketChannel.read(readBuffer)) > 0) {// // 讀取客戶端發送來的消息
              readBuffer.flip();
              byte[] bytes = new byte[readBuffer.remaining()];
              readBuffer.get(bytes);
              String body = new String(bytes, "utf-8");
              doWrite(socketChannel, body);// 將客戶端發送的內容原封不動的發回去
              readBuffer.clear();
            }
            socketChannel.close();//讀取數據完畢後關閉鏈接,若是不關閉一直處於鏈接狀態。

          }
        }

        selectedKeyIterator.remove(); // 注意每次必須手動remove(),下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中
      }
    }
  }

  private static void doWrite(SocketChannel socketChannel, String response) throws IOException {
    if (StringUtils.isNotBlank(response)) {
      byte[] bytes = response.getBytes();
      ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
      writeBuffer.put(bytes);
      writeBuffer.flip();
      // 發送消息到客戶端
      socketChannel.write(writeBuffer);
      writeBuffer.clear();
    }

  }

}

參考文章

Java NIO:淺析I/O模型
socket阻塞與非阻塞,同步與異步、I/O模型
Java BIO、NIO、AIO 學習
Java NIO:NIO概述
Java NIO 系列教程
Java網絡編程——使用NIO實現非阻塞Socket通訊

相關文章
相關標籤/搜索