摘要:當數據被寫入到緩沖區時,線程可以繼續處理它。當滿足下列條件時,表示兩個相等有相同的類型等。調用通道的方法時,可能會導致線程暫時阻塞,就算通道處于非阻塞模式也不例外。當一個通道關閉時,休眠在該通道上的所有線程都將被喚醒并收到一個異常。
1、NIO和I/O區別
I/O和NIO的區別在于數據的打包和傳輸方式。
I/O流的方式處理數據
NIO塊的方式處理數據
面向流 的 I/O 系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。為流式數據創建過濾器非常容易。鏈接幾個過濾器,以便每個過濾器只負責單個復雜處理機制的一部分,這樣也是相對簡單的。面向流的 I/O 通常相當慢。
一個 面向塊 的 I/O 系統以塊的形式處理數據。每一個操作都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。但是面向塊的I/O比較復雜。NIO的主要應用在高性能、高容量服務端應用程序。
IO | NIO |
---|---|
面向流 | 面向塊,緩沖 |
阻塞IO | 非阻塞IO |
無 | Selector |
1、Channels and Buffers(通道和緩沖區)
標準的IO基于字節流和字符流進行操作的,而NIO是基于通道(Channel)和緩沖區(Buffer)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。
2、Non-blocking IO(非阻塞IO)
Java NIO可以非阻塞的方式使用IO,例如:當線程從通道讀取數據到緩沖區時,線程還是可以進行其他事情。當數據被寫入到緩沖區時,線程可以繼續處理它。從緩沖區寫入通道也類似。
3、Selectors(選擇器)
Java NIO引入了選擇器的概念,選擇器用于監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個的線程可以監聽多個數據通道。
通道和緩沖區是 NIO 中的核心對象,幾乎在每一個 I/O 操作中都要使用它們。
一個 Buffer 實質上是一個容器對象,它包含一些要寫入或者剛讀出的數據。
在 NIO 庫中,所有數據都是用緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的。在寫入數據時,它是寫入到緩沖區中的。任何時候訪問 NIO 中的數據,您都是將它放到緩沖區中。
緩沖區實質上就是一個數組,通常它是一個字節數組,但是也可以使用其他種類的數組。但它不僅僅是一個數組,緩沖區還提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。
Buffer 類型最常用的緩沖區類型是 ByteBuffer。一個 ByteBuffer 可以在其底層字節數組上進行 get/set 操作(即字節的獲取和設置)。
ByteBuffer 不是 NIO 中唯一的緩沖區類型。事實上,對于每一種基本 Java 類型都有一種緩沖區類型:
Buffer抽象類中提供了需要處理的方法類型。
Buffer 用法使用Buffer讀寫數據一般遵循以下四個步驟:
寫入數據到Buffer
調用flip()方法
從Buffer中讀取數據
調用clear()方法或者compact()方法
當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數據。一旦讀完了所有的數據,就需要清空緩沖區,讓它可以再次被寫入。
有兩種方式能清空緩沖區:調用clear()或compact()方法。
clear()方法會清空整個緩沖區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩沖區的起始處,新寫入的數據將放到緩沖區未讀數據的后面。
try (RandomAccessFile aFile = new RandomAccessFile("F:1.txt", "rw")) { //從RandomAccesFile獲取通道 FileChannel inChannel = aFile.getChannel(); //創建一個緩沖區并在其中放入一些數據 ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); //read into buffer. //檢查狀態 while (bytesRead != -1) { //flip() 方法讓緩沖區可以將新讀入的數據寫入另一個通道。 buf.flip(); //make buffer ready for read while (buf.hasRemaining()) { System.out.print((char) buf.get()); // read 1 byte at a time } buf.clear(); //make buffer ready for writing bytesRead = inChannel.read(buf); } }capacity & position & limit
緩沖區本質上是一塊可以寫入數據,然后可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,并提供了一組方法,用來方便的訪問該塊內存。
緩沖區對象有三個基本屬性:
容量Capacity:緩沖區能容納的數據元素的最大數量,在緩沖區創建時設定,無法更改
上界Limit:表明還有多少數據需要取出(在從緩沖區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩沖區時),limit不能大于capacity
位置Position:下一個要被讀或寫的元素的索引
這三個變量一起可以跟蹤緩沖區的狀態和它所包含的數據。
四個屬性總是遵循這樣的關系:0<=mark<=position<=limit<=capacity。下圖是新創建的容量為10的緩沖區邏輯視圖:
寫入模式
buffer.put((byte)"H").put((byte)"e").put((byte)"l").put((byte)"l").put((byte)"o");
五次調用put后的緩沖區:
此時,limit表示還有多少空間可以放入數據。position最大可為capacity-1。
讀取模式
現在緩沖區滿了,我們必須將其清空。我們想把這個緩沖區傳遞給一個通道,以使內容能被全部寫出,但現在執行get()無疑會取出未定義的數據。我們必須將posistion設為0,然后通道就會從正確的位置開始讀了,但讀到哪算讀完了呢?這正是limit引入的原因,它指明緩沖區有效內容的未端。這個操作 在緩沖區中叫做翻轉:buffer.flip()。
flip這個方法做兩件非常重要的事:
將 limit 設置為當前 position。
將 position 設置為 0。
rewind操作與flip相似,但不影響limit。
clear
最后一步是調用緩沖區的 clear() 方法。這個方法重設緩沖區以便接收更多的字節。
Clear 做兩種非常重要的事情:
1.將 limit 設置為與 capacity 相同。
2.設置 position 為 0。
Buffer的分配clear()與compact()區別
1、clear()方法,position將被設回0,limit被設置成 capacity的值。Buffer中有一些未讀的數據,調用clear()方法,數據將“被遺忘”
2、compact()方法將所有未讀的數據拷貝到Buffer起始處。然后將position設到最后一個未讀元素正后面。limit屬性依然像clear()方法一樣,設置成capacity。
在能夠讀和寫之前,必須有一個緩沖區。要創建緩沖區,必須要進行分配,。我們使用靜態方法 allocate() 來分配緩沖區,每一個Buffer類都有一個allocate方法。
//分配48字節capacity的ByteBuffer的例子。 ByteBuffer buf = ByteBuffer.allocate(48); //分配一個可存儲1024個字符的CharBuffer: CharBuffer buf = CharBuffer.allocate(1024);Buffer寫入
寫數據到Buffer有兩種方式:
從Channel寫到Buffer。
通過Buffer的put()方法寫到Buffer里。
//從Channel寫到Buffer int bytesRead = inChannel.read(buf); //read into buffer. //通過put方法寫Buffer的例子: buf.put(127);Buffer讀取
從Buffer中讀取數據有兩種方式:
從Buffer讀取數據到Channel。
使用get()方法從Buffer中讀取數據。
//read from buffer into channel. int bytesWritten = inChannel.write(buf); //使用get()方法從Buffer中讀取數據 byte aByte = buf.get();
get方法有很多版本,允許你以不同的方式從Buffer中讀取數據。例如,從指定position讀取,或者從Buffer中讀取數據到字節數組。
rewind()方法
Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。
mark()與reset()
通過調用Buffer.mark()方法,可以標記Buffer中的一個特定position。之后可以通過調用Buffer.reset()方法恢復到這個position。
Buffer比較equals()與compareTo()方法
可以使用equals()和compareTo()方法兩個Buffer。
equals()
當滿足下列條件時,表示兩個Buffer相等:
有相同的類型(byte、char、int等)。
Buffer中剩余的byte、char等的個數相等。
Buffer中所有剩余的byte、char等都相同。
equals只是比較Buffer的一部分,不是每一個在它里面的元素都比較。實際上,它只比較Buffer中的剩余元素。
compareTo()
compareTo()方法比較兩個Buffer的剩余元素(byte、char等), 如果滿足下列條件,則認為一個Buffer“小于”另一個Buffer:
第一個不相等的元素小于另一個Buffer中對應的元素
第一個Buffer的元素個數比另一個少
3、ChannelJava NIO的通道類似流,但又有些不同:
既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。
通道可以異步地讀寫。
通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。
Channel類型FileChannel 從文件中讀寫數據。
DatagramChannel 能通過UDP讀寫網絡中的數據。
SocketChannel 能通過TCP讀寫網絡中的數據。
ServerSocketChannel可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。
close Channel與緩沖區不同,通道不能被重復使用;關閉通道后,通道將不再連接任何東西,任何的讀或寫操作都會導致ClosedChannelException。
調用通道的close()方法時,可能會導致線程暫時阻塞,就算通道處于非阻塞模式也不例外。如果通道實現了InterruptibleChannel接 口,那么阻塞在該通道上的一個線程被中斷時,該通道將被關閉,被阻塞線程也會拋出ClosedByInterruptException異常。
當一個通道 關閉時,休眠在該通道上的所有線程都將被喚醒并收到一個AsynchronousCloseException異常。
Scatter/Gatherscatter/gather用于描述從Channel中讀取或者寫入到Channel的操作。
分散(scatter)從Channel中讀取是指在讀操作時將讀取的數據寫入多個buffer中。因此,Channel將從Channel中讀取的數據"分散(scatter)"到多個Buffer中。
聚集(gather)寫入Channel是指在寫操作時將多個buffer的數據寫入同一個Channel,因此,Channel 將多個Buffer中的數據"聚集(gather)"后發送到Channel。
scatter / gather經常用于需要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中,這樣你可以方便的處理消息頭和消息體。
Scatter ReaderByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
buffer首先被插入到數組,然后再將數組作為channel.read() 的輸入參數。read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿后,channel緊接著向另一個buffer中寫。
Scattering Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味著它不適用于動態消息(譯者注:消息大小不固定)。換句話說,如果存在消息頭和消息體,消息頭必須完成填充(例如 128byte),Scattering Reads才能正常工作。
Gathering WritesGathering Writes是指數據從多個buffer寫入到同一個channel。如下圖描述:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); //write data into buffers ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據才會被寫入。因此,如果一個buffer的容量為128byte,但是僅僅包含58byte的數據,那么這58byte的數據將被寫入到channel中。因此與Scattering Reads相反,Gathering Writes能較好的處理動態消息。
FileChannelJava NIO中的FileChannel是一個連接到文件的通道。可以通過文件通道讀寫文件。FileChannel無法設置為非阻塞模式,它總是運行在阻塞模式下。
FileChannel讀數據因為無法保證write()方法一次能向FileChannel寫入多少字節,因此需要重復調用write()方法,直到Buffer中已經沒有尚未寫入通道的字節。
//通過inputstream或者RandomAccessFile,打開FileChannel RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //往buffer里面讀取數據 ByteBuffer buf = ByteBuffer.allocate(48); //int值表示了有多少字節被讀到了Buffer中。如果返回-1,表示到了文件末尾。 int bytesRead = inChannel.read(buf);FileChannel寫數據
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); } //用完FileChannel后必須將其關閉 channel.close();4、Selector
Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,并能夠知曉通道是否為諸如讀寫事件做好準備的組件。這樣,一個多帶帶的線程可以管理多個channel,從而管理多個網絡連接。
對于操作系統來說,線程之間上下文切換的開銷很大,而且每個線程都要占用系統的一些資源(如內存)。因此,使用的線程越少越好。
Selector的創建與注冊使用Selector.open()方法創建Selector,為了將Channel和Selector配合使用,必須將channel注冊到selector上。通過SelectableChannel.register()方法來實現。如下所示
//創建Selector Selector selector = Selector.open(); //向Selector注冊通道 channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
與Selector一起使用時,Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以。
register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什么事件感興趣。可以監聽四種不同類型的事件:
事件類型 | 常量 |
---|---|
Connect | SelectionKey.OP_CONNECT |
Accept | SelectionKey.OP_ACCEPT |
Read | SelectionKey.OP_READ |
Write | SelectionKey.OP_WRITE |
通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱為“連接就緒”。一個server socket channel準備好接收新進入的連接稱為“接收就緒”。一個有數據可讀的通道可以說是“讀就緒”。等待寫數據的通道可以說是“寫就緒”。
SelectionKey當向Selector注冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些屬性:
interest集合
ready集合
Channel
Selector
附加的對象
interest集合
interest集合是你所選擇的感興趣的事件集合。可以通過SelectionKey讀寫interest集合,像這樣:
int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
用|位與操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest 集合中。
ready集合
ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之后,會首先訪問這個ready set。可以這樣訪問ready集合:
int readySet = selectionKey.readyOps(); selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
可以用像檢測interest集合那樣的方法,來檢測channel中什么事件或操作已經就緒。但是,也可以使用以上四個方法,它們都會返回一個布爾類型。
Channel + Selector
從SelectionKey訪問Channel和Selector很簡單。如下:
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
附加的對象
可以將一個對象或者更多信息附著到SelectionKey上,可以方便識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集數據的某個對象。使用方法如下:
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment(); //用register()方法向Selector注冊Channel的時候附加對象 SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);Selector選擇通道
一旦向Selector注冊了一或多個通道,就可以調用select()方法,選擇已經準備就緒的事件的通道類型。
select方法
int select() //阻塞到至少有一個通道在你注冊的事件上就緒了 int select(long timeout)//和select()一樣,除了最長會阻塞timeout毫秒(參數)。 int selectNow()//不會阻塞,不管什么通道就緒都立刻返回
select()方法返回的int值表示有多少通道已經就緒,然后可以通過調用selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:
//訪問“已選擇鍵集(selected key set)”中的就緒通道 Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); }
這個循環遍歷已選擇鍵集中的每個鍵,并檢測各個鍵所對應的通道的就緒事件。
注意每次迭代末尾的keyIterator.remove()調用。Selector不會自己從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
wakeUp()
某個線程調用select()方法后阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。
如果有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會立即“醒來(wake up)”。
close()
用完Selector后調用其close()方法會關閉該Selector,且使注冊到該Selector上的所有SelectionKey實例無效。通道本身并不會關閉。
5、 管道(pip)管道是2個線程之間的單向數據連接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。
原理圖如下:
//Pipe.open()方法打開管道 Pipe pipe = Pipe.open(); //訪問sink通道,向管道寫數據 Pipe.SinkChannel sinkChannel = pipe.sink(); //調用SinkChannel的write()方法,將數據寫入SinkChannel String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { sinkChannel.write(buf); }管道讀取數據
//訪問source通道 Pipe.SourceChannel sourceChannel = pipe.source(); //調用source通道的read()方法來讀取數據 ByteBuffer buf = ByteBuffer.allocate(48); //read()方法返回的int值表示多少字節被讀進了緩沖區 int bytesRead = sourceChannel.read(buf);
參考資料引用
1、NIO 入門
2、Java NIO教程
3、理解Java NIO
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/66975.html
摘要:從通道進行數據寫入創建一個緩沖區,填充數據,并要求通道寫入數據。三之通道主要內容通道介紹通常來說中的所有都是從通道開始的。從中選擇選擇器維護注冊過的通道的集合,并且這種注冊關系都被封裝在當中停止選擇的方法方法和方法。 由于內容比較多,我下面放的一部分是我更新在我的微信公眾號上的鏈接,微信排版比較好看,更加利于閱讀。每一篇文章下面我都把文章的主要內容給列出來了,便于大家學習與回顧。 Ja...
摘要:的選擇器允許單個線程監視多個輸入通道。一旦執行的線程已經超過讀取代碼中的某個數據片段,該線程就不會在數據中向后移動通常不會。 1、引言 很多初涉網絡編程的程序員,在研究Java NIO(即異步IO)和經典IO(也就是常說的阻塞式IO)的API時,很快就會發現一個問題:我什么時候應該使用經典IO,什么時候應該使用NIO? 在本文中,將嘗試用簡明扼要的文字,闡明Java NIO和經典IO之...
摘要:而我們現在都已經發布了,的都不知道,這有點說不過去了。而對一個的讀寫也會有響應的描述符,稱為文件描述符,描述符就是一個數字,指向內核中的一個結構體文件路徑,數據區等一些屬性。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 本來我預想是先來回顧一下傳統的IO模式的,將傳統的IO模式的相關類理清楚(因為IO的類很多)。 但是,發現在整理的過程已...
摘要:上篇說了最基礎的五種模型,相信大家對相關的概念應該有了一定的了解,這篇文章主要講講基于多路復用的。 上篇說了最基礎的五種IO模型,相信大家對IO相關的概念應該有了一定的了解,這篇文章主要講講基于多路復用IO的Java NIO。 背景 Java誕生至今,有好多種IO模型,從最早的Java IO到后來的Java NIO以及最新的Java AIO,每種IO模型都有它自己的特點,詳情請看我的上...
摘要:的出現解決了這尷尬的問題,非阻塞模式下,通過,我們的線程只為已就緒的通道工作,不用盲目的重試了。注意要將注冊到,首先需要將設置為非阻塞模式,否則會拋異常。 showImg(https://segmentfault.com/img/remote/1460000017053374); 背景知識 同步、異步、阻塞、非阻塞 首先,這幾個概念非常容易搞混淆,但NIO中又有涉及,所以總結一下。 ...
閱讀 2682·2021-11-16 11:53
閱讀 2744·2021-07-26 23:38
閱讀 2077·2019-08-30 15:55
閱讀 1758·2019-08-30 13:21
閱讀 3671·2019-08-29 17:26
閱讀 3313·2019-08-29 13:20
閱讀 881·2019-08-29 12:20
閱讀 3198·2019-08-26 10:21