java NIO理解分析與基本使用

我前段時間的一篇部落格java網路編程——多執行緒數據收發並行總結了服務端與客戶端之間的收發並行實踐。原理很簡單,就是針對單一客戶端,服務端起兩個執行緒分別負責read和write操作,然後執行緒保持阻塞等待讀寫執行。

事實上,這樣的模式非常糟糕。因為每一個客戶端在服務端需要佔用兩條執行緒,假如有1000個客戶端,則需要2000+條執行緒。cpu需要花費大量的時間進行執行緒上下文切換,造成系統資源浪費。

想要縮減執行緒數量,先要解決阻塞問題。而NIO可以通過IO多路復用將read和write的阻塞給抹去。再配合執行緒池,即可實現用少量的執行緒支撐起上百萬個客戶端的連接。

什麼是NIO

NIO與IO多路復用

java NIO全稱java non-blocking IO。字面意思即非阻塞式IO。實際上這裡的非阻塞只是宏觀的說法。

關於IO模式,這裡引一個別人的部落格,介紹了幾種IO模式的區別:

簡述同步IO、非同步IO、阻塞IO、非阻塞IO之間的聯繫與區別

本部落格不再贅述這些,只是想說NIO屬於其中的IO復用模型。(實驗室里有一本《UNIX網路編程》疫情結束回學校一定把這部分好好看看)

多路復用IO模型中,會有一個執行緒去不斷輪詢多個socket的狀態,當socket有讀寫事件時,才來調用IO操作。因為是一個執行緒來管理多個socket,系統不需要建立其它執行緒、維護執行緒,只有socket就緒時,才會使用IO資源,所以它大大降低了資源佔用。

java NIO中,使用selector.select()監聽多個通道是否有到達事件,沒有事件就一直阻塞,有事件就調用IO進行處理。

三大核心

  • 通道(Channel)
  • 緩衝區(Buffer)
  • 選擇器(Selectors)

詳細介紹如下
image

NIO使用舉例

這裡以服務端讀取客戶端消息的流程為例,介紹NIO的使用(完整內容只有輸入,暫且不管輸出)。畫了一個流程圖,如下所示:
image

  1. 建立selector和ServerSocketChannel,並綁定註冊,用於監聽客戶端連接請求,程式碼如下:
selector = Selector.open();  ServerSocketChannel server = ServerSocketChannel.open();  // 設置為非阻塞  server.configureBlocking(false);  // 綁定本地埠  server.socket().bind(new InetSocketAddress(port));  // 註冊客戶端連接到達監聽  server.register(selector, SelectionKey.OP_ACCEPT);  

同時還要建立readSelector和writeSelector。其實執行緒池也是提前建立的,這裡暫且不寫。

readSelector = Selector.open();  writeSelector = Selector.open();  
  1. 監聽通道,得到客戶端,並建立SocketChannel,用於監聽後續客戶端消息
//select()方法返回已就緒的通道數  if (selector.select() == 0) {      continue;  }    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();  while (iterator.hasNext()) {        SelectionKey key = iterator.next();      iterator.remove();        // 檢查當前Key的狀態是否是accept的      // 客戶端到達狀態      if (key.isAcceptable()) {          ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();          // 非阻塞狀態拿到客戶端連接          SocketChannel socketChannel = serverSocketChannel.accept();            try {              // 客戶端構建非同步執行緒              // 添加同步處理              //此處程式碼暫且忽略          } catch (IOException e) {              e.printStackTrace();              System.out.println("客戶端連接異常:" + e.getMessage());          }      }  
  1. 將SocketChannel註冊進readSelector和writeSelector
