java nio
阻塞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