1. 程式人生 > >java nio

java nio

觸發 新的 display 打開 ket lec 概念 連接 興趣

阻塞IO和非阻塞IO:

阻塞IO:在代碼進行 read() 調用時,代碼會阻塞直至有可供讀取的數據。同樣, write()調用將會阻塞直至數據能夠寫入。換句話說,當你發了一個請求(或方法調用)之後,必須等待,直到程序返回結果,這段期間不能幹其他事情。Everything is in sequence,每件事情都是有先後順序的。

非阻塞IO就是當你發了一個請求之後,不用去管結果何時返回,而是去幹其他的事情,當程序返回結果時,會通知你,例如你用全自動洗衣機洗衣服,你不用在旁邊等著洗衣機洗完,而是可以去做飯啊上網什麽的,當洗衣機洗完衣服之後,會發出“滴滴滴”的通知聲音,當你得知這個消息後,就可以去處理它的結果了(例如晾衣服等)。

假設某時刻有大量的請求同時到達服務端,如果對每一個連接都創建一個線程去處理該請求,那麽連接數越多,創建的線程數就越多,但是一臺服務器能夠產生的線程數是有限的,這樣就限制了它的並發處理能力。In addition,如果來一個請求就創建一個線程,處理完之後又銷毀掉,那麽頻繁的創建、銷毀線程會帶來巨大的系統開銷,所以必須引入線程池,維持一個核心線程數和最大線程數。但是,服務端的並發處理能力依然有限,因為處理能力還是與最大線程數相關。如何解決呢?使用NIO。通過一個selector(只需要開啟一個線程),每一個客戶端連接為一個channel,這些channel都註冊到selector上,selector進行事件的監聽、選擇性處理。

NIO:non-blocking IO,JDK中自帶得有NIO的包(java.nio);但是通常基於java nio開發的程序有各種問題,至少要半年甚至一年才會慢慢解決掉,所以只需要了解其中的核心概念(ByteBuffer,Channel,Selector)。通道是對 I/O 包中的流的模擬,到任何目的地(或來自任何地方)的所有數據都必須通過一個 Channel 對象。Buffer 實質上是一個容器對象,發送給一個通道的所有對象都必須首先放到緩沖區中;同樣地,從通道中讀取的任何數據都要讀到緩沖區中。

緩沖區實質上是一個數組,最常用的緩沖區類型是 ByteBuffer。一個 ByteBuffer 是在其底層字節數組上進行 get/set 操作。通道與流的不同之處在於通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而 通道 可以用於讀、寫或者同時用於讀寫。

從文件中讀取(從外面的文件讀取進入內存)

如果使用原來的 I/O,那麽我們只需創建一個 FileInputStream 並從它那裏讀取。而在 NIO 中,情況有所不同:

第一步是獲取通道。我們從 FileInputStream 獲取通道:

FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();

下一步是創建緩沖區:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );    //緩沖區分配

最後,需要將數據從通道讀到緩沖區中,如下所示:

fc.read( buffer );

寫入文件(從內存寫出到外面的文件)

在 NIO 中寫入文件,首先從 FileOutputStream 獲取一個通道:

FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();

下一步是創建一個緩沖區並在其中放入一些數據

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {
     buffer.put( message[i] );
}
buffer.flip();

最後一步是寫入到文件中:

fc.write( buffer );

現在重點研究一下Buffer中的結構(狀態變量:position、limit 、capacity,可以理解為三個指針),通過這三個狀態變量,Buffer就可以自動管理裏面的數據了。capacity始終為字節數組的長度,當從外界讀入數據到Buffer中時,limit指向position的位置,positon初始位置為0,每讀入一個字節,position++,但是不會超過capacity的位置。

如果要將Buffer中的數據讀出到外界去,則需要調用buffer.flip()方法。flip方法會改變position和limit指針的位置,將positon指向0的位置,將limit指向原先的position的位置,這樣一來,讀取的時候就只會在[0,limit]範圍內了,每讀取一個字節,position++,直到到達limit,這樣就將buffer中的數據全部讀走了。

最後,調用buffer.clear()方法,該方法會重置這三個指針,使得其回到最原始的位置,準備新的讀入工作。

如何直接操作Buffer中的數據呢?使用put()、get()方法,put往Buffer中放入字節,get從Buffer中讀取字節。


Selector:Selector是一個選擇器,每一個Channel都會在Selector上進行註冊,並指定Selector對該channel感興趣的事件(返回值為SelectedKey)。Selector上同時註冊得有多個channel,當某個事件發生時,Selector就會通知對應的處理程序進行處理。

我們需要做的第一件事就是創建一個 Selector:

Selector selector = Selector.open();

然後,我們將對各個通道對象調用 register() 方法,以便註冊我們對這些對象中發生 I/O 事件的興趣。register() 的第一個參數總是這個 Selector。為了接收連接,我們需要一個 ServerSocketChannel。對於每一個端口,我們打開一個 ServerSocketChannel,如下所示:

ServerSocketChannel ssc = ServerSocketChannel.open();    //創建一個新的 ServerSocketChannel 
ssc.configureBlocking( false );   //我們必須對每一個要使用的套接字通道調用這個方法,否則異步 I/O 就不能工作。

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );    //將它綁定到給定的端口

將新打開的 ServerSocketChannels 註冊到 Selector上。為此我們使用 ServerSocketChannel.register() 方法,如下所示:

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );    //返回選擇鍵

register() 的第一個參數總是這個 Selector。第二個參數是 OP_ACCEPT,這裏它指定我們想要監聽 ACCEPT 事件,也就是在新的連接建立時所發生的事件。請註意對 register() 的調用的返回值,SelectionKey 代表這個通道在此 Selector 上的註冊。當某個 Selector 通知您某個傳入事件時,它是通過提供對應於該事件的 SelectionKey 來進行的。

接著進入主循環。使用 Selectors 的幾乎每個程序都像下面這樣使用內部循環:

int num = selector.select();    //該方法會阻塞,直到至少有一個已註冊的事件發生。當一個或者更多的事件發生時, select() 方法將返回所發生的事件的數量

Set selectedKeys = selector.selectedKeys();   //返回發生了事件的 SelectionKey 對象的集合
Iterator it = selectedKeys.iterator();

while (it.hasNext()) {
     SelectionKey key = (SelectionKey)it.next();
     if ((key.readyOps() & SelectionKey.OP_ACCEPT)   //對每個感興趣事件的處理
          == SelectionKey.OP_ACCEPT) {

         ServerSocketChannel ssc = (ServerSocketChannel)key.channel();      //
         SocketChannel sc = ssc.accept();              //接受新的連接
sc.configureBlocking( false ); SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); //將SocketChannel又註冊一次,用於讀取來自套接字的數據 } }

我們通過叠代 SelectionKeys 並依次處理每個 SelectionKey 來處理事件。對於每一個 SelectionKey,您必須確定發生的是什麽 I/O 事件,以及這個事件影響哪些 I/O 對象。

當來自一個套接字的數據到達時,它會觸發一個 I/O 事件。這會導致在主循環中調用 Selector.select(),並返回一個或者多個 I/O 事件。這一次, SelectionKey 將被標記為 OP_READ 事件,如下所示:

} else if ((key.readyOps() & SelectionKey.OP_READ)
     == SelectionKey.OP_READ) {
     // Read the data
     SocketChannel sc = (SocketChannel)key.channel();
     // ...
}


這樣Selector就實現了一個線程同時處理多個套接字連接的需求(高並發)。

java nio