天天看點

NIO的使用心得筆者使用NIO的背景–話說在前NIO概念NIO的體系結構筆者對NIO的認識

筆者使用NIO的背景–話說在前

筆者為何會使用上NIO呢?回顧自己的這次使用經曆:直到最後去找老師請教後,才直到自己仍未有足以使用NIO的理由。當初筆者是在程式設計一個小的網絡java application,用單線程的通訊,即與應用同一線程。在調試運作中,發現由于通訊的監聽input阻塞,應用界面沒有響應,這令筆者很快的放棄了這個程式模型,去追求一種不阻塞的通訊模式。後來筆者按照網上的一些範例基本,完成了NIO雛形的子產品。然而,這次使用還是阻塞,這令筆者很氣惱,于是被逼學習多線程。(特别說明:筆者使用javafx,而fx它另有自己的一個concurrent包,與普通的java多線程有特别之處)。而結果就是有了筆者如下的NIO使用心得。(老師指出筆者的錯誤是:在最初單線程BIO監聽循環結構中不使用thread.sleep,導緻界面無響應)(忽略各種糾結邏輯)

NIO概念

據筆者的學習了解,IO有BIO、NIO、AIO,分别對應同步阻塞IO、同步非阻塞IO、異步IO。BIO在java.io包中,而NIO和AIO在java.nio目錄下。

三者的差別可以這樣比喻:你要收一個快件(讀操作)

  • BIO:你要在樓下等快件,一直死等,直到快遞員來了你才收到快件
  • NIO:你待在家裡邊做其他事情邊等快件,當快遞員到了,收到短信通知你到樓下的櫃裡提取快遞
  • AIO:你待在家裡邊做其他事情邊等快件,當快遞員到了,快遞員不把快件放櫃裡,還按定好的詳細位址,上門派送

NIO的體系結構

以下僅為使用層面的介紹,并不會介紹詳細的uml關系

以TCP/IP為例,原來BIO的用戶端伺服器模型是使用socket系列,server先new一個serversocket并指定端口,然後使用serversocket.accept()等待連結傳回一個socket;client需要new一個socket并指定server的位址和端口。至此連接配接成立。之後雙方可以使用使用channel對象的configureblocking()設定是否阻塞工作。資料交流使用stream系列。從socket.getInputStream()/socket.getOutputStream()得到對應流,然後可以再用其他流封裝得到的基礎流來使用。

對此,NIO則有很大變化。首先NIO的C/S模型使用channel系列。channel是個抽象類,基本上nio.channel目錄下的都是抽象類,其實作子類在sun包裡,筆者暫時看不到sun包的源代碼。server首先new一個inetsocketaddress指定位址和端口,然後使用serversocketchannel靜态方法open()得到該類的實作類對象,再使用該類bind()指定剛才定義的socketaddress以綁定。client需要使用socketchannel的靜态方法open()得到它的實作類對象。

//伺服器端準備工作
    InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT);  
    ServerSocketChannel server = ServerSocketChannel.open();  
    server.bind(isa);  
    server.configureBlocking(false);  
    //用戶端準備工作
    InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT);  
    SocketChannel sc = SocketChannel.open();  
    sc.configureBlocking(false);  
           

至此,連接配接未成立,而NIO的新異之處才開始。

Selector-SelectKey ——非阻塞核心

NIO引入選擇器selector概念。原本BIO的阻塞現象是因為線程自身要循環監聽,而NIO則使用selector,利用通知/回調機制,線程會在收到通知或受到喚醒才進行監聽(然而這監聽還是阻塞現象,“非阻塞”的稱呼并沒有是以失去意義的原因,待體系介紹末尾再解釋)。然後,誰能通知或者喚醒它呢?這可以是它自己,或者選擇鍵selectkey。

selectkey是用來跟蹤channel情況的一個key條目,而selector充當一個集中管理key條目的角色。channel可以向selector注冊登記自己,讓selector管理channel的工作行程。selectkey包含着channel的引用和一個可有可無、特殊具體要求才起作用的附加對象Object。selectkey有四種用靜态int表示的狀态,可以進行或運算以疊加狀态:OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE,分别表示伺服器連接配接就緒、用戶端連接配接就緒、讀就緒、寫就緒,前兩個狀态分别隻有伺服器和用戶端的channel可以使用。

//selector也是抽象的。兩端各使用靜态方法建立選擇器對象
    //伺服器端準備工作
    Selector sel = Selector.open();
    server.register(sel, SelectionKey.OP_ACCEPT);
    //用戶端準備工作
    Selector sel = Selector.open();
    sc.connect(isa);//通常上,用戶端若無其他準備工作就可直接連接配接伺服器,跳過選擇器管理的連接配接就緒 
           

Selector工作

selector的工作是監聽已register()注冊的channel的情況,觀測由selectkey訓示狀态,它有劃分keys、selectedkeys、cancelledkeys三個set,前兩個可以通路。在selectkey設定了感興趣的事件集(即就緒事件集)後,若執行selector的選擇方法,若無就緒的channel,則會有因執行的具體選擇方法而不同的動作:

  • select():線程阻塞,selector沉默
  • select(long):線程在指定毫秒内阻塞,時間結束後傳回
  • selectNow():選擇後立即傳回

若有則selector會将有就緒事件的selectkey加入selectedkeys集中,并将selectkey的感興趣集清空。

selector選擇後形成的selectedkeys集的意義是:集中給出的鍵都是就緒的,程式員可以對它們進行不需等待的操作,即連接配接、讀寫操作。

selector管理的通常代碼結構

while(true){
        while(sel.select() > ){
            for(SelectKey sk : sel.selectedKeys()){
                if(sk.isReadable()){
                    //具體的寫操作
                }
                if(sk.isWritable()){
                    //具體的寫操作
                }
            }
        }
    }
           

這是一種selector的輪詢工作。需要注意的是:

  • 在選擇後對通道進行了具體的操作後,若無後續操作,應該手動從selectedKeys集中删除該鍵,否則,下次的選擇後該鍵仍在該已選擇鍵集中,有出錯的風險。//sel.selectedKeys().remove(sk);
  • 在選擇器選擇某鍵後,某鍵的感興趣集會清空,若有後續的操作,應在對該鍵重設感興趣集。//sk.interestOps(int ops),該語句的位置靈活,不一定要在非選擇器線程上
  • 該輪詢是循環選擇,一次性的工作應不套用該while(true)條件。

使用ByteBuffer作為資訊媒體

channel.read()的傳回值和channel.write()的參數都是ByteBuffer類型,使用位元組為機關。不過可以使用ByteBuffer對象的asDoubleBUffer()等等獲得支援的有限種基本資料類型封裝後的Buffer,友善其他資料類型的讀寫。具體的Buffer系列使用方法這裡不詳盡。

筆者對NIO的認識

首先解釋上文筆者對選擇器的工作仍然是阻塞,但NIO未失去意義的原因:原本BIO的讀寫監聽都由程式的讀寫子產品直接負責,而使用NIO的選擇器則可将此阻塞的監聽轉交給選擇器承擔。就如上文的比喻一樣,你真的不需等待,待通知到來就可獲得所需。

筆者的老師對我談過他的經曆:他學了NIO後其實以後沒有多少使用的機會。由筆者再提煉老師的想法就是:就阻塞監聽任務的轉移上,隻有轉移,沒有省去。是以隻是子產品上的再劃分,在管理上的便利化,但照樣可以完成讀寫的結果沒有變化。一般的程式沒有NIO的需求,除非那程式的讀寫、連接配接任務比重非常大。