通道
和 緩衝區
是 NIO 中的核心對象,幾乎在每個 I/O 操做中都要使用它們。html
通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的全部數據都必須經過一個 Channel 對象。一個 Buffer 實質上是一個容器對象。發送給一個通道的全部對象都必須首先放到緩衝區中;一樣地,從通道中讀取的任何數據都要讀到緩衝區中。java
在本節中,您會了解到 NIO 中通道和緩衝區是如何工做的。數組
Buffer
是一個對象, 它包含一些要寫入或者剛讀出的數據。 在 NIO 中加入 Buffer
對象,體現了新庫與原 I/O 的一個重要區別。在面向流的 I/O 中,您將數據直接寫入或者將數據直接讀到 Stream
對象中。網絡
在 NIO 庫中,全部數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的。在寫入數據時,它是寫入到緩衝區中的。任什麼時候候訪問 NIO 中的數據,您都是將它放到緩衝區中。app
緩衝區實質上是一個數組。一般它是一個字節數組,可是也可使用其餘種類的數組。可是一個緩衝區不 僅僅 是一個數組。緩衝區提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。dom
最經常使用的緩衝區類型是 ByteBuffer
。一個 ByteBuffer
能夠在其底層字節數組上進行 get/set 操做(即字節的獲取和設置)。異步
ByteBuffer
不是 NIO 中惟一的緩衝區類型。事實上,對於每一種基本 Java 類型都有一種緩衝區類型:socket
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每個 Buffer
類都是 Buffer
接口的一個實例。 除了 ByteBuffer
,每個 Buffer 類都有徹底同樣的操做,只是它們所處理的數據類型不同。由於大多數標準 I/O 操做都使用 ByteBuffer
,因此它具備全部共享的緩衝區操做以及一些特有的操做。學習
如今您能夠花一點時間運行 UseFloatBuffer.java,它包含了類型化的緩衝區的一個應用例子。this
Channel
是一個對象,能夠經過它讀取和寫入數據。拿 NIO 與原來的 I/O 作個比較,通道就像是流。
正如前面提到的,全部數據都經過 Buffer
對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩衝區。一樣,您不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。
通道與流的不一樣之處在於通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream
或者OutputStream
的子類), 而 通道
能夠用於讀、寫或者同時用於讀寫。
由於它們是雙向的,因此通道能夠比流更好地反映底層操做系統的真實狀況。特別是在 UNIX 模型中,底層操做系統通道是雙向的。
讀和寫是 I/O 的基本過程。從一個通道中讀取很簡單:只需建立一個緩衝區,而後讓通道將數據讀到這個緩衝區中。寫入也至關簡單:建立一個緩衝區,用數據填充它,而後讓通道用這些數據來執行寫入操做。
在本節中,咱們將學習有關在 Java 程序中讀取和寫入數據的一些知識。咱們將回顧 NIO 的主要組件(緩衝區、通道和一些相關的方法),看看它們是如何交互以進行讀寫的。在接下來的幾節中,咱們將更詳細地分析這其中的每一個組件以及其交互。
在咱們第一個練習中,咱們將從一個文件中讀取一些數據。若是使用原來的 I/O,那麼咱們只需建立一個FileInputStream
並從它那裏讀取。而在 NIO 中,狀況稍有不一樣:咱們首先從 FileInputStream
獲取一個 Channel
對象,而後使用這個通道來讀取數據。
在 NIO 系統中,任什麼時候候執行一個讀操做,您都是從通道中讀取,可是您不是 直接 從通道讀取。由於全部數據最終都駐留在緩衝區中,因此您是從通道讀到緩衝區中。
所以讀取文件涉及三個步驟:(1) 從 FileInputStream
獲取 Channel
,(2) 建立 Buffer
,(3) 將數據從 Channel
讀到 Buffer
中。
如今,讓咱們看一下這個過程。
第一步是獲取通道。咱們從 FileInputStream
獲取通道:
1
2
|
FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();
|
下一步是建立緩衝區:
1
|
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
|
最後,須要將數據從通道讀到緩衝區中,以下所示:
1
|
fc.read( buffer );
|
您會注意到,咱們不須要告訴通道要讀 多少數據 到緩衝區中。每個緩衝區都有複雜的內部統計機制,它會跟蹤已經讀了多少數據以及還有多少空間能夠容納更多的數據。咱們將在 緩衝區內部細節 中介紹更多關於緩衝區統計機制的內容。
在 NIO 中寫入文件相似於從文件中讀取。首先從 FileOutputStream
獲取一個通道:
1
2
|
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();
|
下一步是建立一個緩衝區並在其中放入一些數據 - 在這裏,數據將從一個名爲 message
的數組中取出,這個數組包含字符串 "Some bytes" 的 ASCII 字節(本教程後面將會解釋 buffer.flip()
和buffer.put()
調用)。
1
2
3
4
5
6
|
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
|
最後一步是寫入緩衝區中:
1
|
fc.write( buffer );
|
注意在這裏一樣不須要告訴通道要寫入多數據。緩衝區的內部統計機制會跟蹤它包含多少數據以及還有多少數據要寫入。
下面咱們將看一下在結合讀和寫時會有什麼狀況。咱們以一個名爲 CopyFile.java 的簡單程序做爲這個練習的基礎,它將一個文件的全部內容拷貝到另外一個文件中。CopyFile.java 執行三個基本操做:首先建立一個 Buffer
,而後從源文件中將數據讀到這個緩衝區中,而後將緩衝區寫入目標文件。這個程序不斷重複 ― 讀、寫、讀、寫 ― 直到源文件結束。
CopyFile 程序讓您看到咱們如何檢查操做的狀態,以及如何使用 clear()
和 flip()
方法重設緩衝區,並準備緩衝區以便將新讀取的數據寫到另外一個通道中。
public class CopyFile { static public void main( String args[] ) throws Exception { if (args.length<2) { System.err.println( "Usage: java CopyFile infile outfile" ); System.exit( 1 ); } String infile = args[0]; String outfile = args[1]; FileInputStream fin = new FileInputStream( infile ); FileOutputStream fout = new FileOutputStream( outfile ); FileChannel fcin = fin.getChannel(); FileChannel fcout = fout.getChannel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); while (true) { buffer.clear(); int r = fcin.read( buffer ); if (r==-1) { break; } buffer.flip(); fcout.write( buffer ); } } }
使用管道實現讀和寫 buffer.flip
clear()
方法重設緩衝區,使它能夠接受讀入的數據。 flip()
方法讓緩衝區能夠將新讀入的數據寫入另外一個通道。
緩衝區的三個狀態變量:
position limit capacity
如今咱們要將數據寫到輸出通道中。在這以前,咱們必須調用 flip()
方法。這個方法作兩件很是重要的事:
limit
設置爲當前 position
。position
設置爲 0。前一小節中的圖顯示了在 flip 以前緩衝區的狀況。下面是在 flip 以後的緩衝區:
咱們如今能夠將數據從緩衝區寫入通道了。 position
被設置爲 0,這意味着咱們獲得的下一個字節是第一個字節。 limit
已被設置爲原來的 position
,這意味着它包括之前讀到的全部字節,而且一個字節也很少。
在第一次寫入時,咱們從緩衝區中取四個字節並將它們寫入輸出通道。這使得 position
增長到 4,而 limit
不變,以下所示:
咱們只剩下一個字節可寫了。 limit
在咱們調用 flip()
時被設置爲 5,而且 position
不能超過 limit
。因此最後一次寫入操做從緩衝區取出一個字節並將它寫入輸出通道。這使得 position
增長到 5,並保持 limit
不變,以下所示:
ByteBuffer中存放不一樣的數據類型:
public class TypesInByteBuffer { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(64); buffer.putInt(30); buffer.putLong(700000L); buffer.putDouble(Math.PI); buffer.flip(); System.out.println(buffer.getInt()); System.out.println(buffer.getLong()); System.out.println(buffer.getDouble()); }}
在可以讀和寫以前,必須有一個緩衝區。要建立緩衝區,您必須 分配 它。咱們使用靜態方法 allocate()
來分配緩衝區:
1
|
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
|
allocate()
方法分配一個具備指定大小的底層數組,並將它包裝到一個緩衝區對象中 ― 在本例中是一個 ByteBuffer
。
您還能夠將一個現有的數組轉換爲緩衝區,以下所示:
1
2
|
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
|
本例使用了 wrap()
方法將一個數組包裝爲緩衝區。必須很是當心地進行這類操做。一旦完成包裝,底層數據就能夠經過緩衝區或者直接訪問。
slice()
方法根據現有的緩衝區建立一種 子緩衝區 。也就是說,它建立一個新的緩衝區,新緩衝區與原來的緩衝區的一部分共享數據。
使用例子能夠最好地說明這點。讓咱們首先建立一個長度爲 10 的 ByteBuffer
:
1
|
ByteBuffer buffer = ByteBuffer.allocate( 10 );
|
而後使用數據來填充這個緩衝區,在第 n 個槽中放入數字 n:
1
2
3
|
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
|
如今咱們對這個緩衝區 分片 ,以建立一個包含槽 3 到槽 6 的子緩衝區。在某種意義上,子緩衝區就像原來的緩衝區中的一個窗口 。
窗口的起始和結束位置經過設置 position
和 limit
值來指定,而後調用 Buffer
的 slice()
方法:
1
2
3
|
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
|
片
是緩衝區的 子緩衝區
。不過, 片斷
和 緩衝區
共享同一個底層數據數組,咱們在下一節將會看到這一點。
咱們已經建立了原緩衝區的子緩衝區,而且咱們知道緩衝區和子緩衝區共享同一個底層數據數組。讓咱們看看這意味着什麼。
咱們遍歷子緩衝區,將每個元素乘以 11 來改變它。例如,5 會變成 55。
1
2
3
4
5
|
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}
|
最後,再看一下原緩衝區中的內容:
1
2
3
4
5
6
|
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}
|
只讀緩衝區:asReadOnlyBuffer()
直接和間接緩衝區:
ByteBuffer buffer =ByteBuffer.allocateDirect(1024);
直接緩衝區加快IO速度 給定一個直接字節緩衝區,Java 虛擬機將盡最大努力直接對它執行本機 I/O 操做。也就是說,它會在每一次調用底層操做系統的本機 I/O 操做以前(或以後),嘗試避免將緩衝區的內容拷貝到一箇中間緩衝區中(或者從一箇中間緩衝區中拷貝數據)。
內存映射文件 I/O 是一種讀和寫文件數據的方法,它能夠比常規的基於流或者基於通道的 I/O 快得多。
內存映射文件 I/O 是經過使文件中的數據神奇般地出現爲內存數組的內容來完成的。這其初聽起來彷佛不過就是將整個文件讀到內存中,可是事實上並非這樣。通常來講,只有文件中實際讀取或者寫入的部分纔會送入(或者 映射 )到內存中。
內存映射並不真的神奇或者多麼不尋常。現代操做系統通常根據須要將文件的部分映射爲內存的部分,從而實現文件系統。Java 內存映射機制不過是在底層操做系統中能夠採用這種機制時,提供了對該機制的訪問。
儘管建立內存映射文件至關簡單,可是向它寫入多是危險的。僅只是改變數組的單個元素這樣的簡單操做,就可能會直接修改磁盤上的文件。修改數據與將數據保存到磁盤是沒有分開的。
瞭解內存映射的最好方法是使用例子。在下面的例子中,咱們要將一個 FileChannel
(它的所有或者部分)映射到內存中。爲此咱們將使用 FileChannel.map()
方法。下面代碼行將文件的前 1024 個字節映射到內存中:
|
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );
|
map()
方法返回一個 MappedByteBuffer
,它是 ByteBuffer
的子類。所以,您能夠像使用其餘任何 ByteBuffer
同樣使用新映射的緩衝區,操做系統會在須要時負責執行行映射。
分散/彙集 I/O 是使用多個而不是單個緩衝區來保存數據的讀寫方法。
一個分散的讀取就像一個常規通道讀取,只不過它是將數據讀到一個緩衝區數組中而不是讀到單個緩衝區中。一樣地,一個彙集寫入是向緩衝區數組而不是向單個緩衝區寫入數據。
分散/彙集 I/O 對於將數據流劃分爲單獨的部分頗有用,這有助於實現複雜的數據格式。
public class UseScatterGather { static private final int firstHeaderLength = 2; static private final int secondHeaderLength = 4; static private final int bodyLength = 6; public static void main(String[] args) throws Exception{ if (args.length!=1) { System.err.println( "Usage: java UseScatterGather port" ); System.exit( 1 ); } int port=Integer.parseInt(args[0]); ServerSocketChannel ssc=ServerSocketChannel.open(); InetSocketAddress address= new InetSocketAddress(port); ssc.socket().bind(address); int messageLength=firstHeaderLength+secondHeaderLength+bodyLength; ByteBuffer buffers[] = new ByteBuffer[3]; buffers[0] = ByteBuffer.allocate( firstHeaderLength ); buffers[1] = ByteBuffer.allocate( secondHeaderLength ); buffers[2] = ByteBuffer.allocate( bodyLength ); SocketChannel sc= ssc.accept(); while (true){ //分散讀到 buffers int bytesRead=0; while(bytesRead<messageLength){ long r = sc.read(buffers); bytesRead+=r; System.out.println("r"+r); for(int i=0;i<buffers.length;i++){ ByteBuffer bb= buffers[i]; System.out.println("b"+i+" "+bb.position()+" "+bb.limit()); } } //處理消息 //flip buffers for(int i =0;i<buffers.length;i++){ ByteBuffer bb= buffers[i]; bb.flip(); } //scatter-write back out long bytesWritten = 0; while(bytesWritten<messageLength){ long r= sc.write(buffers); bytesWritten+=r; } //clear buffers for(int i =0;i<buffers.length;i++){ ByteBuffer bb= buffers[i]; bb.clear(); } System.out.println(bytesRead+" "+bytesWritten+" "+messageLength); } } }
文件鎖定初看起來可能讓人迷惑。它 彷佛 指的是防止程序或者用戶訪問特定文件。事實上,文件鎖就像常規的 Java 對象鎖 ― 它們是 勸告式的(advisory) 鎖。它們不阻止任何形式的數據訪問,相反,它們經過鎖的共享和獲取賴容許系統的不一樣部分相互協調。
您能夠鎖定整個文件或者文件的一部分。若是您獲取一個排它鎖,那麼其餘人就不能得到同一個文件或者文件的一部分上的鎖。若是您得到一個共享鎖,那麼其餘人能夠得到同一個文件或者文件一部分上的共享鎖,可是不能得到排它鎖。文件鎖定並不老是出於保護數據的目的。例如,您可能臨時鎖定一個文件以保證特定的寫操做成爲原子的,而不會有其餘程序的干擾。
大多數操做系統提供了文件系統鎖,可是它們並不都是採用一樣的方式。有些實現提供了共享鎖,而另外一些僅提供了排它鎖。事實上,有些實現使得文件的鎖定部分不可訪問,儘管大多數實現不是這樣的。
在本節中,您將學習如何在 NIO 中執行簡單的文件鎖過程,咱們還將探討一些保證被鎖定的文件儘量可移植的方法。
要獲取文件的一部分上的鎖,您要調用一個打開的 FileChannel
上的 lock()
方法。注意,若是要獲取一個排它鎖,您必須以寫方式打開文件。
1
2
3
|
RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock( start, end, false );
|
在擁有鎖以後,您能夠執行須要的任何敏感操做,而後再釋放鎖:
1
|
lock.release();
|
在釋放鎖後,嘗試得到鎖的其餘任何程序都有機會得到它。
本小節的例子程序 UseFileLocks.java 必須與它本身並行運行。這個程序獲取一個文件上的鎖,持有三秒鐘,而後釋放它。若是同時運行這個程序的多個實例,您會看到每一個實例依次得到鎖。
文件鎖定多是一個複雜的操做,特別是考慮到不一樣的操做系統是以不一樣的方式實現鎖這一事實。下面的指導原則將幫助您儘量保持代碼的可移植性:
public class UseFileLocks { static private final int start=10; static private final int end=20; public static void main(String[] args) throws Exception{ //get file channel RandomAccessFile raf= new RandomAccessFile("test.txt","rw"); FileChannel fc=raf.getChannel(); //get lock System.out.println("trying to get lock"); FileLock lock=fc.lock(start,end,false); System.out.println("got lock"); //pause System.out.println("pausing"); try{ Thread.sleep(3000); }catch (InterruptedException e){ } //release lock System.out.println("going to release lock"); lock.release(); System.out.println("release lock"); raf.close(); } }
連網是學習異步 I/O 的很好基礎,而異步 I/O 對於在 Java 語言中執行任何輸入/輸出過程的人來講,無疑都是必須具有的知識。NIO 中的連網與 NIO 中的其餘任何操做沒有什麼不一樣 ― 它依賴通道和緩衝區,而您一般使用 InputStream
和OutputStream
來得到通道。
本節首先介紹異步 I/O 的基礎 ― 它是什麼以及它不是什麼,而後轉向更實用的、程序性的例子。
異步 I/O 是一種 沒有阻塞地 讀寫數據的方法。一般,在代碼進行 read()
調用時,代碼會阻塞直至有可供讀取的數據。一樣, write()
調用將會阻塞直至數據可以寫入。
另外一方面,異步 I/O 調用不會阻塞。相反,您將註冊對特定 I/O 事件的興趣 ― 可讀的數據的到達、新的套接字鏈接,等等,而在發生這樣的事件時,系統將會告訴您。
異步 I/O 的一個優點在於,它容許您同時根據大量的輸入和輸出執行 I/O。同步程序經常要求助於輪詢,或者建立許許多多的線程以處理大量的鏈接。使用異步 I/O,您能夠監放任何數量的通道上的事件,不用輪詢,也不用額外的線程。
咱們將經過研究一個名爲 MultiPortEcho.java 的例子程序來查看異步 I/O 的實際應用。這個程序就像傳統的 echo server,它接受網絡鏈接並向它們迴響它們可能發送的數據。不過它有一個附加的特性,就是它能同時監聽多個端口,並處理來自全部這些端口的鏈接。而且它只在單個線程中完成全部這些工做。
public class MultiPortEcho { private int ports[]; private ByteBuffer echoBuffer = ByteBuffer.allocate(1024); public MultiPortEcho(int[] ports) throws IOException { this.ports = ports; go(); } private void go() throws IOException { Selector selector = Selector.open(); //open a listener on each port and register each one with the selector for (int i = 0; i < ports.length; i++) { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ServerSocket ss = ssc.socket(); InetSocketAddress address = new InetSocketAddress(ports[i]); ss.bind(address); SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT); System.out.println("going to listen on" + ports[i]); } while (true) { int num = selector.select(); Set selectedKeys = selector.selectedKeys(); Iterator it = selectedKeys.iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false); //add new connection to the selector SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ); it.remove(); System.out.println("got connection from " + sc); } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { //read data SocketChannel sc = (SocketChannel) key.channel(); //echo data int bytesEchoed = 0; while (true) { echoBuffer.clear(); int r = sc.read(echoBuffer); if (r <= 0) break; echoBuffer.flip(); sc.write(echoBuffer); bytesEchoed += r; } System.out.println("echoed" + bytesEchoed + "from " + sc); it.remove(); } } } } public static void main(String[] args) throws Exception { if (args.length <= 0) { System.err.println("Usage: java MultiPortEcho port [port port ...]"); System.exit(1); } int ports[] = new int[args.length]; for (int i = 0; i < args.length; i++) { ports[i] = Integer.parseInt(args[i]); } new MultiPortEcho(ports); } }