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)

詳細介紹如下

java NIO理解分析與基本使用

NIO使用舉例

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

java NIO理解分析與基本使用

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

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

<code>readSelector = Selector.open();
writeSelector = Selector.open();
/<code>
  1. 監聽通道,得到客戶端,並建立SocketChannel,用於監聽後續客戶端消息
<code>//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());
}
}
/<selectionkey>/<code>
  1. 將SocketChannel註冊進readSelector和writeSelector
<code>/**
*參數分別是:channel,對應的selector,以及
*registerOps:待註冊的操作集,這個在後文中有詳細解析;
*locker:用於標識同步代碼塊的狀態,是鎖定還是可用;
*runnable:執行具體讀寫操作的類,送給線程池執行;
*map:建立SelectionKey與Runnable映射關係的HashMap。
*/
private static SelectionKey registerSelection(SocketChannel channel, Selector selector,
int registerOps, AtomicBoolean locker,
HashMap<selectionkey> 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) {
}
}
}
}
/<selectionkey>/<code>
  1. 監聽各個客戶端消息,通過selectionKeys獲取channel,再執行輸入操作
<code>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();
}
/<selectionkey>/<code>

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

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

selector類

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

  • open相關 open()開啟一個selector public abstract boolean isOpen(); 判斷是否開啟
<code>public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
/<code>
  • 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
<code>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;
/<code>
  • 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部分的方法沒有分析,線程池部分沒有加上,還有輸出操作那一套,都沒講。總想盡可能多地詳細完整一點,但是越深入,知識點就越龐大,所以只能放棄一部分內容,於是成了現在這個樣子。如果詳細規劃一下拆開多個博客寫會更好。

本文轉載於博客園:https://www.cnblogs.com/buptleida/p/12633675.html


分享到:


相關文章: