【本文版權歸微信公衆號"代碼藝術"(ID:onblog)全部,如果轉載請務必保留本段原創聲明,違者必究。如果文章有不足之處,歡迎關注微信公衆號私信與我進行交流!】本文是從網絡複製、通過個人整理、開光而來的,而來的,來的,的。
爲何?由於寫的太好了~沒事打開看看打發打發時間,哈哈~html
NIO
類包含在一個叫做java.nio
包的包中。要了解NIO子系統不會取代java.io
包中可用的基於流的I/O類,若是有對java.io
基於流的I/O的如何工做有全部瞭解,這有助於您學習和使用NIO
中的知識內容。 java
NIO
類包含在如下包中:git
包名稱 | 使用/目的 |
---|---|
java.nio |
它是NIO系統的頂級包,NIO系統封裝了各類類型的緩衝區。 |
java.nio.charset |
它封裝了字符集,而且還支持分別將字符轉換爲字節和字節到編碼器和解碼器的操做。 |
java.nio.charset.spi |
它支持字符集服務提供者 |
java.nio.channels |
它支持通道,這些通道本質上是打開I/O鏈接。 |
java.nio.channels.spi |
它支持頻道的服務提供者 |
java.nio.file |
它提供對文件的支持 |
java.nio.file.spi |
它支持文件系統的服務提供者 |
java.nio.file.attribute |
它提供對文件屬性的支持 |
原文連接: http://tutorials.jenkov.com/j...
Java NIO Channel通道和流很是類似,主要有如下幾點區別:github
正如上面提到的,咱們能夠從通道中讀取數據,寫入到buffer;也能夠中buffer內讀數據,寫入到通道中。下面有個示意圖:編程
Java NIO: Channels read data into Buffers, and Buffers write data into Channels數組
下面列出Java NIO中最重要的集中Channel的實現:緩存
FileChannel用於文件的數據讀寫。 DatagramChannel用於UDP的數據讀寫。 SocketChannel用於TCP的數據讀寫。 ServerSocketChannel容許咱們監聽TCP連接請求,每一個請求會建立會一個SocketChannel.安全
這有一個利用FileChannel讀取數據到Buffer的例子:服務器
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); while (bytesRead != -1) { System.out.println("Read " + bytesRead); buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get()); } buf.clear(); bytesRead = inChannel.read(buf); } aFile.close();
注意buf.flip()的調用。首先把數據讀取到Buffer中,而後調用flip()方法。接着再把數據讀取出來。在後續的章節中咱們還會講解先關知識。微信
原文連接: http://tutorials.jenkov.com/j...
Java NIO Buffers用於和NIO Channel交互。正如你已經知道的,咱們從channel中讀取數據到buffers裏,從buffer把數據寫入到channels.
buffer本質上就是一塊內存區,能夠用來寫入數據,並在稍後讀取出來。這塊內存被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發的接口。
利用Buffer讀寫數據,一般遵循四個步驟:
當寫入數據到buffer中時,buffer會記錄已經寫入的數據大小。當須要讀數據時,經過flip()方法把buffer從寫模式調整爲讀模式;在讀模式下,能夠讀取全部已經寫入的數據。
當讀取完數據後,須要清空buffer,以知足後續寫入操做。清空buffer有兩種方式:調用clear()或compact()方法。clear會清空整個buffer,compact則只清空已讀取的數據,未被讀取的數據會被移動到buffer的開始位置,寫入位置則近跟着未讀數據以後。
這裏有一個簡單的buffer案例,包括了write,flip和clear操做:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //create buffer with capacity of 48 bytes ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); //read into buffer. while (bytesRead != -1) { 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); } aFile.close();
buffer緩衝區實質上就是一塊內存,用於寫入數據,也供後續再次讀取數據。這塊內存被NIO Buffer管理,並提供一系列的方法用於更簡單的操做這塊內存。
一個Buffer有三個屬性是必須掌握的,分別是:
position和limit的具體含義取決於當前buffer的模式。capacity在兩種模式下都表示容量。
下面有張示例圖,描訴了不一樣模式下position和limit的含義:
Buffer capacity, position and limit in write and read mode.
做爲一塊內存,buffer有一個固定的大小,叫作capacity容量。也就是最多隻能寫入容量值得字節,整形等數據。一旦buffer寫滿了就須要清空已讀數據以便下次繼續寫入新的數據。
當寫入數據到Buffer的時候須要中一個肯定的位置開始,默認初始化時這個位置position爲0,一旦寫入了數據好比一個字節,整形數據,那麼position的值就會指向數據以後的一個單元,position最大能夠到capacity-1.
當從Buffer讀取數據時,也須要從一個肯定的位置開始。buffer從寫入模式變爲讀取模式時,position會歸零,每次讀取後,position向後移動。
在寫模式,limit的含義是咱們所能寫入的最大數據量。它等同於buffer的容量。
一旦切換到讀模式,limit則表明咱們所能讀取的最大數據量,他的值等同於寫模式下position的位置。
數據讀取的上限時buffer中已有的數據,也就是limit的位置(原position所指的位置)。
Java NIO有以下具體的Buffer類型:
正如你看到的,Buffer的類型表明了不一樣數據類型,換句話說,Buffer中的數據能夠是上述的基本類型;
MappedByteBuffer稍有不一樣,咱們會單獨介紹。
爲了獲取一個Buffer對象,你必須先分配。每一個Buffer實現類都有一個allocate()方法用於分配內存。下面看一個實例,開闢一個48字節大小的buffer:
ByteBuffer buf = ByteBuffer.allocate(48);
開闢一個1024個字符的CharBuffer:
CharBuffer buf = CharBuffer.allocate(1024);
寫數據到Buffer有兩種方法:
下面是一個實例,演示從Channel寫數據到Buffer:
int bytesRead = inChannel.read(buf); //read into buffer.
經過put寫數據:
buf.put(127);
put方法有不少不一樣版本,對應不一樣的寫數據方法。例如把數據寫到特定的位置,或者把一個字節數據寫入buffer。看考JavaDoc文檔能夠查閱的更多數據。
flip()方法能夠吧Buffer從寫模式切換到讀模式。調用flip方法會把position歸零,並設置limit爲以前的position的值。 也就是說,如今position表明的是讀取位置,limit標示的是已寫入的數據位置。
衝Buffer讀數據也有兩種方式。
讀取數據到channel的例子:
//read from buffer into channel. int bytesWritten = inChannel.write(buf);
調用get讀取數據的例子:
byte aByte = buf.get();
get也有諸多版本,對應了不一樣的讀取方式。
Buffer.rewind()方法將position置爲0,這樣咱們能夠重複讀取buffer中的數據。limit保持不變。
一旦咱們從buffer中讀取完數據,須要複用buffer爲下次寫數據作準備。只須要調用clear或compact方法。
clear方法會重置position爲0,limit爲capacity,也就是整個Buffer清空。實際上Buffer中數據並無清空,咱們只是把標記爲修改了。
若是Buffer還有一些數據沒有讀取完,調用clear就會致使這部分數據被「遺忘」,由於咱們沒有標記這部分數據未讀。
針對這種狀況,若是須要保留未讀數據,那麼可使用compact。 所以compact和clear的區別就在於對未讀數據的處理,是保留這部分數據仍是一塊兒清空。
經過mark方法能夠標記當前的position,經過reset來恢復mark的位置,這個很是像canva的save和restore:
buffer.mark(); //call buffer.get() a couple of times, e.g. during parsing. buffer.reset(); //set position back to mark.
能夠用eqauls和compareTo比較兩個buffer
判斷兩個buffer相對,需知足:
從上面的三個條件能夠看出,equals只比較buffer中的部份內容,並不會去比較每個元素。
compareTo也是比較buffer中的剩餘元素,只不過這個方法適用於比較排序的:
原文連接: http://tutorials.jenkov.com/j...
Java NIO發佈時內置了對scatter / gather的支持。scatter / gather是經過通道讀寫數據的兩個概念。
Scattering read指的是從通道讀取的操做能把數據寫入多個buffer,也就是sctters表明了數據從一個channel到多個buffer的過程。
gathering write則正好相反,表示的是從多個buffer把數據寫入到一個channel中。
Scatter/gather在有些場景下會很是有用,好比須要處理多份分開傳輸的數據。舉例來講,假設一個消息包含了header和body,咱們可能會把header和body保存在不一樣獨立buffer中,這種分開處理header與body的作法會使開發更簡明。
"scattering read"是把數據從單個Channel寫入到多個buffer,下面是示意圖:
Java NIO: Scattering Read
用代碼來表示的話以下:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
觀察代碼能夠發現,咱們把多個buffer寫在了一個數組中,而後把數組傳遞給channel.read()方法。read()方法內部會負責把數據按順序寫進傳入的buffer數組內。一個buffer寫滿後,接着寫到下一個buffer中。
實際上,scattering read內部必須寫滿一個buffer後纔會向後移動到下一個buffer,所以這並不適合消息大小會動態改變的部分,也就是說,若是你有一個header和body,而且header有一個固定的大小(好比128字節),這種情形下能夠正常工做。
"gathering write"把多個buffer的數據寫入到同一個channel中,下面是示意圖:
Java NIO: Gathering Write
用代碼表示的話以下:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); //write data into buffers ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
相似的傳入一個buffer數組給write,內部機會按順序將數組內的內容寫進channel,這裏須要注意,寫入的時候針對的是buffer中position到limit之間的數據。也就是若是buffer的容量是128字節,但它只包含了58字節數據,那麼寫入的時候只有58字節會真正寫入。所以gathering write是能夠適用於可變大小的message的,這和scattering reads不一樣。
原文連接: http://tutorials.jenkov.com/j...
在Java NIO中若是一個channel是FileChannel類型的,那麼他能夠直接把數據傳輸到另外一個channel。逐個特性得益於FileChannel包含的transferTo和transferFrom兩個方法。
FileChannel.transferFrom方法把數據從通道源傳輸到FileChannel:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); toChannel.transferFrom(fromChannel, position, count);
transferFrom的參數position和count表示目標文件的寫入位置和最多寫入的數據量。若是通道源的數據小於count那麼就傳實際有的數據量。 另外,有些SocketChannel的實如今傳輸時只會傳輸哪些處於就緒狀態的數據,即便SocketChannel後續會有更多可用數據。所以,這個傳輸過程可能不會傳輸整個的數據。
transferTo方法把FileChannel數據傳輸到另外一個channel,下面是案例:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); fromChannel.transferTo(position, count, toChannel);
這段代碼和以前介紹transfer時的代碼很是類似,區別只在於調用方法的是哪一個FileChannel.
SocketChannel的問題也存在與transferTo.SocketChannel的實現可能只在發送的buffer填充滿後才發送,並結束。
原文連接: http://tutorials.jenkov.com/j...
一個Java NIO的管道是兩個線程間單向傳輸數據的鏈接。一個管道(Pipe)有一個source channel和一個sink channel(沒想到合適的中文名)。咱們把數據寫到sink channel中,這些數據能夠同過source channel再讀取出來。
下面是一個管道的示意圖:
打開一個管道經過調用Pipe.open()工廠方法,以下:
Pipe pipe = Pipe.open();
向管道寫入數據須要訪問他的sink channel:
Pipe.SinkChannel sinkChannel = pipe.sink();
接下來就是調用write()方法寫入數據了:
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 channel:
Pipe.SourceChannel sourceChannel = pipe.source();
接下來調用read()方法讀取數據:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
注意這裏read()的整形返回值表明實際讀取到的字節數。
Selector(選擇器)是 Java NIO 中可以檢測一到多個 NIO 通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個 channel,從而管理多個網絡鏈接。
下面是本文所涉及到的主題列表:
僅用單個線程來處理多個 Channels 的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。
可是,須要記住,現代的操做系統和 CPU 在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個 CPU 有多個內核,不使用多任務多是在浪費 CPU 能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用 Selector 可以處理多個通道就足夠了。
下面是單線程使用一個 Selector 處理 3 個 channel 的示例圖:
經過調用 Selector.open() 方法建立一個 Selector,以下:
Selector selector = Selector.open();
爲了將 Channel 和 Selector 配合使用,必須將 channel 註冊到 selector 上。經過 SelectableChannel.register() 方法來實現,以下:
channel.configureBlocking(false); SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
與 Selector 一塊兒使用時,Channel 必須處於非阻塞模式下。這意味着不能將 FileChannel 與 Selector 一塊兒使用,由於 FileChannel 不能切換到非阻塞模式。而套接字通道均可以。
注意 register() 方法的第二個參數。這是一個 「interest 集合」,意思是在經過 Selector 監聽 Channel 時對什麼事件感興趣。能夠監聽四種不一樣類型的事件:
通道觸發了一個事件意思是該事件已經就緒。因此,某個 channel 成功鏈接到另外一個服務器稱爲 「鏈接就緒」。一個 server socket channel 準備好接收新進入的鏈接稱爲 「接收就緒」。一個有數據可讀的通道能夠說是 「讀就緒」。等待寫數據的通道能夠說是 「寫就緒」。
這四種事件用 SelectionKey 的四個常量來表示:
若是你對不止一種事件感興趣,那麼能夠用 「位或」 操做符將常量鏈接起來,以下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
在下面還會繼續提到 interest 集合
在上一小節中,當向 Selector 註冊 Channel 時,register() 方法會返回一個 SelectionKey 對象。這個對象包含了一些你感興趣的屬性:
下面我會描述這些屬性。
interest 集合
就像向 Selector 註冊通道一節中所描述的,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。Selection 將在下一小節進行解釋。能夠這樣訪問 ready 集合:
int readySet = selectionKey.readyOps();
能夠用像檢測 interest 集合那樣的方法,來檢測 channel 中什麼事件或操做已經就緒。可是,也可使用如下四個方法,它們都會返回一個布爾類型:
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
Channel + Selector
從 SelectionKey 訪問 Channel 和 Selector 很簡單。以下:
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
還能夠在用 register() 方法向 Selector 註冊 Channel 的時候附加對象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
一旦向 Selector 註冊了一或多個通道,就能夠調用幾個重載的 select() 方法。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫)已經準備就緒的那些通道。換句話說,若是你對 「讀就緒」 的通道感興趣,select() 方法會返回讀事件已經就緒的那些通道。
下面是 select() 方法:
select()阻塞到至少有一個通道在你註冊的事件上就緒了。
select(long timeout)和 select() 同樣,除了最長會阻塞 timeout 毫秒 (參數)。
selectNow()不會阻塞,無論什麼通道就緒都馬上返回(譯者注:此方法執行非阻塞的選擇操做。若是自從前一次選擇操做後,沒有通道變成可選擇的,則此方法直接返回零。)。
select() 方法返回的 int 值表示有多少通道已經就緒。亦即,自上次調用 select() 方法後有多少通道變成就緒狀態。若是調用 select() 方法,由於有一個通道變成就緒狀態,返回了 1,若再次調用 select() 方法,若是另外一個通道就緒了,它會再次返回 1。若是對第一個就緒的 channel 沒有作任何操做,如今就有兩個就緒的通道,但在每次 select() 方法調用之間,只有一個通道就緒了。
selectedKeys()
一旦調用了 select() 方法,而且返回值代表有一個或更多個通道就緒了,而後能夠經過調用 selector 的 selectedKeys() 方法,訪問 「已選擇鍵集(selected key set)」 中的就緒通道。以下所示:
Set selectedKeys = selector.selectedKeys();
當像 Selector 註冊 Channel 時,Channel.register() 方法會返回一個 SelectionKey 對象。這個對象表明了註冊到該 Selector 的通道。能夠經過 SelectionKey 的 selectedKeySet() 方法訪問這些對象。
能夠遍歷這個已選擇的鍵集合來訪問就緒的通道。以下:
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 會再次將其放入已選擇鍵集中。
SelectionKey.channel() 方法返回的通道須要轉型成你要處理的類型,如 ServerSocketChannel 或 SocketChannel 等。
wakeUp()
某個線程調用 select() 方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從 select() 方法返回。只要讓其它線程在第一個線程調用 select() 方法的那個對象上調用 Selector.wakeup() 方法便可。阻塞在 select() 方法上的線程會立馬返回。
若是有其它線程調用了 wakeup() 方法,但當前沒有線程阻塞在 select() 方法上,下個調用 select() 方法的線程會當即 「醒來(wake up)」。
close()
用完 Selector 後調用其 close() 方法會關閉該 Selector,且使註冊到該 Selector 上的全部 SelectionKey 實例無效。通道自己並不會關閉。
這裏有一個完整的示例,打開一個 Selector,註冊一個通道註冊到這個 Selector 上 (通道的初始化過程略去), 而後持續監控這個 Selector 的四種事件(接受,鏈接,讀,寫)是否就緒。
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; 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(); } }
Java NIO 中的 FileChannel 是一個鏈接到文件的通道。能夠經過文件通道讀寫文件。
FileChannel 沒法設置爲非阻塞模式,它老是運行在阻塞模式下。
在使用 FileChannel 以前,必須先打開它。可是,咱們沒法直接打開一個 FileChannel,須要經過使用一個 InputStream、OutputStream 或 RandomAccessFile 來獲取一個 FileChannel 實例。下面是經過 RandomAccessFile 打開 FileChannel 的示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel();
調用多個 read() 方法之一從 FileChannel 中讀取數據。如:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
首先,分配一個 Buffer。從 FileChannel 中讀取的數據將被讀到 Buffer 中。
而後,調用 FileChannel.read() 方法。該方法將數據從 FileChannel 讀取到 Buffer 中。read() 方法返回的 int 值表示了有多少字節被讀到了 Buffer 中。若是返回 - 1,表示到了文件末尾。
使用 FileChannel.write() 方法向 FileChannel 寫數據,該方法的參數是一個 Buffer。如:
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.write() 是在 while 循環中調用的。由於沒法保證 write() 方法一次能向 FileChannel 寫入多少字節,所以須要重複調用 write() 方法,直到 Buffer 中已經沒有還沒有寫入通道的字節。
用完 FileChannel 後必須將其關閉。如:
channel.close();
有時可能須要在 FileChannel 的某個特定位置進行數據的讀 / 寫操做。能夠經過調用 position() 方法獲取 FileChannel 的當前位置。
也能夠經過調用 position(long pos) 方法設置 FileChannel 的當前位置。
這裏有兩個例子:
long pos = channel.position(); channel.position(pos +123);
若是將位置設置在文件結束符以後,而後試圖從文件通道中讀取數據,讀方法將返回 - 1 —— 文件結束標誌。
若是將位置設置在文件結束符以後,而後向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能致使 「文件空洞」,磁盤上物理文件中寫入的數據間有空隙。
FileChannel 實例的 size() 方法將返回該實例所關聯文件的大小。如:
long fileSize = channel.size();
可使用 FileChannel.truncate() 方法截取一個文件。截取文件時,文件將中指定長度後面的部分將被刪除。如:
channel.truncate(1024);
這個例子截取文件的前 1024 個字節。
FileChannel.force() 方法將通道里還沒有寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操做系統會將數據緩存在內存中,因此沒法保證寫入到 FileChannel 裏的數據必定會即時寫到磁盤上。要保證這一點,須要調用 force() 方法。
force() 方法有一個 boolean 類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。
下面的例子同時將文件數據和元數據強制寫到磁盤上:
channel.force(true);
Java NIO 中的 ServerSocketChannel 是一個能夠監聽新進來的 TCP 鏈接的通道, 就像標準 IO 中的 ServerSocket 同樣。ServerSocketChannel 類在 java.nio.channels 包中。
這裏有個例子:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9999)); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... }
經過調用 ServerSocketChannel.open() 方法來打開 ServerSocketChannel. 如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
經過調用 ServerSocketChannel.close() 方法來關閉 ServerSocketChannel. 如:
serverSocketChannel.close();
經過 ServerSocketChannel.accept() 方法監聽新進來的鏈接。當 accept() 方法返回的時候, 它返回一個包含新進來的鏈接的 SocketChannel。所以, accept() 方法會一直阻塞到有新鏈接到達。
一般不會僅僅只監聽一個鏈接, 在 while 循環中調用 accept() 方法. 以下面的例子:
while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... }
固然, 也能夠在 while 循環中使用除了 true 之外的其它退出準則。
ServerSocketChannel 能夠設置成非阻塞模式。在非阻塞模式下,accept() 方法會馬上返回,若是尚未新進來的鏈接,返回的將是 null。 所以,須要檢查返回的 SocketChannel 是不是 null。 如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9999)); serverSocketChannel.configureBlocking(false); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ //do something with socketChannel... } }
Java NIO 中的 SocketChannel 是一個鏈接到 TCP 網絡套接字的通道。能夠經過如下 2 種方式建立 SocketChannel:
下面是 SocketChannel 的打開方式:
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
當用完 SocketChannel 以後調用 SocketChannel.close() 關閉 SocketChannel:
socketChannel.close();
要從 SocketChannel 中讀取數據,調用一個 read() 的方法之一。如下是例子:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = socketChannel.read(buf);
首先,分配一個 Buffer。從 SocketChannel 讀取到的數據將會放到這個 Buffer 中。
而後,調用 SocketChannel.read()。該方法將數據從 SocketChannel 讀到 Buffer 中。read() 方法返回的 int 值表示讀了多少字節進 Buffer 裏。若是返回的是 - 1,表示已經讀到了流的末尾(鏈接關閉了)。
寫數據到 SocketChannel 用的是 SocketChannel.write() 方法,該方法以一個 Buffer 做爲參數。示例以下:
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); }
注意 SocketChannel.write() 方法的調用是在一個 while 循環中的。Write() 方法沒法保證能寫多少字節到 SocketChannel。因此,咱們重複調用 write() 直到 Buffer 沒有要寫的字節爲止。
能夠設置 SocketChannel 爲非阻塞模式(non-blocking mode). 設置以後,就能夠在異步模式下調用 connect(), read() 和 write() 了。
connect()
若是 SocketChannel 在非阻塞模式下,此時調用 connect(),該方法可能在鏈接創建以前就返回了。爲了肯定鏈接是否創建,能夠調用 finishConnect() 的方法。像這樣:
socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80)); while(! socketChannel.finishConnect() ){ //wait, or do something else... }
write()
非阻塞模式下,write() 方法在還沒有寫出任何內容時可能就返回了。因此須要在循環中調用 write()。前面已經有例子了,這裏就不贅述了。
read()
非阻塞模式下, read() 方法在還沒有讀取到任何數據時可能就返回了。因此須要關注它的 int 返回值,它會告訴你讀取了多少字節。
非阻塞模式與選擇器搭配會工做的更好,經過將一或多個 SocketChannel 註冊到 Selector,能夠詢問選擇器哪一個通道已經準備好了讀取,寫入等。Selector 與 SocketChannel 的搭配使用會在後面詳講。
Java NIO 中的 DatagramChannel 是一個能收發 UDP 包的通道。由於 UDP 是無鏈接的網絡協議,因此不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。
下面是 DatagramChannel 的打開方式:
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999));
這個例子打開的 DatagramChannel 能夠在 UDP 端口 9999 上接收數據包。
經過 receive() 方法從 DatagramChannel 接收數據,如:
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf);
receive() 方法會將接收到的數據包內容複製到指定的 Buffer. 若是 Buffer 容不下收到的數據,多出的數據將被丟棄。
經過 send() 方法從 DatagramChannel 發送數據,如:
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
這個例子發送一串字符到」jenkov.com」 服務器的 UDP 端口 80。 由於服務端並無監控這個端口,因此什麼也不會發生。也不會通知你發出的數據包是否已收到,由於 UDP 在數據傳送方面沒有任何保證。
能夠將 DatagramChannel「鏈接」 到網絡中的特定地址的。因爲 UDP 是無鏈接的,鏈接到特定地址並不會像 TCP 通道那樣建立一個真正的鏈接。而是鎖住 DatagramChannel ,讓其只能從特定地址收發數據。
這裏有個例子:
channel.connect(new InetSocketAddress("jenkov.com", 80));
當鏈接後,也可使用 read() 和 write() 方法,就像在用傳統的通道同樣。只是在數據傳送方面沒有任何保證。這裏有幾個例子:
int bytesRead = channel.read(buf); int bytesWritten = channel.write(but);
當學習了 Java NIO 和 IO 的 API 後,一個問題立刻涌入腦海:
我應該什麼時候使用 IO,什麼時候使用 NIO 呢?在本文中,我會盡可能清晰地解析 Java NIO 和 IO 的差別、它們的使用場景,以及它們如何影響您的代碼設計。
下表總結了 Java NIO 和 IO 之間的主要差異,我會更詳細地描述表中每部分的差別。
IO NIO 面向流 面向緩衝 阻塞IO 非阻塞IO 無 選擇器
Java NIO 和 IO 之間第一個最大的區別是,IO 是面向流的,NIO 是面向緩衝區的。 Java IO 面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。 Java NIO 的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據。
Java IO 的各類流是阻塞的。這意味着,當一個線程調用 read() 或 write() 時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。 Java NIO 的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞 IO 的空閒時間用於在其它通道上執行 IO 操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。
Java NIO 的選擇器容許一個單獨的線程來監視多個輸入通道,你能夠註冊多個通道使用一個選擇器,而後使用一個單獨的線程來 「選擇」 通道:這些通道里已經有能夠處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
不管您選擇 IO 或 NIO 工具箱,可能會影響您應用程序設計的如下幾個方面:
固然,使用 NIO 的 API 調用時看起來與使用 IO 時有所不一樣,但這並不意外,由於並非僅從一個 InputStream 逐字節讀取,而是數據必須先讀入緩衝區再處理。
使用純粹的 NIO 設計相較 IO 設計,數據處理也受到影響。
在 IO 設計中,咱們從 InputStream 或 Reader 逐字節讀取數據。假設你正在處理一基於行的文本數據流,例如:
Name: Anna Age: 25 Email: anna@mailserver.com Phone: 1234567890
該文本行的流能夠這樣處理:
InputStream input = … ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input)); String nameLine = reader.readLine(); String ageLine = reader.readLine(); String emailLine = reader.readLine(); String phoneLine = reader.readLine();
請注意處理狀態由程序執行多久決定。換句話說,一旦 reader.readLine() 方法返回,你就知道確定文本行就已讀完, readline() 阻塞直到整行讀完,這就是緣由。你也知道此行包含名稱;一樣,第二個 readline() 調用返回的時候,你知道這行包含年齡等。 正如你能夠看到,該處理程序僅在有新數據讀入時運行,並知道每步的數據是什麼。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據(大多如此)。下圖也說明了這條原則:
(Java IO: 從一個阻塞的流中讀數據) 而一個 NIO 的實現會有所不一樣,下面是一個簡單的例子:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
注意第二行,從通道讀取字節到 ByteBuffer。當這個方法調用返回時,你不知道你所需的全部數據是否在緩衝區內。你所知道的是,該緩衝區包含一些字節,這使得處理有點困難。
假設第一次 read(buffer) 調用後,讀入緩衝區的數據只有半行,例如,「Name:An」,你能處理數據嗎?顯然不能,須要等待,直到整行數據讀入緩存,在此以前,對數據的任何處理毫無心義。
因此,你怎麼知道是否該緩衝區包含足夠的數據能夠處理呢?好了,你不知道。發現的方法只能查看緩衝區中的數據。其結果是,在你知道全部數據都在緩衝區裏以前,你必須檢查幾回緩衝區的數據。這不只效率低下,並且可使程序設計方案雜亂不堪。例如:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
bufferFull() 方法必須跟蹤有多少數據讀入緩衝區,並返回真或假,這取決於緩衝區是否已滿。換句話說,若是緩衝區準備好被處理,那麼表示緩衝區滿了。
bufferFull() 方法掃描緩衝區,但必須保持在 bufferFull()方法被調用以前狀態相同。若是沒有,下一個讀入緩衝區的數據可能沒法讀到正確的位置。這是不可能的,但倒是須要注意的又一問題。
若是緩衝區已滿,它能夠被處理。若是它不滿,而且在你的實際案例中有意義,你或許能處理其中的部分數據。可是許多狀況下並不是如此。下圖展現了 「緩衝區數據循環就緒」:
Java NIO:從一個通道里讀數據,直到全部的數據都讀到緩衝區裏。
NIO 可以讓您只使用一個(或幾個)單線程管理多個通道(網絡鏈接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
若是須要管理同時打開的成千上萬個鏈接,這些鏈接每次只是發送少許的數據,例如聊天服務器,實現 NIO 的服務器多是一個優點。一樣,若是你須要維持許多打開的鏈接到其餘計算機上,如 P2P 網絡中,使用一個單獨的線程來管理你全部出站鏈接,多是一個優點。一個線程多個鏈接的設計方案以下圖所示:
Java NIO: 單線程管理多個鏈接。
若是你有少許的鏈接使用很是高的帶寬,一次發送大量的數據,也許典型的 IO 服務器實現可能很是契合。下圖說明了一個典型的 IO 服務器設計:
Java IO:一個典型的 IO 服務器設計 - 一個鏈接經過一個線程處理。
原文連接: http://tutorials.jenkov.com/j...
Java的path接口是做爲Java NIO 2的一部分是Java6,7中NIO的升級增長部分。Path在Java 7新增的。相關接口位於java.nio.file包下,因此Javaz內Path接口的完整名稱是java.nio.file.Path.
一個Path實例表明一個文件系統內的路徑。path能夠指向文件也能夠指向目錄。可使相對路徑也能夠是絕對路徑。絕對路徑包含了從根目錄到該文件(目錄)的完整路徑。相對路徑包含該文件(目錄)相對於其餘路徑的路徑。相對路徑聽起來可能有點讓人頭暈。可是別急,稍後咱們會詳細介紹。
不要把文件系統中路徑和環境變量的路徑混淆。java.nio.file.Path和環境變量沒有任何關係。
在不少狀況下java.no.file.Path接口和java.io.File比較類似,可是他們之間存在一些細微的差別。儘管如此,在大多數狀況下,咱們均可以用File相關類來替換Path接口。
爲了使用java.nio.file.Path實例咱們必須建立Path對象。建立Path實例能夠經過Paths的工廠方法get()。下面是一個實例:
import java.nio.file.Path; import java.nio.file.Paths; public classs PathExample { public static void mian(String[] args) { Path = path = Paths.get("c:\\data\\myfile.txt"); } }
注意上面的兩個import聲明。須要使用Path和Paths的接口,畢現先把他們引入。
其次注意Paths.get("c:datamyfile.txt")的調用。這個方法會建立一個Path實例,換句話說Paths.get()是Paths的一個工廠方法。
建立絕對路徑只須要調動Paths.get()這個工廠方法,同時傳入絕對文件。這是一個例子:
Path path = Paths.get("c:\\data\\myfile.txt");
對路徑是c:datamyfile.txt,裏面的雙斜槓字符是Java 字符串中必須的,由於是轉義字符,表示後面跟的字符在字符串中的真實含義。雙斜槓表示自身。
上面的路徑是Windows下的文件系統路徑表示。在Unixx系統中(Linux, MacOS,FreeBSD等)上述的絕對路徑長得是這樣的:
Path path = Paths.get("/home/jakobjenkov/myfile.txt");
他的絕對路徑是/home/jakobjenkov/myfile.txt。 若是在Windows機器上使用用這種路徑,那麼這個路徑會被認爲是相對於當前磁盤的。例如:
/home/jakobjenkov/myfile.txt
這個路徑會被理解其C盤上的文件,因此路徑又變成了
C:/home/jakobjenkov/myfile.txt
相對路徑是從一個路徑(基準路徑)指向另外一個目錄或文件的路徑。完整路徑實際上等同於相對路徑加上基準路徑。
Java NIO的Path類能夠用於相對路徑。建立一個相對路徑能夠經過調用Path.get(basePath, relativePath),下面是一個示例:
Path projects = Paths.get("d:\\data", "projects"); Path file = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");
第一行建立了一個指向d:dataprojects的Path實例。第二行建立了一個指向d:dataprojectsa-projectmyfile.txt的Path實例。 在使用相對路徑的時候有兩個特殊的符號:
.表示的是當前目錄,例如咱們能夠這樣建立一個相對路徑:
Path currentDir = Paths.get("."); System.out.println(currentDir.toAbsolutePath());
currentDir的實際路徑就是當前代碼執行的目錄。 若是在路徑中間使用了.那麼他的含義實際上就是目錄位置自身,例如:
Path currentDir = Paths.get("d:\\data\\projects\.\a-project");
上訴路徑等同於:
d:\data\projects\a-project
..表示父目錄或者說是上一級目錄:
Path parentDir = Paths.get("..");
這個Path實例指向的目錄是當前程序代碼的父目錄。 若是在路徑中間使用..那麼會相應的改變指定的位置:
String path = "d:\\data\\projects\\a-project\\..\\another-project"; Path parentDir2 = Paths.get(path);
這個路徑等同於:
d:\data\projects\another-project
.和..也能夠結合起來用,這裏不過多介紹。
【本文版權歸微信公衆號"代碼藝術"(ID:onblog)全部,如果轉載請務必保留本段原創聲明,違者必究。如果文章有不足之處,歡迎關注微信公衆號私信與我進行交流!】
Path的normalize()方法能夠把路徑規範化。也就是把.和..都等價去除:
String originalPath = "d:\\data\\projects\\a-project\\..\\another-project"; Path path1 = Paths.get(originalPath); System.out.println("path1 = " + path1); Path path2 = path1.normalize(); System.out.println("path2 = " + path2);
這段代碼的輸出以下:
path1 = d:\data\projects\a-project\..\another-project path2 = d:\data\projects\another-project
原文連接: http://tutorials.jenkov.com/j...
Java NIO中的Files類(java.nio.file.Files)提供了多種操做文件系統中文件的方法。本節教程將覆蓋大部分方法。Files類包含了不少方法,因此若是本文沒有提到的你也能夠直接查詢JavaDoc文檔。
java.nio.file.Files類是和java.nio.file.Path相結合使用的,因此在用Files以前確保你已經理解了Path類。
Files.exits()方法用來檢查給定的Path在文件系統中是否存在。 在文件系統中建立一個本來不存在的Payh是可行的。例如,你想新建一個目錄,那麼閒建立對應的Path實例,而後建立目錄。
因爲Path實例可能指向文件系統中的不存在的路徑,因此須要用Files.exists()來確認。
下面是一個使用Files.exists()的示例:
Path path = Paths.get("data/logging.properties"); boolean pathExists = Files.exists(path, new LinkOption[]{ LinkOption.NOFOLLOW_LINKS});
這個示例中,咱們首先建立了一個Path對象,而後利用Files.exists()來檢查這個路徑是否真實存在。
注意Files.exists()的的第二個參數。他是一個數組,這個參數直接影響到Files.exists()如何肯定一個路徑是否存在。在本例中,這個數組內包含了LinkOptions.NOFOLLOW_LINKS,表示檢測時不包含符號連接文件。
Files.createDirectory()會建立Path表示的路徑,下面是一個示例:
Path path = Paths.get("data/subdir"); try { Path newDir = Files.createDirectory(path); } catch(FileAlreadyExistsException e){ // the directory already exists. } catch (IOException e) { //something else went wrong e.printStackTrace(); }
第一行建立了一個Path實例,表示須要建立的目錄。接着用try-catch把Files.createDirectory()的調用捕獲住。若是建立成功,那麼返回值就是新建立的路徑。
若是目錄已經存在了,那麼會拋出java.nio.file.FileAlreadyExistException異常。若是出現其餘問題,會拋出一個IOException。好比說,要建立的目錄的父目錄不存在,那麼就會拋出IOException。父目錄指的是你要建立的目錄所在的位置。也就是新建立的目錄的上一級父目錄。
Files.copy()方法能夠吧一個文件從一個地址複製到另外一個位置。例如:
Path sourcePath = Paths.get("data/logging.properties"); Path destinationPath = Paths.get("data/logging-copy.properties"); try { Files.copy(sourcePath, destinationPath); } catch(FileAlreadyExistsException e) { //destination file already exists } catch (IOException e) { //something else went wrong e.printStackTrace(); }
這個例子當中,首先建立了原文件和目標文件的Path實例。而後把它們做爲參數,傳遞給Files.copy(),接着就會進行文件拷貝。
若是目標文件已經存在,就會拋出java.nio.file.FileAlreadyExistsException異常。相似的吐過中間出錯了,也會拋出IOException。
copy操做能夠強制覆蓋已經存在的目標文件。下面是具體的示例:
Path sourcePath = Paths.get("data/logging.properties"); Path destinationPath = Paths.get("data/logging-copy.properties"); try { Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING); } catch(FileAlreadyExistsException e) { //destination file already exists } catch (IOException e) { //something else went wrong e.printStackTrace(); }
注意copy方法的第三個參數,這個參數決定了是否能夠覆蓋文件。
Java NIO的Files類也包含了移動的文件的接口。移動文件和重命名是同樣的,可是還會改變文件的目錄位置。java.io.File類中的renameTo()方法與之功能是同樣的。
Path sourcePath = Paths.get("data/logging-copy.properties"); Path destinationPath = Paths.get("data/subdir/logging-moved.properties"); try { Files.move(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { //moving file failed. e.printStackTrace(); }
首先建立源路徑和目標路徑的,原路徑指的是須要移動的文件的初始路徑,目標路徑是指須要移動到的位置。
這裏move的第三個參數也容許咱們覆蓋已有的文件。
Files.delete()方法能夠刪除一個文件或目錄:
Path path = Paths.get("data/subdir/logging-moved.properties"); try { Files.delete(path); } catch (IOException e) { //deleting file failed e.printStackTrace(); }
首先建立須要刪除的文件的path對象。接着就能夠調用delete了。
Files.walkFileTree()方法具備遞歸遍歷目錄的功能。walkFileTree接受一個Path和FileVisitor做爲參數。Path對象是須要遍歷的目錄,FileVistor則會在每次遍歷中被調用。
下面先來看一下FileVisitor這個接口的定義:
public interface FileVisitor { public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs) throws IOException; public FileVisitResult visitFile( Path file, BasicFileAttributes attrs) throws IOException; public FileVisitResult visitFileFailed( Path file, IOException exc) throws IOException; public FileVisitResult postVisitDirectory( Path dir, IOException exc) throws IOException { }
FileVisitor須要調用方自行實現,而後做爲參數傳入walkFileTree().FileVisitor的每一個方法會在遍歷過程當中被調用屢次。若是不須要處理每一個方法,那麼能夠繼承他的默認實現類SimpleFileVisitor,它將全部的接口作了空實現。
下面看一個walkFileTree()的示例:
Files.walkFileTree(path, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("pre visit dir:" + dir); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("visit file: " + file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { System.out.println("visit file failed: " + file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { System.out.println("post visit directory: " + dir); return FileVisitResult.CONTINUE; } });
FileVisitor的方法會在不一樣時機被調用: preVisitDirectory()在訪問目錄前被調用。postVisitDirectory()在訪問後調用。
visitFile()會在整個遍歷過程當中的每次訪問文件都被調用。他不是針對目錄的,而是針對文件的。visitFileFailed()調用則是在文件訪問失敗的時候。例如,當缺乏合適的權限或者其餘錯誤。
上述四個方法都返回一個FileVisitResult枚舉對象。具體的可選枚舉項包括:
返回這個枚舉值可讓調用方決定文件遍歷是否須要繼續。 CONTINE表示文件遍歷和正常狀況下同樣繼續。
TERMINATE表示文件訪問須要終止。
SKIP_SIBLINGS表示文件訪問繼續,可是不須要訪問其餘同級文件或目錄。
SKIP_SUBTREE表示繼續訪問,可是不須要訪問該目錄下的子目錄。這個枚舉值僅在preVisitDirectory()中返回纔有效。若是在另外幾個方法中返回,那麼會被理解爲CONTINE。
下面看一個例子,咱們經過walkFileTree()來尋找一個README.txt文件:
Path rootPath = Paths.get("data"); String fileToFind = File.separator + "README.txt"; try { Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String fileString = file.toAbsolutePath().toString(); //System.out.println("pathString = " + fileString); if(fileString.endsWith(fileToFind)){ System.out.println("file found at path: " + file.toAbsolutePath()); return FileVisitResult.TERMINATE; } return FileVisitResult.CONTINUE; } }); } catch(IOException e){ e.printStackTrace(); }
Files.walkFileTree()也能夠用來刪除一個目錄以及內部的全部文件和子目。Files.delete()只用用於刪除一個空目錄。咱們經過遍歷目錄,而後在visitFile()接口中三次全部文件,最後在postVisitDirectory()內刪除目錄自己。
Path rootPath = Paths.get("data/to-delete"); try { Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("delete file: " + file.toString()); Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); System.out.println("delete dir: " + dir.toString()); return FileVisitResult.CONTINUE; } }); } catch(IOException e){ e.printStackTrace(); }
java.nio.file.Files類還有其餘一些頗有用的方法,好比建立符號連接,肯定文件大小以及設置文件權限等。具體用法能夠查閱JavaDoc中的API說明。
FileLock
鎖定或嘗試鎖定文件的給定部分。它屬於java.nio.channels
包,該功能在JDK 1.4以上版本可用。
FileLock
用於在共享模式或非共享模式下鎖定文件。它有兩個重要的方法以下:
FileLock.lock(long position, long size, boolean shared)
FileLock.tryLock(long position, long size, boolean shared)
上述方法使用參數做爲初始位置,文件大小鎖定和一個參數來決定是否共享鎖定。
當使用FileChannel
或AsynchronousFileChannel
的lock()
或tryLock()
方法之一獲取文件鎖時,將建立文件鎖定對象。
下面來看看使用專用鎖定的通道在文件中寫入(附加)的程序(FileLockExample.java):
package com.yiibai; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.ByteBuffer; import java.nio.file.Paths; import java.nio.file.Path; import java.nio.file.StandardOpenOption; public class FileLockExample { public static void main (String [] args) throws IOException { String input = "* end of the file."; System.out.println("Input string to the test file is: " + input); ByteBuffer buf = ByteBuffer.wrap(input.getBytes()); String fp = "testout-file.txt"; Path pt = Paths.get(fp); FileChannel fc = FileChannel.open(pt, StandardOpenOption.WRITE, StandardOpenOption.APPEND); System.out.println("File channel is open for write and Acquiring lock..."); fc.position(fc.size() - 1); // position of a cursor at the end of file FileLock lock = fc.lock(); System.out.println("The Lock is shared: " + lock.isShared()); fc.write(buf); fc.close(); // Releases the Lock System.out.println("Content Writing is complete. Therefore close the channel and release the lock."); PrintFile.print(fp); } } Java
PrintFile.java文件的內容以下 -
package com.yiibai; import java.io.IOException; import java.io.FileReader; import java.io.BufferedReader; public class PrintFile { public static void print(String path) throws IOException { FileReader filereader = new FileReader(path); BufferedReader bufferedreader = new BufferedReader(filereader); String tr = bufferedreader.readLine(); System.out.println("The Content of testout-file.txt file is: "); while (tr != null) { System.out.println(" " + tr); tr = bufferedreader.readLine(); } filereader.close(); bufferedreader.close(); } } Java
注意:在運行代碼以前,須要建立一個名稱爲「testout-file.txt」
的文本文件,文本文件的內容以下:
Welcome to yiibai.com This is the example of FileLock in Java NIO channel. Txt
執行上面示例代碼,獲得如下結果 -
Input string to the test file is: * end of the file. File channel is open for write and Acquiring lock... The Lock is shared: false Content Writing is complete. Therefore close the channel and release the lock. The Content of testout-file.txt file is: Welcome to yiibai.com This is the example of FileLock in Java NIO channel.* end of the file.
java.nio.charset.Charset
包使用的是在JDK 1.4中引入了字符集的概念。它在給定的字符集和UNICODE之間的編碼和解碼中起着重要的做用。
字符集的名稱必須遵循某些規則。它必須以數字或字母開頭。Charset
類方法在多線程環境中也是安全的。
Java支持的字符集列表以下:
8
位UCS轉換格式。7
位ASCII字符。16
位UCS轉換,小字節順序。16
位UCS變換格式charbuffer
編碼爲給定字符集的CharBuffer
。Unicode
字符集的CharBuffer
。基本字符串示例
package com.yiibai; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; public class CharsetExample { public static void main(String[] args) { Charset cs = Charset.forName("UTF-8"); System.out.println(cs.displayName()); System.out.println(cs.canEncode()); String st = "Welcome to yiibai.com, it is Charset test Example."; // The conversion of byte buffer from given charset to char buffer in // unicode ByteBuffer bytebuffer = ByteBuffer.wrap(st.getBytes()); CharBuffer charbuffer = cs.decode(bytebuffer); // The converesion of char buffer from unicode to byte buffer in given // charset ByteBuffer newbytebuffer = cs.encode(charbuffer); while (newbytebuffer.hasRemaining()) { char ca = (char) newbytebuffer.get(); System.out.print(ca); } newbytebuffer.clear(); } } Java
執行上面示例代碼,獲得如下結果 -
UTF-8 true Welcome to yiibai.com, it is Charset test Example.
使用Java NIO API編碼和解碼操做能夠從一個字符串執行到另外一個字符集。兩個類:CharsetEncoder
和CharsetDecoder
在ByteBuffer
和CharBuffer
之間的編碼和解碼中起着重要的做用。
只有當處理程序可用時,反應堆(Reactor
)保持到達事件的跟蹤和調度。咱們來看看反應堆中執行的編碼和解碼操做的架構:
Java NIO中的CharsetEncoder
CharsetEncoder
用於將Unicode
字符編碼爲字節序列。它還返回一個提供任何錯誤信息的CoderResult
對象。
Java NIO中的CharsetDecoder
CharsetDecoder
用於將數組或字節序列解碼爲Unicode
字符。在從ByteBuffer
到CharBuffer
的解碼過程當中,得到CoderResult
對象。
Java NIO中的Charset.newEncoder()
在CharsetEncoder
中,Charset.newEncoder()
用於建立Charset
對象,而後經過newEncoder()
方法,能夠獲取CharsetEncoder
對象。
Java NIO Charset.newDecoder()
在CharsetDecoder
中,Charset.newDecoder()
用於建立Charset
對象,而後經過newDecoder()
方法,能夠獲取一個CharsetDecoder
對象。
基本編碼和解碼示例
package com.yiibai; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; public class CharsetExam { public static void main(String[] args) throws CharacterCodingException { Charset cs = Charset.forName("UTF-8"); CharsetDecoder csdecoder = cs.newDecoder(); CharsetEncoder csencoder = cs.newEncoder(); String st = "Example of Encode and Decode in Java NIO."; ByteBuffer bb = ByteBuffer.wrap(st.getBytes()); CharBuffer cb = csdecoder.decode(bb); ByteBuffer newbb = csencoder.encode(cb); while (newbb.hasRemaining()) { char ca = (char) newbb.get(); System.out.print(ca); } newbb.clear(); } } Java
執行上面示例代碼,獲得如下結果 -
Example of Encode and Decode in Java NIO.
原文連接: http://tutorials.jenkov.com/j...
Java7中新增了AsynchronousFileChannel做爲nio的一部分。AsynchronousFileChannel使得數據能夠進行異步讀寫。下面將介紹一下AsynchronousFileChannel的使用。
AsynchronousFileChannel的建立能夠經過open()靜態方法:
Path path = Paths.get("data/test.xml"); AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
open()的第一個參數是一個Path實體,指向咱們須要操做的文件。 第二個參數是操做類型。上述示例中咱們用的是StandardOpenOption.READ,表示以讀的形式操做文件。
讀取AsynchronousFileChannel的數據有兩種方式。每種方法都會調用AsynchronousFileChannel的一個read()接口。下面分別看一下這兩種寫法。
第一種方式是調用返回值爲Future的read()方法:
Future<Integer> operation = fileChannel.read(buffer, 0);
這種方式中,read()接受一個ByteBuffer座位第一個參數,數據會被讀取到ByteBuffer中。第二個參數是開始讀取數據的位置。
read()方法會馬上返回,即便讀操做沒有完成。咱們能夠經過isDone()方法檢查操做是否完成。
下面是一個略長的示例:
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); yteBuffer buffer = ByteBuffer.allocate(1024); ong position = 0; uture<Integer> operation = fileChannel.read(buffer, position); hile(!operation.isDone()); uffer.flip(); yte[] data = new byte[buffer.limit()]; uffer.get(data); ystem.out.println(new String(data)); uffer.clear();
在這個例子中咱們建立了一個AsynchronousFileChannel,而後建立一個ByteBuffer做爲參數傳給read。接着咱們建立了一個循環來檢查是否讀取完畢isDone()。這裏的循環操做比較低效,它的意思是咱們須要等待讀取動做完成。
一旦讀取完成後,咱們就能夠把數據寫入ByteBuffer,而後輸出。
另外一種方式是調用接收CompletionHandler做爲參數的read()方法。下面是具體的使用:
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("result = " + result); attachment.flip(); byte[] data = new byte[attachment.limit()]; attachment.get(data); System.out.println(new String(data)); attachment.clear(); } @Override public void failed(Throwable exc, ByteBuffer attachment) { } });
這裏,一旦讀取完成,將會觸發CompletionHandler的completed()方法,並傳入一個Integer和ByteBuffer。前面的整形表示的是讀取到的字節數大小。第二個ByteBuffer也能夠換成其餘合適的對象方便數據寫入。 若是讀取操做失敗了,那麼會觸發failed()方法。
和讀數據相似某些數據也有兩種方式,調動不一樣的的write()方法,下面分別看介紹這兩種方法。
經過AsynchronousFileChannel咱們能夠一步寫數據
Path path = Paths.get("data/test-write.txt"); AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; buffer.put("test data".getBytes()); buffer.flip(); Future<Integer> operation = fileChannel.write(buffer, position); buffer.clear(); while(!operation.isDone()); System.out.println("Write done");
首先把文件已寫方式打開,接着建立一個ByteBuffer座位寫入數據的目的地。再把數據進入ByteBuffer。最後檢查一下是否寫入完成。 須要注意的是,這裏的文件必須是已經存在的,否者在嘗試write數據是會拋出一個java.nio.file.NoSuchFileException.
檢查一個文件是否存在能夠經過下面的方法:
if(!Files.exists(path)){ Files.createFile(path); }
咱們也能夠經過CompletionHandler來寫數據:
Path path = Paths.get("data/test-write.txt"); if(!Files.exists(path)){ Files.createFile(path); } AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; buffer.put("test data".getBytes()); buffer.flip(); fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("bytes written: " + result); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("Write failed"); exc.printStackTrace(); } });
一樣當數據吸入完成後completed()會被調用,若是失敗了那麼failed()會被調用。
原文連接: http://tutorials.jenkov.com/j...
如今咱們已經知道了Java NIO裏面那些非阻塞特性是怎麼工做的,可是要設計一個非阻塞的服務仍舊比較困難。非阻塞IO相對傳統的阻塞IO給開發者帶來了更多的挑戰。在本節非阻塞服務的講解中,咱們一塊兒來討論這些會面臨的主要挑戰,同時也會給出一些潛在的解決方案。
查找關於設計非阻塞服務的相關資料是比較難的,本文提出的解決方案也只能是基於筆者我的的工做經驗,構思。若是你有其餘的解決方案或者是更好的點子,那麼還請不吝賜教。你能夠在文章下方的評論區回覆,或者能夠給我發送郵件,也能夠直接在Twitter上聯繫我。
雖然本文介紹的一些點子是爲Java NIO設計的,可是我相信這些思路一樣適用於其餘編程語言,只要他們也存在和Selector相似結構,概念。就目前個人瞭解來講,這些結構底層OS提供的,因此基本上你能夠運用到其餘編程語言中去。
爲了演示本文探討的一些技術,筆者已經在GitHub上面創建了相應的源碼倉,地址以下:
https://github.com/jjenkov/ja...
非阻塞的IO管道(Non-blocking IO Pipelines)能夠看作是整個非阻塞IO處理過程的鏈條。包括在以非阻塞形式進行的讀與寫操做。下面有一張插圖,簡單的描述了一個基礎的非阻塞的IO管道(Non-blocking IO Pipelines):
咱們的組件(Component)經過Selector檢查當前Channel是否有數據須要寫入。此時component讀入數據,而且根據輸入的數據input對外提供數據輸出output。這個對外的數據輸出output被寫到了另外一個Channel中。
一個非阻塞的IO管道沒必要同時須要讀和寫數據,一般來講有些管道只須要讀數據,而另外一些管道則只需寫數據。
上面的這幅流程圖僅僅展現了一個組件。實際上一個管道可能存在多個component在處理輸入數據。管道的長度取決於管道具體要作的事情。
固然一個非阻塞的IO管道他也能夠同時從多個Channel中讀取數據,例如同時衝多個SocketChannel中讀取數據;
上面的流程圖實際上被簡化了,圖中的Component實際上負責初始化Selector,從Channel中讀取數據,而不是由Channel往Selector壓如數據(push),這是簡化的上圖容易給人帶來的誤解。
非阻塞IO管道和阻塞IO管道之間最大的區別是他們各自如何從Channel(套接字socket或文件file)讀寫數據。
IO管道一般直接從流中(來自於socket活file的流)讀取數據,而後把數據分割爲連續的消息。這個處理與咱們讀取流信息,用tokenizer進行解析很是類似。不一樣的是咱們在這裏會把數據流分割爲更大一些的消息塊。我把這個過程叫作Message Reader.下面是一張說明的插圖:
一個阻塞IO管道的使用能夠和輸入流同樣調用,每次從Channel中讀取一個字節的數據,阻塞自身直到有數據可讀。這個流程就是一個阻塞的Messsage Reader實現。
使用阻塞IO大大簡化了Message Reader的實現成本。阻塞的Message Reader無需關注沒有數據返回的情形,無需關注返回部分數據或者數據解析須要被複用的問題。
類似的,一個阻塞的Message Writer也不須要關注寫入部分數據,和數據複用的問題。
上面提到了阻塞的Message Reader易於實現,可是阻塞也給他帶了不可避免的缺點,必須爲每一個數據數量都分配一個單獨線程。緣由就在於IO接口在讀取數據時在有數據返回前會一直被阻塞住。這直接致使咱們沒法用單線程來處理一個流沒有數據返回時去讀取其餘的流。每當一個線程嘗試去讀取一個流的數據,這個線程就會被阻塞直到有數據真正返回。
若是這樣的IO管道運用到服務器去處理高併發的連接請求,服務器將不得不爲每個到來的連接分配一個單獨的線程。若是併發數不高好比每一時刻只有幾百併發,也行不會有太大問題。一旦服務器的併發數上升到百萬級別,這種設計就缺少伸縮性。每一個線程須要爲堆棧分配320KB(32位JVM)到1024KB(64位JVM)的內存空間。這就是說若是有1,000,000個線程,須要1TB的內存。而這些在還沒開始真正處理接收到的消息前就須要(消息處理中還須要爲對象開拍內存)。
爲了減小線程數,不少服務器都設計了線程池,把全部接收到的請求放到隊列內,每次讀取一條鏈接進行處理。這種設計能夠用下圖表示:
可是這種設計要求緩衝的鏈接進程發送有意義的數據。若是這些鏈接長時間處於非活躍的狀態,那麼大量非活躍的鏈接會阻塞線程池中的全部線程。這會致使服務器的響應速度特別慢甚至無響應。
有些服務器爲了減輕這個問題,採起的操做是適當增長線程池的彈性。例如,當線程池全部線程都處於飽和時,線程池可能會自動擴容,啓動更多的線程來處理事務。這個解決方案會使得服務器維護大量不活躍的連接。可是須要謹記服務器所能開闢的線程數是有限制的。全部當有1,000,000個低速的連接時,服務器仍是不具有伸縮性。
一個非阻塞的IO通道能夠用單線程讀取多個數據流。這個前提是相關的流能夠切換爲非阻塞模式(並非全部流均可以以非阻塞形式操做)。在非阻塞模式下,讀取一個流可能返回0個或多個字節。若是流尚未可供讀取的數據那麼就會返回0,其餘大於1的返回都代表這是實際讀取到的數據;
爲了避開沒有數據可讀的流,咱們結合Java NIO中的Selector。一個Selector能夠註冊多個SelectableChannel實例。當咱們調用select()或selectorNow()方法時Selector會返回一個有數據可讀的SelectableChannel實例。這個設計能夠以下插圖:
當咱們衝SelectableChannel中讀取一段數據後,咱們並不知道這段數據是不是完整的一個message。由於一個數據段可能包含部分message,也就是說便可能少於一個message,也可能多一個message,正以下面這張插圖所示意的那樣:
要處理這種截斷的message,咱們會遇到兩個問題:
檢測完整message要求Message Reader查看數據段中的數據是否至少包含一個完整的message。若是包含一個或多個完整message,這些message能夠被下發到通道中處理。查找完整message的過程是個大量重複的操做,因此這個操做必須是越快越好的。
當數據段中有一個不完整的message時,不管不完整消息是整個數據段仍是說在完整message先後,這個不完整的message數據都須要在剩餘部分得到前存儲起來。
檢查message完整性和存儲不完整message都是Message Reader的職責。爲了不混淆來自不一樣Channel的數據,咱們爲每個Channel分配一個Message Reader。整個設計大概是這樣的:
當咱們經過Selector獲取到一個有數據能夠讀取的Channel以後,改Channel關聯的Message Reader會讀取數據,而且把數據打斷爲Message塊。獲得完整的message後就能夠經過通道下發到其餘組件進行處理。
一個Message Reader天然是協議相關的。他須要知道message的格式以便讀取。若是咱們的服務器是跨協議複用的,那他必須實現Message Reader的協議-大體相似於接收一個Message Reader工廠做爲配置參數。
如今咱們已經明確了由Message Reader負責不完整消息的存儲直到接收到完整的消息。閒雜咱們還須要知道這個存儲過程須要如何來實現。
在設計的時候咱們須要考慮兩個關鍵因素:
顯然不完整的消息數據須要存儲在某種buffer中。比較直接的辦法是咱們爲每一個Message Reader都分配一個內部的buffer成員。可是,多大的buffer才合適呢?這個buffer必須能存儲下一個message最大的大小。若是一個message最大是1MB,那每一個Message Reader內部的buffer就至少有1MB大小。
在百萬級別的併發連接數下,1MB的buffer基本無法正常工做。舉例來講,1,000,000 x 1MB就是1TB的內存大小!若是消息的最大數據量是16MB又須要多少內存呢?128MB呢?
另外一個方案是在每一個Message Reader內部維護一個容量可變的buffer。一個可變的buffer在初始化時佔用較少控件,在消息變得很大超出容量時自動擴容。這樣每一個連接就不須要都佔用好比1MB的空間。每一個連接只使用承載下一個消息所必須的內存大小。
要實現一個可伸縮的buffer有幾種不一樣的辦法。每一種都有它的優缺點,下面幾個小結我會逐一討論它們。
第一種實現可伸縮buffer的辦法是初始化buffer的時候只申請較少的空間,好比4KB。若是消息超出了4KB的大小那麼開賠一個更大的空間,好比8KB,而後把4KB中的數據拷貝紙8KB的內存塊中。
以拷貝方式擴容的優勢是一個消息的所有數據都被保存在了一個連續的字節數組中。這使得數據解析變得更加容易。
同時它的缺點是會增長大量的數據拷貝操做。
爲了減小數據的拷貝操做,你能夠分析整個消息流中的消息大小,一次來找到最適合當前機器的能夠減小拷貝操做的buffer大小。例如,你可能會注意到覺大多數的消息都是小於4KB的,由於他們僅僅包含了一個很是請求和響應。這意味着消息的處所榮校應該設置爲4KB。
同時,你可能會發現若是一個消息大於4KB,極可能是由於他包含了一個文件。你會可能注意到 大多數經過系統的數據都是小於128KB的。因此咱們能夠在第一次擴容設置爲128KB。
最後你可能會發現當一個消息大於128KB後,沒有什麼規律可循來肯定下次分配的空間大小,這意味着最後的buffer容量應該設置爲消息最大的可能數據量。
結合這三次擴容時的大小設置,能夠必定程度上減小數據拷貝。4KB如下的數據無需拷貝。在1百萬的鏈接下須要的空間例如1,000,000x4KB=4GB,目前(2015)大多數服務器都扛得住。4KB到128KB會僅需拷貝一次,即拷貝4KB數據到128KB的裏面。消息大小介於128KB和最大容量的時須要拷貝兩次。首先4KB數據被拷貝第二次是拷貝128KB的數據,因此總共須要拷貝132KB數據。假設沒有不少的消息會超過128KB,那麼這個方案仍是能夠接受的。
當一個消息被完整的處理完畢後,它佔用的內容應立即刻被釋放。這樣下一個來自東一個連接通道的消息能夠從最小的buffer大小從新開始。這個操做是必須的若是咱們須要儘量高效地複用不一樣連接之間的內存。大多數狀況下並非全部的連接都會在同一時刻須要大容量的buffer。
筆者寫了一個完整的教程闡述瞭如何實現一個內存buffer使其支持擴容:Resizable Arrays 。這個教程也附帶了一個指向GitHub上的源碼倉地址,裏面有實現方案的具體代碼。
另外一種實現buffer擴容的方案是讓buffer包含幾個數組。當須要擴容的時候只須要在開闢一個新的字節數組,而後把內容寫到裏面去。
這種擴容也有兩個具體的辦法。一中是開闢單獨的字節數組,而後用一個列表把這些獨立數組關聯起來。另外一種是開闢一些更大的,相互共享的字節數組切片,而後用列表把這些切片和buffer關聯起來。我的而言,筆者認爲第二種切片方案更好一點點,可是它們以前的差別比較小。(譯者話:關於這兩個辦法,我我的以爲概念介紹有點難懂,建議讀者也參考一下原文?)
這種追加擴容的方案無論是用獨立數組仍是切片都有一個優勢,那就是寫數據的時候不須要二外的拷貝操做。全部的數據能夠直接從socket(Channel)中拷貝至數組活切片當中。
這種方案的缺點也很明顯,就是數據不是存儲在一個連續的數組中。這會使得數據的解析變得更加複雜,由於解析器不得不一樣時查找每個獨立數組的結尾和全部數組的結尾。正由於咱們須要在寫數據時查找消息的結尾,這個模型在設計實現時會相對不那麼容易。
有些協議的消息消失採用的是一種TLV格式(Type, Length, Value)。這意味着當消息到達時,消息的完整大小存儲在了消息的開始部分。咱們能夠馬上判斷爲消息開闢多少內存空間。
TLV編碼是的內存管理變得更加簡單。咱們能夠馬上知道爲消息分配多少內存。即使是不完整的消息,buffer結尾後面也不會有浪費的內存。
TLV編碼的一個缺點是咱們須要在消息的所有數據接收到以前就開闢好須要用的全部內存。所以少許連接慢,但發送了大塊數據的連接會佔用較多內存,致使服務器無響應。
解決上訴問題的一個變通辦法是使用一種內部包含多個TLV的消息格式。這樣咱們爲每一個TLV段分配內存而不是爲整個的消息分配,而且只在消息的片斷到達時才分配內存。可是消息片斷很大時,任然會出現同樣的問題。
另外一個辦法是爲消息設置超時,若是長時間未接收到的消息(好比10-15秒)。這可讓服務器從偶發的併發處理大塊消息恢復過來,不過仍是會讓服務器有一段時間無響應。另外惡意的DoS攻擊會致使服務器開闢大量內存。
TLV編碼有不一樣的變種。有多少字節使用這樣確切的類型和字段長度取決於每一個獨立的TLV編碼。有的TLV編碼吧字段長度放在前面,接着放類型,最後放值。儘管字段的順序不一樣,但他任然是一個TLV的類型。
TLV編碼使得內存管理更加簡單,這也是HTTP1.1協議讓人以爲是一個不太優良的的協議的緣由。正因如此,HTTP2.0協議在設計中也利用TLV編碼來傳輸數據幀。也是由於這個緣由咱們設計了本身的利用TLV編碼的網絡協議VStack.co。
在非阻塞IO管道中,寫數據也是一個不小的挑戰。當你調用一個非阻塞模式Channel的write()方法時,沒法保證有多少機字節被寫入了ByteBuffer中。write方法返回了實際寫入的字節數,因此跟蹤記錄已被寫入的字節數也是可行的。這就是咱們遇到的問題:持續記錄被寫入的不完整的小樹知道一個消息中全部的數據都發送完畢。
爲了管理不完整消息的寫操做,咱們須要建立一個Message Writer。正如前面的Message Reader,咱們也須要每一個Channel配備一個Message Writer來寫數據。在每一個Message Writer中咱們記錄準確的已經寫入的字節數。
爲了不多個消息傳遞到Message Writer超出他所能處理到Channel的量,咱們須要讓到達的消息進入隊列。Message Writer則儘量快的將數據寫到Channel裏。
下面是一個流程圖,展現的是不完整消息被寫入的過程:
爲了使Message Writer可以持續發送剛纔已經發送了一部分的消息,Message Writer須要被移植調用,這樣他就能夠發送更多數據。
若是你有大量的連接,你會持有大量的Message Writer實例。檢查好比1百萬的Message Writer實例是來肯定他們是否處於可寫狀態是很慢的操做。首先,許多Message Writer可能根本就沒有數據須要發送。咱們不想檢查這些實例。其次,不是全部的Channel都處於可寫狀態。咱們不想浪費時間在這些非寫入狀態的Channel。
爲了檢查一個Channel是否可寫,能夠把它註冊到Selector上。可是咱們不但願把全部的Channel實例都註冊到Selector。試想一下,若是你有1百萬的連接,這裏面大部分是空閒的,把1百萬連接都祖冊到Selector上。而後調用select方法的時候就會有不少的Channel處於可寫狀態。你須要檢查全部這些連接中的Message Writer以確認是否有數據可寫。
爲了不檢查全部的這些Message Writer,以及那些根本沒有消息須要發送給他們的Channel實例,我麼能夠採用入校兩步策略:
這兩個小步驟確保只有有數據要寫的Channel纔會被註冊到Selector。
正如你所知到的,一個被阻塞的服務器須要時刻檢查當前是否有顯得完整消息抵達。在一個消息被完整的收到前,服務器可能須要檢查屢次。檢查一次是不夠的。
相似的,服務器也須要時刻檢查當前是否有任何可寫的數據。若是有的話,服務器須要檢查相應的連接看他們是否處於可寫狀態。僅僅在消息第一次進入隊列時檢查是不夠的,由於一個消息可能被部分寫入。
總而言之,一個非阻塞的服務器要三個管道,而且常常執行:
這三個管道在循環中重複執行。你能夠嘗試優化它的執行。好比,若是沒有消息在隊列中等候,那麼能夠跳過寫數據管道。或者,若是沒有收到新的完整消息,你甚至能夠跳過處理數據管道。
下面這張流程圖闡述了這整個服務器循環過程:
假如你仍是柑橘這比較複雜難懂,能夠去clone咱們的源碼倉: https://github.com/jjenkov/ja... 也許親眼看到了代碼會幫助你理解這一塊是如何實現的。
咱們在GitHub上的源碼中實現的非阻塞IO服務使用了一個包含兩條線程的線程模型。第一個線程負責從ServerSocketChannel接收到達的連接。另外一個線程負責處理這些連接,包括讀消息,處理消息,把響應寫回到連接。這個雙線程模型以下:
前一節中已經介紹過的服務器的循環處理在處理線程中執行。
服務端:
package cn.zyzpp.nio; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Create by yster@foxmail.com 2018/10/11 17:44 */ public class Server { private Selector selector; private ExecutorService tp = Executors.newCachedThreadPool(); class HandleMsg implements Runnable { ByteBuffer byteBuffer; SelectionKey key; public HandleMsg(ByteBuffer byteBuffer, SelectionKey key) { this.byteBuffer = byteBuffer; this.key = key; } @Override public void run() { byteBuffer.flip(); //byte[] bytes = new byte[byteBuffer.remaining()]; //byteBuffer.get(bytes); //System.out.println(new String(bytes, 0, bytes.length)); System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit())); //將此鍵的 interest 集合設置爲給定值 key.interestOps(SelectionKey.OP_WRITE); //強迫selector返回, 使還沒有返回的第一個選擇操做當即返回, 即取消selector.select()的阻塞 selector.wakeup(); } } private void doAccept(SelectionKey key) { //返回建立此鍵的通道 ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel clientChannel; try { //生成和客戶端的通訊的通道 clientChannel = server.accept(); //設置非阻塞模式 clientChannel.configureBlocking(false); //註冊選擇器, 讀就緒 clientChannel.register(selector, SelectionKey.OP_READ); InetAddress clientAddress = clientChannel.socket().getInetAddress(); System.out.println("鏈接到客戶端, 客戶端ip: " + clientAddress.getHostAddress()); } catch (Exception e) { e.printStackTrace(); } } private void doRead(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); try { int readBytes = channel.read(byteBuffer); if(readBytes > 0) { tp.execute(new HandleMsg(byteBuffer, key)); } } catch (IOException e) { //請求取消此鍵的通道到其選擇器的註冊 key.cancel(); if(key.channel() != null) { key.channel().close(); } e.printStackTrace(); } } private void doWrite(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); try { byteBuffer.put("客戶端,我服務端收到消息了".getBytes()); byteBuffer.flip(); channel.write(byteBuffer); } catch (Exception e) { key.channel(); if(key.channel() != null) { key.channel().close(); } e.printStackTrace(); } //將此鍵的 interest 集合設置爲給定值 key.interestOps(SelectionKey.OP_READ); } private void startServer() throws IOException { selector = SelectorProvider.provider().openSelector(); ServerSocketChannel ssc = ServerSocketChannel.open(); //設置爲非阻塞模式 ssc.configureBlocking(false); InetSocketAddress isa = new InetSocketAddress(8100); ssc.socket().bind(isa); //讓Selector爲這個Channel服務, 接收鏈接繼續事件,表示服務器監聽到了客戶鏈接,服務器能夠接收這個鏈接了 // ServerSocketChannel只有OP_ACCEPT可用,OP_CONNECT,OP_READ,OP_WRITE用於SocketChannel ssc.register(selector, SelectionKey.OP_ACCEPT); while(true) { //阻塞方法 selector.select(); Set<SelectionKey> readyKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = readyKeys.iterator(); while(iterator.hasNext()) { SelectionKey key = (SelectionKey) iterator.next(); //避免重複處理相同的SelectionKey iterator.remove(); //測試此鍵的通道是否已準備好接受新的套接字鏈接(socket鏈接) if(key.isAcceptable()) { doAccept(key); //此鍵是否有效&&此鍵的通道是否已準備好進行讀取 } else if(key.isValid() && key.isReadable()) { doRead(key); //此鍵是否有效&&此鍵的通道是否已準備好進行寫入 } else if(key.isValid() && key.isWritable()) { doWrite(key); } } } } public static void main(String[] args) throws IOException { new Server().startServer(); } }
客戶端:
package cn.zyzpp.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.Iterator; /** * Create by yster@foxmail.com 2018/10/11 16:36 */ public class Client { private Selector selector; public void init(String ip, int port) throws IOException { SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); this.selector = SelectorProvider.provider().openSelector(); channel.connect(new InetSocketAddress(ip, port)); //鏈接就緒,表示客戶與服務器的鏈接已經創建成功 channel.register(selector, SelectionKey.OP_CONNECT); } public void connect(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); try { //若是正在鏈接, 則完成鏈接 if(channel.isConnectionPending()) { //完成套接字通道的鏈接過程 channel.finishConnect(); } channel.configureBlocking(false); channel.write(ByteBuffer.wrap(new String("hello server, I am client!\r\n").getBytes())); //註冊選擇器,讀就緒 channel.register(this.selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); channel.close(); key.selector().close(); } } public void read(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); //建立讀取的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(2048); try { channel.read(buffer); byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("客戶端收到信息: " + msg); } catch (IOException e) { channel.close(); key.selector().close(); } } public void working() throws IOException { while(true) { if(!selector.isOpen()) { break; } selector.select(); Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator(); while(ite.hasNext()) { SelectionKey key = ite.next(); ite.remove(); //鏈接事件發生 if(key.isConnectable()) { connect(key); } else if(key.isReadable()) { read(key); } } } } public static void main(String[] args) throws IOException { Client c = new Client(); c.init("127.0.0.1", 8080); c.working(); } }
服務端打印:
鏈接到客戶端, 客戶端ip: 127.0.0.1 hello server, I am client!
客戶端打印:
客戶端收到信息: 客戶端,我服務端收到消息了
【本文版權歸微信公衆號"代碼藝術"(ID:onblog)全部,如果轉載請務必保留本段原創聲明,違者必究。如果文章有不足之處,歡迎關注微信公衆號私信與我進行交流!】