java NIO理解分析與基本使用
- 2020 年 4 月 4 日
- 筆記
我前段時間的一篇部落格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)
詳細介紹如下
NIO使用舉例
這裡以服務端讀取客戶端消息的流程為例,介紹NIO的使用(完整內容只有輸入,暫且不管輸出)。畫了一個流程圖,如下所示:
- 建立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();
- 監聽通道,得到客戶端,並建立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()); } }
- 將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) { } } } }
- 監聽各個客戶端消息,通過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的集合
- public abstract Set
- 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部分的方法沒有分析,執行緒池部分沒有加上,還有輸出操作那一套,都沒講。總想儘可能多地詳細完整一點,但是越深入,知識點就越龐大,所以只能放棄一部分內容,於是成了現在這個樣子。如果詳細規劃一下拆開多個部落格寫會更好。