/**  *參數分別是:channel,對應的selector,以及  *registerOps:待註冊的操作集,這個在後文中有詳細解析;  *locker:用於標識同步程式碼塊的狀態,是鎖定還是可用;  *runnable:執行具體讀寫操作的類,送給執行緒池執行;  *map:建立SelectionKey與Runnable映射關係的HashMap。  */  private static SelectionKey registerSelection(SocketChannel channel, Selector selector,                                                    int registerOps, AtomicBoolean locker,                                                    HashMap<SelectionKey, Runnable> map,                                                    Runnable runnable) {      synchronized (locker) {      // 設置鎖定狀態      locker.set(true);        try {          // 喚醒當前的selector,讓selector不處於select()狀態          //註冊channel時一定要將selector喚醒,否則當前select中沒有剛註冊的channel          selector.wakeup();            SelectionKey key = null;          if (channel.isRegistered()) {              // 查詢是否已經註冊過              key = channel.keyFor(selector);              if (key != null) {              //將新的Ops添加進去                  key.interestOps(key.readyOps() | registerOps);              }          }            if (key == null) {              // 註冊selector得到Key              key = channel.register(selector, registerOps);              // 註冊回調              map.put(key, runnable);          }            return key;      } catch (ClosedChannelException e) {          return null;      } finally {          // 解除鎖定狀態          locker.set(false);          try {              // 通知              locker.notify();          } catch (Exception ignored) {          }      }      }  }  
  1. 監聽各個客戶端消息,通過selectionKeys獲取channel,再執行輸入操作
try {  if (readSelector.select() == 0) {      continue;  }    Set<SelectionKey> selectionKeys = readSelector.selectedKeys();  for (SelectionKey selectionKey : selectionKeys) {      if (selectionKey.isValid()) {          //IO處理程式碼,暫且忽略      }  }  selectionKeys.clear();  } catch (IOException e) {  e.printStackTrace();  }  

注意:以上都是一些程式碼片段,沒有完全串聯起來,省略了一些類對象調用、方法調用以及關鍵的執行緒池操作等等。但是基本的方法已經展示出來,剩下的後面的部落格再去填坑。

光看上面的程式碼,對於一些NIO方法的認知還是很模糊的。下面通過閱讀selector類和SelectionKey類的源碼注釋,來加深對部分方法的理解。

selector類

selector是NIO的核心類,下面是選擇器的一些重要方法:

  • open相關
    • open()開啟一個selector
    • public abstract boolean isOpen(); 判斷是否開啟
public static Selector open() throws IOException {      return SelectorProvider.provider().openSelector();  }  
  • keys相關
    • public abstract Set keys();返回所有key的集合
    • public abstract Set selectedKeys();返回已被選擇的key的集合
  • select
    • 下面幾個方法都是返回已就緒通道的數量,可能是0;
    • selectNow(),非阻塞方法;
    • select(),僅在三種情況下返回,1.通道被選擇;2.調用wakeup方法;3.執行緒中斷。
    • select(timeout),比select()多一個解除阻塞的條件,即超時。
  • wakeup(),解除正在阻塞的select方法的阻塞,立即返回
  • close(),關閉selector。

SelectionKey類

註冊進selector的任何一個channel都用一個SelectionKey對象來指代。

操作集

  • Operation-set:操作集,一些常量int值,代表各種類型的操作;一個selection key包含兩個操作集,interest set和ready-operation set
public static final int OP_READ = 1 << 0;  public static final int OP_WRITE = 1 << 2;  public static final int OP_CONNECT = 1 << 3;  public static final int OP_ACCEPT = 1 << 4;  
  • interest set:興趣集;一個channel所有的操作集;可通過interestOps(int)方法來更新

  • ready-operation set:就緒操作集,只包含使得channel被報告就緒的操作,底層通過與或操作來更新;例如當一個channel讀取就緒時,將read操作集加入到就緒集中。

方法列表

  • public abstract SelectableChannel channel():返回此選擇鍵所關聯的通道.即使此key已經被取消,仍然會返回;
  • public abstract Selector selector():返回此選擇鍵所關聯的選擇器,即使此鍵已經被取消,仍然會返回;
  • public abstract boolean isValid():檢測此key是否有效.當key被取消,或者通道被關閉,或者selector被關閉,都將導致此key無效.在AbstractSelector.removeKey(key)中,會導致selectionKey被置為無效;
  • public abstract void cancel():請求將此鍵取消註冊.一旦返回成功,那麼該鍵就是無效的,被添加到selector的cancelledKeys中.cancel操作將key的valid屬性置為false,並執行selector.cancel(key)(即將key加入cancelledkey集合);
  • public abstract int interesOps():獲得此鍵的interes集合;
  • public abstract SelectionKey interestOps(int ops):將此鍵的interst設置為指定值.此操作會對ops和channel.validOps進行校驗.如果此ops不會當前channel支援,將拋出異常;
  • public abstract int readyOps():獲取此鍵上ready操作集合.即在當前通道上已經就緒的事件;
  • public final boolean isReadable(): 檢測此鍵"read"事件是否就緒.等效於:(readyOps() & OP_READ) != 0;還有isWritable(),isConnectable(),isAcceptable()
  • public final Object attach(Object ob):將給定的對象作為附件添加到此key上.在key有效期間,附件可以在多個ops事件中傳遞;
  • public final Object attachment():獲取附件.一個channel的附件,可以再當前Channel(或者說是SelectionKey)生命周期中共享,但是attachment數據不會作為socket數據在網路中傳輸。

終於寫完了,這篇部落格只能算是對NIO簡單介紹,一些東西還沒講到。channel和buffer部分的方法沒有分析,執行緒池部分沒有加上,還有輸出操作那一套,都沒講。總想儘可能多地詳細完整一點,但是越深入,知識點就越龐大,所以只能放棄一部分內容,於是成了現在這個樣子。如果詳細規劃一下拆開多個部落格寫會更好。