我前段時間的一篇博客java網絡編程——多線程數據收發並行總結了服務端與客戶端之間的收發並行實踐。原理很簡單,就是針對單一客戶端,服務端起兩個線程分別負責read和write操做,而後線程保持阻塞等待讀寫執行。html
事實上,這樣的模式很是糟糕。由於每個客戶端在服務端須要佔用兩條線程,假若有1000個客戶端,則須要2000+條線程。cpu須要花費大量的時間進行線程上下文切換,形成系統資源浪費。java
想要縮減線程數量,先要解決阻塞問題。而NIO能夠經過IO多路複用將read和write的阻塞給抹去。再配合線程池,便可實現用少許的線程支撐起上百萬個客戶端的鏈接。編程
什麼是NIO
NIO與IO多路複用
java NIO全稱java non-blocking IO。字面意思即非阻塞式IO。實際上這裏的非阻塞只是宏觀的說法。網絡
關於IO模式,這裏引一個別人的博客,介紹了幾種IO模式的區別:多線程
簡述同步IO、異步IO、阻塞IO、非阻塞IO之間的聯繫與區別異步
本博客再也不贅述這些,只是想說NIO屬於其中的IO複用模型。(實驗室裏有一本《UNIX網絡編程》疫情結束回學校必定把這部分好好看看)socket
多路複用IO模型中,會有一個線程去不斷輪詢多個socket的狀態,當socket有讀寫事件時,纔來調用IO操做。由於是一個線程來管理多個socket,系統不須要創建其它線程、維護線程,只有socket就緒時,纔會使用IO資源,因此它大大下降了資源佔用。ide
java NIO中,使用selector.select()監聽多個通道是否有到達事件,沒有事件就一直阻塞,有事件就調用IO進行處理。url
三大核心
- 通道(Channel)
- 緩衝區(Buffer)
- 選擇器(Selectors)
詳細介紹以下
spa
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的集合
- 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部分的方法沒有分析,線程池部分沒有加上,還有輸出操做那一套,都沒講。總想盡量多地詳細完整一點,可是越深刻,知識點就越龐大,因此只能放棄一部份內容,因而成了如今這個樣子。若是詳細規劃一下拆開多個博客寫會更好。