NIO教程 ——檢視閱讀java
參考linux
BIO,NIO,AIO 總結程序員
Java NIO淺析sql
Java NIO 教程——極客,藍本數據庫
Java NIO 系列教程 ——併發編程網編程
BIO,NIO——知乎數組
NIO 入門——IBM緩存
Java NIO教程 ——易百服務器
Java NIO Tutorial英文版網絡
首先Java中的IO有如下三種: BIO(Blocking IO) 同步式阻塞IO NIO(Non-BlockingIO/New IO) 同步式非阻塞IO JDK1.4提供 AIO(AsynchronousIO) 異步式非阻塞IO JDK1.8提供
略讀:
ibm
NIO 的建立目的是爲了讓 Java 程序員能夠實現高速 I/O 而無需編寫自定義的本機代碼。NIO 將最耗時的 I/O 操做(即填充和提取緩衝區)轉移回操做系統,於是能夠極大地提升速度。
原來的 I/O 庫(在 java.io.*中) 與 NIO 最重要的區別是數據打包和傳輸的方式。正如前面提到的,原來的 I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。
通道和 緩衝區是 NIO 中的核心對象,幾乎在每個 I/O 操做中都要使用它們。
通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的全部數據都必須經過一個 Channel 對象。一個 Buffer 實質上是一個容器對象。發送給一個通道的全部對象都必須首先放到緩衝區中;一樣地,從通道中讀取的任何數據都要讀到緩衝區中。
Buffer 是一個對象, 它包含一些要寫入或者剛讀出的數據。 在 NIO 中加入 Buffer 對象,體現了新庫與原 I/O 的一個重要區別。在面向流的 I/O 中,您將數據直接寫入或者將數據直接讀到 Stream 對象中 。
緩衝區實質上是一個數組。一般它是一個字節數組,可是也能夠使用其餘種類的數組。可是一個緩衝區不 僅僅 是一個數組。緩衝區提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。
Channel是一個對象,能夠經過它讀取和寫入數據。拿 NIO 與原來的 I/O 作個比較,通道就像是流。
通道與流的不一樣之處在於通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而 通道能夠用於讀、寫或者同時用於讀寫。
由於它們是雙向的,因此通道能夠比流更好地反映底層操做系統的真實狀況。特別是在 UNIX 模型中,底層操做系統通道是雙向的。
在 NIO 系統中,任什麼時候候執行一個讀操做,您都是從通道中讀取,可是您不是 直接 從通道讀取。由於全部數據最終都駐留在緩衝區中,因此您是從通道讀到緩衝區中。
所以讀取文件涉及三個步驟:(1) 從 FileInputStream 獲取 Channel,(2) 建立 Buffer,(3) 將數據從 Channel 讀到 Buffer中。
clear() 方法重設緩衝區,使它能夠接受讀入的數據。 flip() 方法讓緩衝區能夠將新讀入的數據寫入另外一個通道。
flip
如今咱們要將數據寫到輸出通道中。在這以前,咱們必須調用 flip() 方法。這個方法作兩件很是重要的事:
clear
最後一步是調用緩衝區的 clear() 方法。這個方法重設緩衝區以便接收更多的字節。 Clear 作兩種很是重要的事情:
read() 和 write() 調用獲得了極大的簡化,由於許多工做細節都由緩衝區完成了。 clear() 和 flip() 方法用於讓緩衝區在讀和寫之間切換。
建立不一樣類型的緩衝區以達到不一樣的目的,如可保護數據不被修改的 只讀 緩衝區,和直接映射到底層操做系統緩衝區的 直接 緩衝區。
使用靜態方法 allocate() 來分配緩衝區:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
將一個現有的數組轉換爲緩衝區,以下所示:
byte array[] = new byte[1024];``ByteBuffer buffer = ByteBuffer.wrap( array );
本例使用了 wrap() 方法將一個數組包裝爲緩衝區。必須很是當心地進行這類操做。一旦完成包裝,底層數據就能夠經過緩衝區或者直接訪問。
建立一個包含槽 3 到槽 6 的子緩衝區。在某種意義上,子緩衝區就像原來的緩衝區中的一個窗口 。
窗口的起始和結束位置經過設置 position 和 limit 值來指定,而後調用 Buffer 的 slice() 方法:
buffer.position( 3 );buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
片是緩衝區的 子緩衝區。不過, 片斷和 緩衝區共享同一個底層數據數組
只讀緩衝區很是簡單 ― 您能夠讀取它們,可是不能向它們寫入。能夠經過調用緩衝區的 asReadOnlyBuffer() 方法,將任何常規緩衝區轉換爲只讀緩衝區,這個方法返回一個與原緩衝區徹底相同的緩衝區(並與其共享數據),只不過它是隻讀的。
只讀緩衝區對於保護數據頗有用。在將緩衝區傳遞給某個對象的方法時,您沒法知道這個方法是否會修改緩衝區中的數據。建立一個只讀的緩衝區能夠 保證 該緩衝區不會被修改。
不能將只讀的緩衝區轉換爲可寫的緩衝區。
//直接緩衝區 ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );
分散/彙集 I/O
通道能夠有選擇地實現兩個新的接口: ScatteringByteChannel 和 GatheringByteChannel。一個 ScatteringByteChannel是一個具備兩個附加讀方法的通道:
這些 long read() 方法很像標準的 read 方法,只不過它們不是取單個緩衝區而是取一個緩衝區數組。緩衝區數組就像一個大緩衝區。
以socket.read()爲例子:
傳統的BIO裏面socket.read(),若是TCP RecvBuffer裏沒有數據,函數會一直阻塞,直到收到數據,返回讀到的數據。
meituan
對於NIO,若是TCP RecvBuffer有數據,就把數據從網卡讀到內存,而且返回給用戶;反之則直接返回0,永遠不會阻塞。
最新的AIO(Async I/O)裏面會更進一步:不但等待就緒是非阻塞的,就連數據從網卡到內存的過程也是異步的。
換句話說,BIO裏用戶最關心「我要讀」,NIO裏用戶最關心」我能夠讀了」,在AIO模型裏用戶更須要關注的是「讀完了」。
NIO一個重要的特色是:socket主要的讀、寫、註冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操做是同步阻塞的(消耗CPU但性能很是高)。
回憶BIO模型,之因此須要多線程,是由於在進行I/O操做的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能」傻等」,即便經過各類估算,算出來操做系統沒有能力進行讀寫,也無法在socket.read()和socket.write()函數中返回,這兩個函數沒法進行有效的中斷。因此除了多開線程另起爐竈,沒有好的辦法利用CPU。
NIO的讀寫函數能夠馬上返回,這就給了咱們不開線程利用CPU的最好機會:若是一個鏈接不能讀寫(socket.read()返回0或者socket.write()返回0),咱們能夠把這件事記下來,記錄的方式一般是在Selector上註冊標記位,而後切換到其它就緒的鏈接(channel)繼續進行讀寫。
NIO的主要事件有幾個:讀就緒、寫就緒、有新鏈接到來。
仔細分析一下咱們須要的線程,其實主要包括如下幾種: 1. 事件分發器,單線程選擇就緒的事件。 2. I/O處理器,包括connect、read、write等,這種純CPU操做,通常開啓CPU核心個線程就能夠。 3. 業務線程,在處理完I/O後,業務通常還會有本身的業務邏輯,有的還會有其餘的阻塞I/O,如DB操做,RPC等。只要有阻塞,就須要單獨的線程。
NIO給咱們帶來了些什麼:
BIO,NIO,AIO 總結
如何區分 「同步/異步 」和 「阻塞/非阻塞」 呢?
同步/異步是從行爲角度描述事物的,而阻塞和非阻塞描述的當前事物的狀態(等待調用結果時的狀態)。
阻塞模式使用就像傳統中的支持同樣,比較簡單,可是性能和可靠性都很差;非阻塞模式正好與之相反。對於低負載、低併發的應用程序,能夠使用同步阻塞I/O來提高開發速率和更好的維護性;對於高負載、高併發的(網絡)應用,應使用 NIO 的非阻塞模式來開發。
Buffer是一個對象,它包含一些要寫入或者要讀出的數據。在NIO類庫中加入Buffer對象,體現了新庫與原I/O的一個重要區別。在面向流的I/O中·能夠將數據直接寫入或者將數據直接讀到 Stream 對象中。雖然 Stream 中也有 Buffer 開頭的擴展類,但只是流的包裝類,仍是從流讀到緩衝區,而 NIO 倒是直接讀到 Buffer 中進行操做。
NIO 經過Channel(通道) 進行讀寫。
通道是雙向的,可讀也可寫,而流的讀寫是單向的。不管讀寫,通道只能和Buffer交互。由於 Buffer,通道能夠異步地讀寫。
NIO有選擇器,而IO沒有。
選擇器用於使用單個線程處理多個通道。所以,它須要較少的線程來處理這些通道。線程之間的切換對於操做系統來講是昂貴的。 所以,爲了提升系統效率選擇器是有用的。
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基於事件和回調機制實現的,也就是應用操做以後會直接返回,不會堵塞在那裏,當後臺處理完成,操做系統會通知相應的線程進行後續的操做。
每當要從緩存區的時候讀取數據時,就調用filp()「切換成讀模式」。
讀完咱們還想寫數據到緩衝區,那就使用clear()函數,這個函數會「清空」緩衝區 。
簡介
NIO中的N能夠理解爲Non-blocking ,不單純是New 。
不一樣點:
概覽
NIO包含下面3個核心的組件,Channel,Buffer和Selector組成了這個核心的API:
一般來講NIO中的全部IO都是從Channel開始的。Channel和流有點相似。經過Channel,咱們便可以從Channel把數據寫到Buffer中,也能夠把數據衝Buffer寫入到Channel 。
有不少的Channel,Buffer類型。下面列舉了主要的幾種:
正如你看到的,這些channel基於於UDP和TCP的網絡IO,以及文件IO。 和這些類一塊兒的還有其餘一些比較有趣的接口,在本節中暫時很少介紹。爲了簡潔起見,咱們會在必要的時候引入這些概念。 下面是核心的Buffer實現類的列表:
這些Buffer涵蓋了能夠經過IO操做的基礎類型:byte,short,int,long,float,double以及characters. NIO實際上還包含一種MappedBytesBuffer,通常用於和內存映射的文件。
選擇器容許單線程操做多個通道。若是你的程序中有大量的連接,同時每一個連接的IO帶寬不高的話,這個特性將會很是有幫助。好比聊天服務器。 下面是一個單線程中Slector維護3個Channel的示意圖:
要使用Selector的話,咱們必須把Channel註冊到Selector上,而後就能夠調用Selector的select()方法。這個方法會進入阻塞,直到有一個channel的狀態符合條件。當方法返回後,線程能夠處理這些事件。
Java NIO Channel通道
Java NIO Channel通道和流很是類似,主要有如下3點區別:
Channel的實現
下面列出Java NIO中最重要的集中Channel的實現:
FileChannel用於文件的數據讀寫。 DatagramChannel用於UDP的數據讀寫。 SocketChannel用於TCP的數據讀寫。 ServerSocketChannel容許咱們監聽TCP連接請求,每一個請求會建立會一個SocketChannel.
RandomAccessFile擴展:
RandomAccessFile(隨機訪問文件)類。該類是Java語言中功能最爲豐富的文件訪問類 。RandomAccessFile類支持「隨機訪問」方式,這裏「隨機」是指能夠跳轉到文件的任意位置處讀寫數據。在訪問一個文件的時候,沒必要把文件從頭讀到尾,而是但願像訪問一個數據庫同樣「爲所欲爲」地訪問一個文件的某個部分,這時使用RandomAccessFile類就是最佳選擇。
四種模式:R RW RWD RWS
r 以只讀的方式打開文本,也就意味着不能用write來操做文件
rw 讀操做和寫操做都是容許的
rws 每當進行寫操做,同步的刷新到磁盤,刷新內容和元數據
rwd 每當進行寫操做,同步的刷新到磁盤,刷新內容
RandomAccessFile的用處:
一、大型文本日誌類文件的快速定位獲取數據:
得益於seek的巧妙設計,我認爲咱們能夠從超大的文本中快速定位咱們的遊標,例如每次存日誌的時候,咱們能夠創建一個索引緩存,索引是日誌的起始日期,value是文本的poiniter 也就是光標,這樣咱們能夠快速定位某一個時間段的文本內容
二、併發讀寫
也是得益於seek的設計,我認爲多線程能夠輪流操做seek控制光標的位置,從未達到不一樣線程的併發寫操做。
三、更方便的獲取二進制文件
經過自帶的讀寫轉碼(readDouble、writeLong等),我認爲能夠快速的完成字節碼到字符的轉換功能,對使用者來講比較友好。
RandomAccessFile參考
實例:
public class FileChannelTest { public static void main(String[] args) throws IOException { RandomAccessFile file = new RandomAccessFile("D:\\text\\1_loan.sql", "r"); //mode只有4中,若是不是讀寫的mode或者給的不是4種中的,就會報錯。 RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r"); try { FileChannel fileChannel = file.getChannel(); FileChannel copyFileChannel = copyFile.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int read = fileChannel.read(byteBuffer); while (read != -1) { System.out.println("read:" + read); //byteBuffer緩衝區切換爲讀模式 byteBuffer.flip(); copyFileChannel.write(byteBuffer); //「清空」byteBuffer緩衝區,以知足後續寫入操做 byteBuffer.clear(); //注意,每次讀時都要返回讀後的狀態read值賦值給循環判斷體read,不然會陷入死循環true read = fileChannel.read(byteBuffer); } } finally { file.close(); copyFile.close(); } } }
報錯:
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "w"); //由於沒有"w"的mode Exception in thread "main" java.lang.IllegalArgumentException: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd" at java.io.RandomAccessFile.<init>(RandomAccessFile.java:221) RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r"); //由於沒有"w"的權限 Exception in thread "main" java.nio.channels.NonWritableChannelException at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:194) at com.niotest.FileChannelTest.main(FileChannelTest.java:33)
NIO Buffer緩衝區
Java NIO Buffers用於和NIO Channel交互。正如你已經知道的,咱們從channel中讀取數據到buffers裏,從buffer把數據寫入到channels.
buffer本質上就是一塊內存區,能夠用來寫入數據,並在稍後讀取出來。這塊內存被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發的接口。
Buffer基本用法
利用Buffer讀寫數據,一般遵循四個步驟:
當寫入數據到buffer中時,buffer會記錄已經寫入的數據大小。當須要讀數據時,經過flip()方法把buffer從寫模式調整爲讀模式;在讀模式下,能夠讀取全部已經寫入的數據。
當讀取完數據後,須要清空buffer,以知足後續寫入操做。清空buffer有兩種方式:調用clear()或compact()方法。clear會清空整個buffer,compact則只清空已讀取的數據,未被讀取的數據會被移動到buffer的開始位置,寫入位置則近跟着未讀數據以後。
Buffer的容量,位置,上限(Buffer Capacity, Position and Limit)
buffer緩衝區實質上就是一塊內存,用於寫入數據,也供後續再次讀取數據。這塊內存被NIO Buffer管理,並提供一系列的方法用於更簡單的操做這塊內存。
一個Buffer有三個屬性是必須掌握的,分別是:
position和limit的具體含義取決於當前buffer的模式。capacity在兩種模式下都表示容量。
下面有張示例圖,描訴了不一樣模式下position和limit的含義:
容量(Capacity)
做爲一塊內存,buffer有一個固定的大小,叫作capacity容量。也就是最多隻能寫入容量值的字節,整形等數據。一旦buffer寫滿了就須要清空已讀數據以便下次繼續寫入新的數據。
位置(Position)
當寫入數據到Buffer的時候須要中一個肯定的位置開始,默認初始化時這個位置position爲0,一旦寫入了數據好比一個字節,整形數據,那麼position的值就會指向數據以後的一個單元,position最大能夠到capacity-1.
當從Buffer讀取數據時,也須要從一個肯定的位置開始。buffer從寫入模式變爲讀取模式時,position會歸零,每次讀取後,position向後移動。
上限(Limit)
在寫模式,limit的含義是咱們所能寫入的最大數據量。它等同於buffer的容量。
一旦切換到讀模式,limit則表明咱們所能讀取的最大數據量,他的值等同於寫模式下position的位置。
數據讀取的上限時buffer中已有的數據,也就是limit的位置(原寫模式下position所指的位置)。
Buffer Types
Java NIO有以下具體的Buffer類型:
正如你看到的,Buffer的類型表明了不一樣數據類型,換句話說,Buffer中的數據能夠是上述的基本類型;
分配一個Buffer(Allocating a Buffer)
爲了獲取一個Buffer對象,你必須先分配。每一個Buffer實現類都有一個allocate()方法用於分配內存。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); CharBuffer charBuffer = CharBuffer.allocate(48);
寫入數據到Buffer(Writing Data to a Buffer)
寫數據到Buffer有兩種方法:
從Channel中寫數據到Buffer
手動寫數據到Buffer,調用put方法
//從Channel中寫數據到Buffer
int read = fileChannel.read(byteBuffer);
//調用put方法寫
buf.put(3);
//把數據寫到特定的位置
public ByteBuffer put(int i, byte x);
//把一個具體類型數據寫入buffer
public ByteBuffer putInt(int x);
flip()——翻轉
flip()方法能夠把Buffer從寫模式切換到讀模式。調用flip方法會把position歸零,並設置limit爲以前的position的值。 也就是說,如今position表明的是讀取位置,limit標示的是已寫入的數據位置。
從Buffer讀取數據(Reading Data from a Buffer)
衝Buffer讀數據也有兩種方式。
從buffer讀數據到channel。
從buffer直接讀取數據,調用get方法。
//讀取數據到channel的例子:
int bytesWritten = inChannel.write(buf);
//調用get讀取數據的例子:
byte aByte = buf.get();
rewind()——倒帶
Buffer.rewind()方法將position置爲0,這樣咱們能夠重複讀取buffer中的數據。limit保持不變。
clear() and compact()
一旦咱們從buffer中讀取完數據,須要複用buffer爲下次寫數據作準備。只須要調用clear或compact方法。
clear方法會重置position爲0,limit爲capacity,也就是整個Buffer清空。實際上Buffer中數據並無清空,咱們只是把標記爲修改了。(從新寫入的時候這些存在的數據就會被新的數據覆蓋)
若是Buffer還有一些數據沒有讀取完,調用clear就會致使這部分數據被「遺忘」,由於咱們沒有標記這部分數據未讀。
針對這種狀況,若是須要保留未讀數據,那麼能夠使用compact()。 所以compact()和clear()的區別就在於對未讀數據的處理,是保留這部分數據仍是一塊兒清空。
mark() and reset()
經過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.
equals() and compareTo()
能夠用eqauls和compareTo比較兩個buffer
equals()
判斷兩個buffer相對,需知足:
從上面的三個條件能夠看出,equals只比較buffer中的部份內容,並不會去比較每個元素。
compareTo()
compareTo也是比較buffer中的剩餘元素,只不過這個方法適用於比較排序的:
NIO Scatter (分散)/ Gather(彙集)
——分散讀和彙集寫的場景。
Java NIO發佈時內置了對scatter / gather的支持。scatter / gather是經過通道讀寫數據的兩個概念。
Scattering read指的是從通道讀取的操做能把數據寫入多個buffer,也就是scatters表明了數據從一個channel到多個buffer的過程。
gathering write則正好相反,表示的是從多個buffer把數據寫入到一個channel中。
Scatter/gather在有些場景下會很是有用,好比須要處理多份分開傳輸的數據。舉例來講,假設一個消息包含了header和body,咱們可能會把header和body保存在不一樣獨立buffer中,這種分開處理header與body的作法會使開發更簡明。
Scattering Reads
"scattering read"是把數據從單個Channel寫入到多個buffer,下面是示意圖:
觀察代碼能夠發現,咱們把多個buffer寫在了一個數組中,而後把數組傳遞給channel.read()方法。read()方法內部會負責把數據按順序寫進傳入的buffer數組內。一個buffer寫滿後,接着寫到下一個buffer中。
實際上,scattering read內部必須寫滿一個buffer後纔會向後移動到下一個buffer,所以這並不適合消息大小會動態改變的部分,也就是說,若是你有一個header和body,而且header有一個固定的大小(好比128字節),這種情形下能夠正常工做。
athering Writes
"gathering write"把多個buffer的數據寫入到同一個channel中.
似的傳入一個buffer數組給write,內部機會按順序將數組內的內容寫進channel,這裏須要注意,寫入的時候針對的是buffer中position到limit之間的數據。也就是若是buffer的容量是128字節,但它只包含了58字節數據,那麼寫入的時候只有58字節會真正寫入。所以gathering write是能夠適用於可變大小的message的,這和scattering reads不一樣。
NIO Channel to Channel Transfers通道傳輸接口
在Java NIO中若是一個channel是FileChannel類型的,那麼他能夠直接把數據傳輸到另外一個channel。這個特性得益於FileChannel包含的transferTo和transferFrom兩個方法。
transferFrom()——目標channel用,參數爲源數據channel。
transferFrom的參數position和count表示目標文件的寫入位置和最多寫入的數據量。若是通道源的數據小於count那麼就傳實際有的數據量。 另外,有些SocketChannel的實如今傳輸時只會傳輸哪些處於就緒狀態的數據,即便SocketChannel後續會有更多可用數據。所以,這個傳輸過程可能不會傳輸整個的數據。
transferTo()——源數據用,參數爲目標channel
SocketChannel的問題也存在於transferTo.SocketChannel的實現可能只在發送的buffer填充滿後才發送,並結束。
實例:
public class ChannelTransferTest { public static void main(String[] args) throws IOException { RandomAccessFile fromfile = new RandomAccessFile("D:\\text\\1_loan.sql", "rw"); //mode只有4中,若是不是讀寫的mode或者給的不是4種中的,就會報錯。 RandomAccessFile toFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "rw"); FileChannel fromfileChannel = fromfile.getChannel(); FileChannel toFileChannel = toFile.getChannel(); //==========================transferTo================================= //transferTo方法把fromfileChannel數據傳輸到另外一個toFileChannel //long transferSize = fromfileChannel.transferTo(0, fromfileChannel.size(), toFileChannel); //System.out.println(transferSize); //=============================transferFrom============================== //把數據從通道源傳輸到toFileChannel,相比經過buffer讀寫更加的便捷 long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()); //參數position和count表示目標文件的寫入位置和最多寫入的數據量 //long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()-1000); //若是通道源的數據小於count那麼就傳實際有的數據量。 //long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()+1000); System.out.println(transferSize1); } }
NIO Selector選擇器
Selector是Java NIO中的一個組件,用於檢查一個或多個NIO Channel的狀態是否處於可讀、可寫。如此能夠實現單線程管理多個channels,也就是能夠管理多個網絡連接。
爲何使用Selector
用單線程處理多個channels的好處是我須要更少的線程來處理channel。實際上,你甚至能夠用一個線程來處理全部的channels。從操做系統的角度來看,切換線程開銷是比較昂貴的,而且每一個線程都須要佔用系統資源,所以暫用線程越少越好。
須要留意的是,現代操做系統和CPU在多任務處理上已經變得愈來愈好,因此多線程帶來的影響也愈來愈小。若是一個CPU是多核的,若是不執行多任務反而是浪費了機器的性能。不過這些設計討論是另外的話題了。簡而言之,經過Selector咱們能夠實現單線程操做多個channel。
建立Selector
建立一個Selector能夠經過Selector.open()方法:
Selector selector = Selector.open();
註冊Channel到Selector上
先把Channel註冊到Selector上,這個操做使用SelectableChannel的register()。SocketChannel等都有繼承此抽象類。
channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必須是非阻塞的。因此FileChannel不適用Selector,由於FileChannel不能切換爲非阻塞模式。Socket channel能夠正常使用。
注意register的第二個參數,這個參數是一個「關注集合」,表明咱們關注的channel狀態,有四種基礎類型可供監聽:
一個channel觸發了一個事件也可視做該事件處於就緒狀態。所以當channel與server鏈接成功後,那麼就是「鏈接就緒」狀態。server channel接收請求鏈接時處於「可鏈接就緒」狀態。channel有數據可讀時處於「讀就緒」狀態。channel能夠進行數據寫入時處於「寫就緒」狀態。
上述的四種就緒狀態用SelectionKey中的常量表示以下:
若是對多個事件感興趣可利用位的或運算結合多個常量,好比:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
在上一小節中,咱們利用register方法把Channel註冊到了Selectors上,這個方法的返回值是SelectionKeys,這個返回的對象包含了一些比較有價值的屬性:
Interest Set
這個「關注集合」實際上就是咱們但願處理的事件的集合,它的值就是註冊時傳入的參數,咱們能夠用按爲與運算把每一個事件取出來:
int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
Ready Set
"就緒集合"中的值是當前channel處於就緒的值,通常來講在調用了select方法後都會須要用到就緒狀態
int readySet = selectionKey.readyOps();
從「就緒集合」中取值的操做相似於「關注集合」的操做,固然還有更簡單的方法,SelectionKey提供了一系列返回值爲boolean的的方法:
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
Channel + Selector
從SelectionKey操做Channel和Selector很是簡單:
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
Attaching Objects
咱們能夠給一個SelectionKey附加一個Object,這樣作一方面能夠方便咱們識別某個特定的channel,同時也增長了channel相關的附加信息。例如,能夠把用於channel的buffer附加到SelectionKey上:
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();
附加對象的操做也能夠在register的時候就執行:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
從Selector中選擇channel
一旦咱們向Selector註冊了一個或多個channel後,就能夠調用select來獲取channel。select方法會返回全部處於就緒狀態的channel。 select方法具體以下:
select()方法在返回channel以前處於阻塞狀態。 select(long timeout)和select作的事同樣,不過他的阻塞有一個超時限制。
selectNow()不會阻塞,根據當前狀態馬上返回合適的channel。
select()方法的返回值是一個int整形,表明有多少channel處於就緒了。也就是自上一次select後有多少channel進入就緒。舉例來講,假設第一次調用select時正好有一個channel就緒,那麼返回值是1,而且對這個channel作任何處理,接着再次調用select,此時剛好又有一個新的channel就緒,那麼返回值仍是1,如今咱們一共有兩個channel處於就緒,可是在每次調用select時只有一個channel是就緒的。
selectedKeys()
在調用select並返回了有channel就緒以後,能夠經過選中的key集合來獲取channel,這個操做經過調用selectedKeys()方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
還記得在register時的操做吧,咱們register後的返回值就是SelectionKey實例,也就是咱們如今經過selectedKeys()方法所返回的SelectionKey。
遍歷這些SelectionKey能夠經過以下方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> 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(); }
上述循環會迭代key集合,針對每一個key咱們單獨判斷他是處於何種就緒狀態。
注意:keyIterater.remove()方法的調用,Selector自己並不會移除SelectionKey對象,這個操做須要咱們手動執行。當下次channel處於就緒是,Selector任然會吧這些key再次加入進來。
SelectionKey.channel返回的channel實例須要強轉爲咱們實際使用的具體的channel類型,例如ServerSocketChannel或SocketChannel.
wakeUp()
因爲調用select而被阻塞的線程,能夠經過調用Selector.wakeup()來喚醒即使此時已然沒有channel處於就緒狀態。具體操做是,在另一個線程調用wakeup,被阻塞與select方法的線程就會馬上返回。
close()
當操做Selector完畢後,須要調用close方法。close的調用會關閉Selector並使相關的SelectionKey都無效。channel自己不會被關閉。
示例:首先打開一個Selector,而後註冊channel,最後監聽Selector的狀態。
public class NIOServer { public static void main(String[] args) throws IOException { // 1.獲取通道 ServerSocketChannel server = ServerSocketChannel.open(); // 2.切換成非阻塞模式 server.configureBlocking(false); // 3. 綁定鏈接 server.bind(new InetSocketAddress(6666)); // 4. 獲取選擇器 Selector selector = Selector.open(); // 4.1將通道註冊到選擇器上,指定接收「監聽通道」事件 server.register(selector, SelectionKey.OP_ACCEPT); // 5. 輪訓地獲取選擇器上已「就緒」的事件--->只要select()>0,說明已就緒 while (selector.select() > 0) { // 6. 獲取當前選擇器全部註冊的「選擇鍵」(已就緒的監聽事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // 7. 獲取已「就緒」的事件,(不一樣的事件作不一樣的事) while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 接收事件就緒 if (selectionKey.isAcceptable()) { // 8. 獲取客戶端的連接 SocketChannel client = server.accept(); // 8.1 切換成非阻塞狀態 client.configureBlocking(false); // 8.2 註冊到選擇器上-->拿到客戶端的鏈接爲了讀取通道的數據(監聽讀就緒事件) client.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { // 讀事件就緒 // 9. 獲取當前選擇器讀就緒狀態的通道 SocketChannel client = (SocketChannel) selectionKey.channel(); // 9.1讀取數據 ByteBuffer buffer = ByteBuffer.allocate(1024); // 9.2獲得文件通道,將客戶端傳遞過來的圖片寫到本地項目下(寫模式、沒有則建立) FileChannel outChannel = FileChannel.open(Paths.get("2_loan.sql"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); while (client.read(buffer) > 0) { // 在讀以前都要切換成讀模式 buffer.flip(); outChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("yeah,i know,i got your message!".getBytes()); byteBuffer.flip(); client.write(byteBuffer); } // 10. 取消選擇鍵(已經處理過的事件,就應該取消掉了) iterator.remove(); } } } } public class NIOClientTwo { public static void main(String[] args) throws IOException { // 1. 獲取通道 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666)); // 1.1切換成非阻塞模式 socketChannel.configureBlocking(false); // 1.2獲取選擇器 Selector selector = Selector.open(); // 1.3將通道註冊到選擇器中,獲取服務端返回的數據 socketChannel.register(selector, SelectionKey.OP_READ); // 2. 發送一張圖片給服務端吧 FileChannel fileChannel = FileChannel.open(Paths.get("D:\\text\\1_loan.sql"), StandardOpenOption.READ); // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數據打交道的呢 ByteBuffer buffer = ByteBuffer.allocate(1024); // 4.讀取本地文件(圖片),發送到服務器 while (fileChannel.read(buffer) != -1) { // 在讀以前都要切換成讀模式 buffer.flip(); socketChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } // 5. 輪訓地獲取選擇器上已「就緒」的事件--->只要select()>0,說明已就緒 while (selector.select() > 0) { // 6. 獲取當前選擇器全部註冊的「選擇鍵」(已就緒的監聽事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // 7. 獲取已「就緒」的事件,(不一樣的事件作不一樣的事) while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 8. 讀事件就緒 if (selectionKey.isReadable()) { // 8.1獲得對應的通道 SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer responseBuffer = ByteBuffer.allocate(1024); // 9. 知道服務端要返回響應的數據給客戶端,客戶端在這裏接收 int readBytes = channel.read(responseBuffer); if (readBytes > 0) { // 切換讀模式 responseBuffer.flip(); System.out.println(new String(responseBuffer.array(), 0, readBytes)); } } // 10. 取消選擇鍵(已經處理過的事件,就應該取消掉了) iterator.remove(); } } } }
NIO FileChannel文件通道
Java NIO中的FileChannel是用於鏈接文件的通道。經過文件通道能夠讀、寫文件的數據。Java NIO的FileChannel是相對標準Java IO API的可選接口。
FileChannel不能夠設置爲非阻塞模式,他只能在阻塞模式下運行。
打開文件通道
在使用FileChannel前必須打開通道,打開一個文件通道須要經過輸入/輸出流或者RandomAccessFile,下面是經過RandomAccessFile打開文件通道的案例:
RandomAccessFile aFile = new RandomAccessFile("D:\text\1_loan.sql", "rw"); FileChannel inChannel = aFile.getChannel();
從文件通道內讀取數據
讀取文件通道的數據能夠經過read方法:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
首先開闢一個Buffer,從通道中讀取的數據會寫入Buffer內。接着就能夠調用read方法,read的返回值表明有多少字節被寫入了Buffer,返回-1則表示已經讀取到文件結尾了。
向文件通道寫入數據
寫數據用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); }
注意這裏的write調用寫在了wihle循環彙總,這是由於write不能保證有多少數據真實被寫入,所以須要循環寫入直到沒有更多數據。
關閉通道
操做完畢後,須要把通道關閉:
channel.close();
FileChannel Position
當操做FileChannel的時候讀和寫都是基於特定起始位置的(position),獲取當前的位置能夠用FileChannel的position()方法,設置當前位置能夠用帶參數的position(long pos)方法。
//獲取當前的位置 long position = fileChannel.position(); //設置當前位置爲pos +123 fileChannel.position(pos +123);
假設咱們把當前位置設置爲文件結尾以後,那麼當咱們視圖從通道中讀取數據時就會發現返回值是-1,表示已經到達文件結尾了。 若是把當前位置設置爲文件結尾以後,再向通道中寫入數據,文件會自動擴展以便寫入數據,可是這樣會致使文件中出現相似空洞,即文件的一些位置是沒有數據的。
FileChannel Size
size()方法能夠返回FileChannel對應的文件的文件大小:
long fileSize = channel.size();
FileChannel Truncate
利用truncate方法能夠截取指定長度的文件:
FileChannel truncateFile = fileChannel.truncate(1024);
FileChannel Force
force方法會把全部未寫磁盤的數據都強制寫入磁盤。這是由於在操做系統中出於性能考慮回把數據放入緩衝區,因此不能保證數據在調用write寫入文件通道後就及時寫到磁盤上了,除非手動調用force方法。 force方法須要一個布爾參數,表明是否把meta data也一併強制寫入。
channel.force(true);
NIO SocketChannel套接字通道
在Java NIO體系中,SocketChannel是用於TCP網絡鏈接的套接字接口,至關於Java網絡編程中的Socket套接字接口。建立SocketChannel主要有兩種方式,以下:
創建一個SocketChannel鏈接
打開一個SocketChannel能夠這樣操做:
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
關閉一個SocketChannel鏈接
關閉一個SocketChannel只須要調用他的close方法,以下:
socketChannel.close();
從SocketChannel中讀數據
從一個SocketChannel鏈接中讀取數據,能夠經過read()方法,以下:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = socketChannel.read(buf);
首先須要開闢一個Buffer。從SocketChannel中讀取的數據將放到Buffer中。
接下來就是調用SocketChannel的read()方法.這個read()會把通道中的數據讀到Buffer中。read()方法的返回值是一個int數據,表明這次有多少字節的數據被寫入了Buffer中。若是返回的是-1,那麼意味着通道內的數據已經讀取完畢,到底了(連接關閉)。
向SocketChannel寫數據
向SocketChannel中寫入數據是經過write()方法,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設置爲non-blocking(非阻塞)模式。這樣的話在調用connect(), read(), write()時都是異步的。
socketChannel.configureBlocking(false);
connect()
若是咱們設置了一個SocketChannel是非阻塞的,那麼調用connect()後,方法會在連接創建前就直接返回。爲了檢查當前連接是否創建成功,咱們能夠調用finishConnect(),以下:
socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("http://www.google.com", 80)); while(! socketChannel.finishConnect() ){ //wait, or do something else... }
write()
在非阻塞模式下,調用write()方法不能確保方法返回後寫入操做必定獲得了執行。所以咱們須要把write()調用放到循環內。這和前面在講write()時是同樣的,此處就不在代碼演示。
read()
在非阻塞模式下,調用read()方法也不能確保方法返回後,確實讀到了數據。所以咱們須要本身檢查的整型返回值,這個返回值會告訴咱們實際讀取了多少字節的數據。
Selector結合非阻塞模式
SocketChannel的非阻塞模式能夠和Selector很好的協同工做。把一個或多個SocketChannel註冊到一個Selector後,咱們能夠經過Selector指導哪些channels通道是處於可讀,可寫等等狀態的。
NIO ServerSocketChannel服務端套接字通道
在Java NIO中,ServerSocketChannel是用於監聽TCP連接請求的通道,正如Java網絡編程中的ServerSocket同樣。
ServerSocketChannel實現類位於java.nio.channels包下面。
void test() throws IOException { //打開一個ServerSocketChannel咱們須要調用他的open()方法 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9999)); while(true) { SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... if (socketChannel.isConnected()) { break; } } //關閉一個ServerSocketChannel咱們須要調用close()方法 serverSocketChannel.close(); }
監聽連接
經過調用accept()方法,咱們就開始監聽端口上的請求鏈接。當accept()返回時,他會返回一個SocketChannel鏈接實例,實際上accept()是阻塞操做,他會阻塞帶去線程知道返回一個鏈接; 不少時候咱們是不知足於監聽一個鏈接的,所以咱們會把accept()的調用放到循環中,就像這樣:
while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); //do something with socketChannel... }
固然咱們能夠在循環體內加上合適的中斷邏輯,而不是單純的在while循環中寫true,以此來結束循環監聽;
非阻塞模式
實際上ServerSocketChannel是能夠設置爲非阻塞模式的。在非阻塞模式下,調用accept()函數會馬上返回,若是當前沒有請求的連接,那麼返回值爲空null。所以咱們須要手動檢查返回的SocketChannel是否爲空,例如:
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... } }
Non-blocking Server非阻塞服務器
非阻塞服務器代碼
非阻塞IO通道(Non-blocking IO Pipelines)
非阻塞的IO管道(Non-blocking IO Pipelines)能夠看作是整個非阻塞IO處理過程的鏈條。包括在以非阻塞形式進行的讀與寫操做。
一個非阻塞的IO管道沒必要同時須要讀和寫數據,一般來講有些管道只須要讀數據,而另外一些管道則只需寫數據。
固然一個非阻塞的IO管道他也能夠同時從多個Channel中讀取數據,例如同時從多個SocketChannel中讀取數據;
非阻塞和阻塞通道比較(Non-blocking vs. Blocking IO Pipelines)
非阻塞IO管道和阻塞IO管道之間最大的區別是他們各自如何從Channel(套接字socket或文件file)讀寫數據。
阻塞IO通道的缺點(Blocking IO Pipeline Drawbacks)
上面提到了阻塞的Message Reader易於實現,可是阻塞也給他帶了不可避免的缺點,必須爲每一個數據數量都分配一個單獨線程。緣由就在於IO接口在讀取數據時在有數據返回前會一直被阻塞住。這直接致使咱們沒法用單線程來處理一個流沒有數據返回時去讀取其餘的流。每當一個線程嘗試去讀取一個流的數據,這個線程就會被阻塞直到有數據真正返回。
若是這樣的IO管道運用到服務器去處理高併發的連接請求,服務器將不得不爲每個到來的連接分配一個單獨的線程。若是併發數不高好比每一時刻只有幾百併發,也行不會有太大問題。一旦服務器的併發數上升到百萬級別,這種設計就缺少伸縮性。每一個線程須要爲堆棧分配320KB(32位JVM)到1024KB(64位JVM)的內存空間。這就是說若是有1,000,000個線程,須要1TB的內存。而這些在還沒開始真正處理接收到的消息前就須要(消息處理中還須要爲對象開闢內存)。
爲了減小線程數,不少服務器都設計了線程池,把全部接收到的請求放到隊列內,每次讀取一條鏈接進行處理。這種設計能夠用下圖表示:
可是這種設計要求緩衝的鏈接進程發送有意義的數據。若是這些鏈接長時間處於非活躍的狀態,那麼大量非活躍的鏈接會阻塞線程池中的全部線程。這會致使服務器的響應速度特別慢甚至無響應。
有些服務器爲了減輕這個問題,採起的操做是適當增長線程池的彈性。例如,當線程池全部線程都處於飽和時,線程池可能會自動擴容,啓動更多的線程來處理事務。這個解決方案會使得服務器維護大量不活躍的連接。可是須要謹記服務器所能開闢的線程數是有限制的。全部當有1,000,000個低速的連接時(大量非活躍連接時),服務器仍是不具有伸縮性。
基礎的非阻塞通道設計(Basic Non-blocking IO Pipeline Design)
一個非阻塞的IO通道能夠用單線程讀取多個數據流。這個前提是相關的流能夠切換爲非阻塞模式(並非全部流均可以以非阻塞形式操做,FileChannel就不能切換非阻塞模式)。在非阻塞模式下,讀取一個流可能返回0個或多個字節。若是流尚未可供讀取的數據那麼就會返回0,其餘大於1的返回都代表這是實際讀取到的數據;
爲了避開沒有數據可讀的流,咱們結合Java NIO中的Selector。一個Selector能夠註冊多個SelectableChannel實例。當咱們調用select()或selectorNow()方法時Selector會返回一個有數據可讀的SelectableChannel實例。這個設計能夠以下插圖:
讀取部分信息(Reading Partial Messages)
當咱們衝SelectableChannel中讀取一段數據後,咱們並不知道這段數據是不是完整的一個message。由於一個數據段可能包含部分message,也就是說便可能少於一個message,也可能多一個message(0到多個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(Storing Partial Messages)
如今咱們已經明確了由Message Reader負責不完整消息的存儲直到接收到完整的消息。如今咱們還須要知道這個存儲過程須要如何來實現。
在設計的時候咱們須要考慮兩個關鍵因素:
爲每一個Message Reade分配Buffer(A Buffer Per Message Reader)
固定大小buffer
顯然不完整的消息數據須要存儲在某種buffer中。比較直接的辦法是咱們爲每一個Message Reader都分配一個內部的buffer成員。可是,多大的buffer才合適呢?這個buffer必須能存儲下一個message最大的大小。若是一個message最大是1MB,那每一個Message Reader內部的buffer就至少有1MB大小。
在百萬級別的併發連接數下,1MB的buffer基本無法正常工做。舉例來講,1,000,000 x 1MB就是1TB的內存大小!若是消息的最大數據量是16MB又須要多少內存呢?128MB呢?
缺點:這種直接分配一個message最大的大小值的buffer是很是浪費空間的。
可伸縮Buffer(Resizable Buffers)
另外一個方案是在每一個Message Reader內部維護一個容量可變的buffer。一個可變的buffer在初始化時佔用較少空間,在消息變得很大超出容量時自動擴容。這樣每一個連接就不須要都佔用好比1MB的空間。每一個連接只使用承載下一個消息所必須的內存大小。
容量可變的buffer優勢就是高效利用內存空間,不會浪費內存。
要實現一個可伸縮的buffer有幾種不一樣的辦法。每一種都有它的優缺點,下面幾個小結我會逐一討論它們。
拷貝擴容(Resize by Copy)
第一種實現可伸縮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上的源碼倉地址,裏面有實現方案的具體代碼。
追加擴容(Resize by Append)
另外一種實現buffer擴容的方案是讓buffer包含幾個數組。當須要擴容的時候只須要在開闢一個新的字節數組,而後把內容寫到裏面去。
這種擴容也有兩個具體的辦法。一種是開闢單獨的字節數組,而後用一個列表把這些獨立數組關聯起來。另外一種是開闢一些更大的,相互共享的字節數組切片,而後用列表把這些切片和buffer關聯起來。我的而言,筆者認爲第二種切片方案更好一點點,可是它們以前的差別比較小
這種追加擴容的方案無論是用獨立數組仍是切片都有一個優勢,那就是寫數據的時候不須要額外的拷貝操做。全部的數據能夠直接從socket(Channel)中拷貝至數組活切片當中。
這種方案的缺點也很明顯,就是數據不是存儲在一個連續的數組中。這會使得數據的解析變得更加複雜,由於解析器不得不一樣時查找每個獨立數組的結尾和全部數組的結尾。正由於咱們須要在寫數據時查找消息的結尾,這個模型在設計實現時會相對不那麼容易。
TLV編碼消息(TLV Encoded Messages)
有些協議的消息消失採用的是一種TLV格式(Type, Length, Value)。這意味着當消息到達時,消息的完整大小存儲在了消息的開始部分。咱們能夠馬上判斷爲消息開闢多少內存空間。
優勢:TLV編碼是的內存管理變得更加簡單。咱們能夠馬上知道爲消息分配多少內存。即使是不完整的消息,buffer結尾後面也不會有浪費的內存。
缺點:咱們須要在消息的所有數據接收到以前就開闢好須要用的全部內存。所以少許連接慢,但發送了大塊數據的連接會佔用較多內存,致使服務器無響應。
解決上訴問題的一個變通辦法是使用一種內部包含多個TLV的消息格式。這樣咱們爲每一個TLV段分配內存而不是爲整個的消息分配,而且只在消息的片斷到達時才分配內存。可是消息片斷很大時,任然會出現同樣的問題。
另外一個辦法是爲消息設置超時,若是長時間未接收到的消息(好比10-15秒)。這可讓服務器從偶發的併發處理大塊消息恢復過來,不過仍是會讓服務器有一段時間無響應。另外惡意的DoS攻擊會致使服務器開闢大量內存。
TLV編碼使得內存管理更加簡單,這也是HTTP1.1協議讓人以爲是一個不太優良的的協議的緣由。正因如此,HTTP2.0協議在設計中也利用TLV編碼來傳輸數據幀。也是由於這個緣由咱們設計了本身的利用TLV編碼的網絡協議VStack.co。
寫不完整的消息(Writing Partial Messages)
在非阻塞IO管道中,寫數據也是一個不小的挑戰。當你調用一個非阻塞模式Channel的write()方法時,沒法保證有多少機字節被寫入了ByteBuffer中。write方法返回了實際寫入的字節數,因此跟蹤記錄已被寫入的字節數也是可行的。這就是咱們遇到的問題:持續記錄被寫入的不完整的消息直到一個消息中全部的數據都發送完畢。
爲了不多個消息傳遞到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。
集成(Putting it All Together)
正如你所知到的,一個被阻塞的服務器須要時刻檢查當前是否有新的完整消息抵達。在一個消息被完整的收到前,服務器可能須要檢查屢次。檢查一次是不夠的。
相似的,服務器也須要時刻檢查當前是否有任何可寫的數據。若是有的話,服務器須要檢查相應的連接看他們是否處於可寫狀態。僅僅在消息第一次進入隊列時檢查是不夠的,由於一個消息可能被部分寫入。
總而言之,一個非阻塞的服務器要三個管道,而且常常執行:
這三個管道在循環中重複執行。你能夠嘗試優化它的執行。好比,若是沒有消息在隊列中等候,那麼能夠跳過寫數據管道。或者,若是沒有收到新的完整消息,你甚至能夠跳過處理數據管道。
下面這張流程圖闡述了這整個服務器循環過程:
服務器線程模型(Server Thread Model)
咱們在GitHub上的源碼中實現的非阻塞IO服務使用了一個包含兩條線程的線程模型。第一個線程負責從ServerSocketChannel接收到達的連接。另外一個線程負責處理這些連接,包括讀消息,處理消息,把響應寫回到連接。這個雙線程模型以下:
NIO DatagramChannel數據報通道
一個Java NIO DatagramChannel是一個能夠發送、接收UDP數據包的通道。因爲UDP是面向無鏈接的網絡協議,咱們不可用像使用其餘通道同樣直接進行讀寫數據。正確的作法是發送、接收數據包。
打開一個DatagramChannel(Opening a DatagramChannel)
打開一個DatagramChannel你這麼操做:
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999));
上述示例中,咱們打開了一個DatagramChannel,它能夠在9999端口上收發UDP數據包。
接收數據(Receiving Data)
接收數據,直接調用DatagramChannel的receive()方法:
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf);
receive()方法會把接收到的數據包中的數據拷貝至給定的Buffer中。若是數據包的內容超過了Buffer的大小,剩餘的數據會被直接丟棄。
發送數據(Sending Data)
發送數據是經過DatagramChannel的send()方法:
String newData = "New String to wrte to file..." +System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); int byteSent = channel.send(buf, new InetSocketAddress("java.com", 80));
上述示例會把一個字符串發送到「java.com」服務器的UDP端口80.目前這個端口沒有被任何程序監聽,因此什麼都不會發生。當發送了數據後,咱們不會收到數據包是否被接收的的通知,這是因爲UDP自己不保證任何數據的發送問題。
連接特定機器地址(Connecting to a Specific Address)
DatagramChannel其實是能夠指定到網絡中的特定地址的。因爲UDP是面向無鏈接的,這種連接方式並不會建立實際的鏈接,這和TCP通道相似。確切的說,他會鎖定DatagramChannel,這樣咱們就只能經過特定的地址來收發數據包。
看一個例子先:
channel.connect(new InetSocketAddress("jenkov.com"), 80));
當鏈接上後,能夠向使用傳統的通道那樣調用read()和Writer()方法。區別是數據的讀寫狀況得不到保證。下面是幾個示例:
int bytesRead = channel.read(buf); int bytesWritten = channel.write(buf);
實例:
public class DataGramChannelClient { public static void main(String[] args) throws IOException { //open a datagramChannel DatagramChannel datagramChannel = DatagramChannel.open(); try { //set non-blocking style datagramChannel.configureBlocking(false); //create a byteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //get test data from console Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String next = scanner.next(); byteBuffer.put(next.getBytes()); byteBuffer.flip(); //Sending Data datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 9999)); byteBuffer.clear(); } } finally { datagramChannel.close(); } } } public class DataGramChannelServer { public static void main(String[] args) throws IOException { //打開了一個DatagramChannel,它能夠在9999端口上收發UDP數據包。 DatagramChannel datagramChannel = DatagramChannel.open(); datagramChannel.configureBlocking(false); datagramChannel.bind(new InetSocketAddress(9999)); Selector selector = Selector.open(); //注意要把數據報通道註冊到selector上,不然不能檢測到請求 datagramChannel.register(selector, SelectionKey.OP_READ); while (selector.select() > 0) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); if (selectionKey.isAcceptable()) { System.out.println("ready Acceptable"); } else if (selectionKey.isReadable()) { System.out.println("ready Readable"); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); datagramChannel.receive(byteBuffer); byteBuffer.flip(); //System.out.println(new String(byteBuffer.array()));//this(bytes, 0, bytes.length); byteBuffer不必定是讀滿的,全部用下面的limit System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit())); byteBuffer.clear(); } } keyIterator.remove(); } } }
NIO Pipe管道
一個Java NIO的管道是兩個線程間單向傳輸數據的鏈接。一個管道(Pipe)有一個source channel和一個sink channel(沒想到合適的中文名)。咱們把數據寫到sink channel中,這些數據能夠同過source channel再讀取出來。
下面是一個管道的示意圖:
建立管道(Creating a Pipe)
打開一個管道經過調用Pipe.open()工廠方法,以下:
Pipe pipe = Pipe.open();
向管道寫入數據(Writing to a Pipe)
向管道寫入數據須要訪問他的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); }
從管道讀取數據(Reading from a Pipe)
相似的從管道中讀取數據須要訪問他的source channel:
Pipe.SourceChannel sourceChannel = pipe.source();
接下來調用read()方法讀取數據:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
注意這裏read()的整形返回值表明實際讀取到的字節數。
示例:
public class PipeTest { public static void main(String[] args) throws IOException { Pipe pipe = Pipe.open(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //經過緩衝區向管道寫入數據 Pipe.SinkChannel sinkChannel = pipe.sink(); byteBuffer.put("i am pipe".getBytes()); byteBuffer.flip(); sinkChannel.write(byteBuffer); //經過緩衝區從管道讀數據 //先要重置緩衝區 byteBuffer.clear(); Pipe.SourceChannel sourceChannel = pipe.source(); int length = sourceChannel.read(byteBuffer); //緩衝區轉爲讀模式 byteBuffer.flip(); System.out.println(new String(byteBuffer.array(),0,length)); } }
NIO vs. IO
問題:
當學習Java的NIO和IO時,有個問題會跳入腦海當中:何時該用IO,何時用NIO?二者之間的區別,使用場景以及他們是如何影響代碼設計的。
NIO和IO之間的主要差別(Mian Differences Between Java NIO and IO)
下面這個表格歸納了NIO和IO的主要差別。咱們會針對每一個差別進行解釋。
IO NIO
Stream oriented Buffer oriented
Blocking IO No blocking IO
Selectors
即:
IO NIO
面向流 面向緩衝
阻塞IO 非阻塞IO
無 選擇器
面向流和麪向緩衝區比較(Stream Oriented vs. Buffer Oriented)
一、第一個重大差別是IO是面向流的,而NIO是面向緩衝區的。這句話是什麼意思呢?
Java IO面向流意思是咱們每次從流當中讀取一個或多個字節。怎麼處理讀取到的字節是咱們本身的事情。他們不會再任何地方緩存。再有就是咱們不能在流數據中向先後移動。若是須要向先後移動讀取位置,那麼咱們須要首先爲它建立一個緩存區。
Java NIO是面向緩衝區的,這有些細微差別。數據是被讀取到緩存當中以便後續加工。咱們能夠在緩存中向前向後移動。這個特性給咱們處理數據提供了更大的彈性空間。固然咱們仍然須要在使用數據前檢查緩存中是否包含咱們須要的全部數據。另外須要確保在往緩衝中寫入數據時避免覆蓋了已經寫入可是還未被處理的數據。
二、阻塞和非阻塞IO比較(Blocking vs. No-blocking IO)
Java IO的各類流都是阻塞的。這意味着一個線程一旦調用了read(),write()方法,那麼該線程就被阻塞住了,直到讀取到數據或者數據完整寫入了。在此期間線程不能作其餘任何事情。
Java NIO的非阻塞模式使得線程能夠經過channel來讀數據,而且是返回當前已有的數據,或者什麼都不返回。若是當前沒有數據可讀的話。這樣一來線程不會被阻塞住,它能夠繼續向下執行其餘事情。
一般線程在調用非阻塞操做後,會通知處理其餘channel上的IO操做。所以一個線程能夠管理多個channel的輸入輸出。
三、Selectors
Java NIO的selector容許一個單一線程監聽多個channel輸入。咱們能夠註冊多個channel到selector上,而後用一個線程來挑出一個處於可讀或者可寫狀態的channel。selector機制使得單線程管理過個channel變得容易。
NIO和IO是如何影響程序設計的(How NIO and IO Influences Application Design)
開發中選擇NIO或者IO會在多方面影響程序設計:
API調用(The API Calls)
顯而易見使用NIO的API接口和使用IO時是不一樣的。不一樣於直接從InputStream讀取字節,咱們的數據須要先寫入到buffer中,而後再從buffer中處理它們。
數據處理(The Processing of Data)
數據的處理方式也隨着是NIO或IO而異。
BIO下數據處理是阻塞的,一旦數據方法處理返回時數據就必定能讀取到或寫入好了,不會有隻作一半的狀況,且不能在流數據中向先後移動。而NIO是非阻塞的,在讀取或寫入數據緩衝區時是不能肯定數據是否已經完整讀完的,可能須要屢次檢查數據完整性。
例子:
在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()調用返回的時候,你知道這行包含年齡等。 正如你能夠看到,該處理程序僅在有新數據讀入時運行,並知道每步的數據是什麼。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據(大多如此)。下圖也說明了這條原則:
而一個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()方法被調用以前狀態相同。若是沒有,下一個讀入緩衝區的數據可能沒法讀到正確的位置。這是不可能的,但倒是須要注意的又一問題。
若是緩衝區已滿,它能夠被處理。若是它不滿,而且在你的實際案例中有意義,你或許能處理其中的部分數據。可是許多狀況下並不是如此。下圖展現了「緩衝區數據循環就緒」:
小結
NIO容許咱們只用一條線程來管理多個通道(網絡鏈接或文件),隨之而來的代價是解析數據相對於阻塞流來講可能會變得更加的複雜。
若是你須要同時管理成千上萬的連接,這些連接只發送少許數據,例如聊天服務器,用NIO來實現這個服務器是有優點的。相似的,若是你須要維持大量的連接,例如P2P網絡,用單線程來管理這些 連接也是有優點的。這種單線程多鏈接的NIO設計圖:
若是連接數不是不少,可是每一個連接的佔用較大帶寬,每次都要發送大量數據,那麼使用傳統的IO設計服務器多是最好的選擇。下面是經典IO服務設計圖:
NIO Path路徑
Java的path接口是做爲Java NIO 2的一部分是Java6,7中NIO的升級增長部分。Path在Java 7新增的。相關接口位於java.nio.file包下,因此Path接口的完整名稱是java.nio.file.Path.
一個Path實例表明一個文件系統內的路徑。path能夠指向文件也能夠指向目錄。能夠是相對路徑也能夠是絕對路徑。絕對路徑包含了從根目錄到該文件(目錄)的完整路徑。相對路徑包含該文件(目錄)相對於其餘路徑的路徑。
在不少狀況下java.no.file.Path接口和java.io.File比較類似,可是他們之間存在一些細微的差別。儘管如此,在大多數狀況下,咱們均可以用Path接口來替換File相關類。
建立Path實例(Creating a Path Instance)
爲了使用java.nio.file.Path實例咱們必須建立Path對象。建立Path實例能夠經過Paths的工廠方法get()。
注意Paths.get("c:\data\myfile.txt")的調用。這個方法會建立一個Path實例,換句話說Paths.get()是Paths的一個工廠方法。
建立絕對路徑(Creating an Absolute Path)
建立絕對路徑只須要調動Paths.get()這個工廠方法,同時傳入絕對文件。這是一個例子:
Path path = Paths.get("c:\\data\\myfile.txt");
對路徑是c:\data\myfile.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
建立相對路徑(Creating a Relative Path)
相對路徑是從一個路徑(基準路徑)指向另外一個目錄或文件的路徑。完整路徑實際上等同於相對路徑加上基準路徑。
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:\data\projects的Path實例。第二行建立了一個指向d:\data\projects\a-project\myfile.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
.和..也能夠結合起來用,這裏不過多介紹。
Path.normalize()
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
實例:
import java.nio.file.Path; import java.nio.file.Paths; public class PathTest { public static void main(String[] args) { //建立Path實例 Path path = Paths.get("c:\\data\\myfile.txt"); //建立絕對路徑(Creating an Absolute Path) Path path1 = Paths.get("c:\\data\\myfile.txt"); //建立相對路徑 Path path2 = Paths.get("d:\\data", "projects\\a-project\\myfile.txt"); //Path的normalize()方法能夠把路徑規範化 String originalPath = "d:\\data\\projects\\a-project\\..\\another-project"; Path path3 = Paths.get(originalPath); System.out.println("path3 = " + path3); Path path4 = path3.normalize(); System.out.println("path4 = " + path4); } }
NIO Files
Java NIO中的Files類(java.nio.file.Files)提供了多種操做文件系統中文件的方法。本節教程將覆蓋大部分方法。Files類包含了不少方法,因此若是本文沒有提到的你也能夠直接查詢JavaDoc文檔。
java.nio.file.Files類是和java.nio.file.Path相結合使用的,因此在用Files以前確保你已經理解了Path類。
Files.exists()
Files.exits()方法用來檢查給定的Path在文件系統中是否存在。 在文件系統中建立一個本來不存在的Path是可行的。例如,你想新建一個目錄,那麼先建立對應的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()
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()
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。
覆蓋已經存在的文件(Overwriting Existing Files)
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方法的第三個參數,這個參數決定了是否能夠覆蓋文件。
Files.move()
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()
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()
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。
Searching For Files
下面看一個例子,咱們經過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(); }
Deleting Directies Recursively
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(); }
示例:
public class FileTest { public static void main(String[] args) throws IOException { //建立絕對路徑 Path path = Paths.get("D:\\text\\file\\testfile.txt");//false //Path path = Paths.get("D:\\text\\1_loan.sql");//true //Path path = Paths.get("D:\\text\\test1\\1_loan.sql");//false //檢查給定的Path在文件系統中是否存在,NOFOLLOW_LINKS:表示檢測時不包含符號連接文件 boolean exists = Files.exists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS}); //默認不傳的話是包含符號連接文件的 //boolean exists = Files.exists(path); System.out.println("exists =" + exists); //路徑格式能夠是這兩種 Path filePath = Paths.get("D:\\text\\file"); //Path filePath = Paths.get("D:/text/file_copy"); //返回值就是新建立的路徑.建立文件夾 //Path directoryPath = Files.createDirectory(filePath); //createDirectory directoryPath=D:\text\file //System.out.println("createDirectory directoryPath=" + directoryPath); //返回值就是新建立的路徑.建立文件夾 //Path rtfilePath = Files.createFile(path); //createFile rtfilePath=D:\text\file\testfile.txt //System.out.println("createFile rtfilePath=" + rtfilePath); //Path path1 = Paths.get("D:\\text\\file\\subfile\\testfile.sql"); //NoSuchFileException: D:\text\file\subfile\testfile.sql because subfile not exist //Path rtpath1 = Files.createFile(path1); //System.out.println("createFile rtpath1=" + rtpath1); //Path path2 = Paths.get("D:\\text\\file\\subfile\\testfile.sql"); //建立連續不存在的文件夾,不存在就建立,不過只能建立文件夾,不能連同文件建立,文件要另外建立 //Path rtpath2 = Files.createDirectories(path2); //createFile rtpath2=D:\text\file\subfile\testfile.sql //System.out.println("createFile rtpath2=" + rtpath2); //copy Path sourcePath = Paths.get("D:\\text\\file\\testfile.txt"); Path destinationPath = Paths.get("D:\\text\\file\\testfile_copy.txt"); //copy =D:\text\file\testfile_copy.txt //copy的目標路徑文件不能存在,不然拋java.nio.file.FileAlreadyExistsException: D:\text\file\testfile_copy.txt異常 //Path copy = Files.copy(sourcePath, destinationPath); //copy操做能夠強制覆蓋已經存在的目標文件,傳入參數 StandardCopyOption.REPLACE_EXISTING //Path copy = Files.copy(sourcePath, destinationPath,StandardCopyOption.REPLACE_EXISTING); //System.out.println("copy =" + copy); //move:移動文件和重命名是同樣的 //Path sourcePathM = Paths.get("D:\\text\\file\\testfile.txt"); //Path destinationPathM = Paths.get("D:\\text\\file\\testfile_move.txt"); //Path move = Files.move(sourcePathM, destinationPathM); //move操做能夠強制覆蓋已經存在的目標文件,傳入參數 StandardCopyOption.REPLACE_EXISTING //原有的testfile.txt將被移動或重命名而不存在了,若是有目標文件testfile_move.txt存在,則會被覆蓋 //Path move = Files.move(sourcePathM, destinationPathM,StandardCopyOption.REPLACE_EXISTING); //System.out.println("move =" + move); //delete 刪除一個文件或目錄 //Path deletePath = Paths.get("D:\\text\\file\\testfile_move.txt"); //Exception in thread "main" java.nio.file.NoSuchFileException: D:\text\file\testfile_move.txt //要求要刪除的文件或目錄必須存在,不然報錯 //Files.delete(deletePath); //存在才刪除 //Files.deleteIfExists(deletePath); Path rootPath = Paths.get("D:"); final 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(); } } }
NIO AsynchronousFileChannel異步文件通道(AIO)
Java7中新增了AsynchronousFileChannel做爲nio的一部分。AsynchronousFileChannel使得數據能夠進行異步讀寫。
建立AsynchronousFileChannel(Creating an AsynchronousFileChannel)
AsynchronousFileChannel的建立能夠經過open()靜態方法:
Path path = Paths.get("data/test.xml"); AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
open()的第一個參數是一個Path實體,指向咱們須要操做的文件。 第二個參數是操做類型。上述示例中咱們用的是StandardOpenOption.READ,表示以讀的形式操做文件。
讀取數據(Reading Data)
讀取AsynchronousFileChannel的數據有兩種方式。每種方法都會調用AsynchronousFileChannel的一個read()接口。下面分別看一下這兩種寫法。
一、經過Future讀取數據(Reading Data Via a Future)
第一種方式是調用返回值爲Future的read()方法:
Future<Integer> operation = fileChannel.read(buffer, 0);
這種方式中,read()接受一個ByteBuffer做爲第一個參數,數據會被讀取到ByteBuffer中。第二個參數是開始讀取數據的位置。
read()方法會馬上返回,即便讀操做沒有完成。咱們能夠經過isDone()方法檢查操做是否完成。
下面是一個略長的示例:
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer buffer = ByteBuffer.allocate(1024); long position = 0; Future<Integer> operation = fileChannel.read(buffer, position); while(!operation.isDone()); buffer.flip(); byte[] data = new byte[buffer.limit()]; buffer.get(data); System.out.println(new String(data)); buffer.clear();
在這個例子中咱們建立了一個AsynchronousFileChannel,而後建立一個ByteBuffer做爲參數傳給read。接着咱們建立了一個循環來檢查是否讀取完畢isDone()。這裏的循環操做比較低效,它的意思是咱們須要等待讀取動做完成。
一旦讀取完成後,咱們就能夠把數據寫入ByteBuffer,而後輸出。
二、經過CompletionHandler讀取數據(Reading Data Via a CompletionHandler)
另外一種方式是調用接收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()方法。
寫數據(Writing Data)
和讀數據相似某些數據也有兩種方式,調動不一樣的的write()方法,下面分別看介紹這兩種方法。
經過Future寫數據(Writing Data Via a Future)
經過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寫數據(Writing Data Via a CompletionHandler)
咱們也能夠經過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()會被調用。
示例:
public class AIOTest { public static void main1(String[] args) throws IOException { //經過Future讀取數據 Path path = Paths.get("D:/test/file/README.txt"); AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); Future<Integer> future = asynChannel.read(byteBuffer, 0); while (!future.isDone()) { //等待讀取動做完成 }; byteBuffer.flip(); //有如下兩種輸出方式,本質上都是把緩衝區byteBuffer轉爲byte數組,再用new String接收 //System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit())); byte[] data = new byte[byteBuffer.limit()]; byteBuffer.get(data); //設置編碼格式 //System.out.println(new String(data, StandardCharsets.UTF_8)); //不設置編碼格式時取的是系統默認的編碼格式。在linux中是utf-8 System.out.println(new String(data)); byteBuffer.clear(); asynChannel.close(); } public static void main2(String[] args) throws IOException { //經過CompletionHandler讀取數據 Path path = Paths.get("D:/test/file/README.txt"); AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); asynChannel.read(byteBuffer, 0, byteBuffer, 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) { System.out.println("result failed " + exc.getMessage()); } }); asynChannel.close(); } public static void main3(String[] args) throws IOException { //經過Future寫數據 Path path = Paths.get("D:/test/file/README_WRITE.txt"); //若文件不存在則建立一個 if (!Files.exists(path)){ Files.createFile(path); } AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("i am batman,and i am rich".getBytes()); byteBuffer.flip(); Future<Integer> future = asynChannel.write(byteBuffer, 0); byteBuffer.clear(); while (!future.isDone()){ } System.out.println("Write done"); asynChannel.close(); } public static void main(String[] args) throws IOException { //經過CompletionHandler寫數據 Path path = Paths.get("D:/test/file/README_WRITE.txt"); //若文件不存在則建立一個 if (!Files.exists(path)){ Files.createFile(path); } AsynchronousFileChannel asynChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("i am batman,and i am rich".getBytes()); byteBuffer.flip(); asynChannel.write(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("Write done"); System.out.println("bytes written: " + result); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("bytes written failes: " + exc.getMessage()); } }); asynChannel.close(); } }
疑問:
Q:NIO的具體使用場景都有哪些?網絡鏈接?學習NIO的目的?
Q:Linux的五種IO模型?與java的io模型的關係?
《漫話:如何給女友解釋什麼是Linux的五種IO模型?》
Q:equals()判斷兩個buffer相對,需知足:
從上面的三個條件能夠看出,equals只比較buffer中的部份內容,並不會去比較每個元素。
全部剩餘字節相等是指若是這個buffer有被讀或者寫過,只比較他們剩餘沒有讀或者寫的部分是麼?
Q:FileChannel不能切換爲非阻塞模式,都有哪些Channel能夠切換爲非阻塞模式?
Q:select()方法的返回值是一個int整形,表明有多少channel處於就緒了。也就是自上一次select後有多少channel進入就緒。舉例來講,假設第一次調用select時正好有一個channel就緒,那麼返回值是1,而且對這個channel作任何處理,接着再次調用select,此時剛好又有一個新的channel就緒,那麼返回值仍是1,如今咱們一共有兩個channel處於就緒,可是在每次調用select時只有一個channel是就緒的。每次調用select時只有一個channel是就緒的?爲何?
Q:如何檢查message完整性?
Q:一個Message Reader天然是協議相關的?都有哪些協議?協議的做用是爲了約定規範麼?
Q:UDP是面向無鏈接的網絡協議,什麼叫無鏈接的網絡協議?
Q:面向流和麪向緩衝區的區別,咱們不能在流數據中向先後移動。若是須要向先後移動讀取位置,那麼咱們須要首先爲它建立一個緩存區?怎麼在緩衝區中向先後移動?
Q:針對做者畫的NIO和BIO這兩個交互圖不能很明確得觀察有什麼不一樣,特別是BIO前面部分,和NIO後面部分沒有畫出來,後面部分也是多線程處理啊?不一樣點是是否阻塞進行鏈接仍是非阻塞鏈接吧?
NIO:
BIO:
Q:NIO Path路徑(java.nio.file.Path )和以前的BIOPath路徑( java.io.File )有什麼區別?在使用時怎麼選擇?
其餘:
在用main方法測試時怎麼給String args[] 參數賦值?
一、直接在代碼中給args參數賦值一個咱們想要的數組。
static public void main(String args[]) throws Exception { args = new String[]{"D:\\text\\1_loan.sql", "D:\\text\\1_loan_copy.sql"}; //.... }
二、在idea裏的運行debug時能夠設置program arguments,以空格符分開參數。
2、注意當copy其餘java類進來時,若是引用類的包名路徑不一樣,會致使報錯,且還不能引用正確路徑上的類,這時候要點開import 裏引用的錯誤包路徑的類引用路徑,刪除了從新引入。快捷鍵 ctrl + alt + o。
3、符號連接文件:與硬鏈接相對應,Lnux系統中還存在另外一種鏈接,稱爲符號鏈接(Symbilc Link),也叫軟鏈接。軟連接文件有點相似於Windows的快捷方式 。
什麼是linux下的符號連接文件