通道是什麼
通道式(Channel)是java.nio的第二個主要創新。通道既不是一個擴充套件也不是一項增強,而是全新的、極好的Java I/O示例,提供與I/O服務的直接連線。Channel用於在位元組緩衝區和位於通道另一側的實體(通常是一個檔案或套接字)之間有效地傳輸資料。
通常情況下,通道與作業系統的檔案描述符(FileDescriptor)和檔案控制程式碼(FileHandler)有著一對一的關係。雖然通道比檔案描述符更廣義,但開發者經常使用到的多數通道都是連線到開放的檔案描述符的。Channel類提供維持平臺獨立性所需的抽象過程,不然仍然會模擬現代作業系統本身的I/O效能。
通道是一種途徑,藉助該途徑,可以用最小的總開銷來訪問作業系統本身的I/O服務。緩衝區則是通道內部用來傳送和接收資料的端點,如下圖:
通道基礎
首先,看一下基本的Channel介面,下面是Channel介面的完整原始碼:
public interface Channel extends Closeable { /** * Tells whether or not this channel is open. </p> * * @return <tt>true</tt> if, and only if, this channel is open */ public boolean isOpen(); /** * Closes this channel. * * <p> After a channel is closed, any further attempt to invoke I/O * operations upon it will cause a {@link ClosedChannelException} to be * thrown. * * <p> If this channel is already closed then invoking this method has no * effect. * * <p> This method may be invoked at any time. If some other thread has * already invoked it, however, then another invocation will block until * the first invocation is complete, after which it will return without * effect. </p> * * @throws IOException If an I/O error occurs */ public void close() throws IOException; }
和緩衝區不同,通道API主要由介面指定。不同的作業系統上通道實現會有根本性的差異,所以通道API僅僅描述了可以做什麼,因此很自然地,通道實現經常使用作業系統的原生程式碼,通道介面允許開發者以一種受控且可移植的方式來訪問底層的I/O服務。
可以從底層的Channel介面看到,對所有通道來說只有兩種共同的操作:檢查一個通道是否開啟isOpen()和關閉一個開啟的通道close(),其餘所有的東西都是那些實現Channel介面以及它的子介面的類。
從Channel介面引申出的其他介面都是面向位元組的子介面:
包括WritableByteChannel和ReadableByteChannel。這也正好支援了我們之前的所學:通道只能在位元組緩衝區上操作。層次介面表明其他資料型別的通道也可以從Channel介面引申而來。這是一種很好的鐳射機,不過非位元組實現是不可能的,因為作業系統都是以位元組的形式實現底層I/O介面的。
認識通道
看一下基本的介面:
public interface ReadableByteChannel extends Channel { public int read(ByteBuffer dst) throws IOException; }
public interface WritableByteChannel extends Channel { public int write(ByteBuffer src) throws IOException; }
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel { }
通道可以是單向的也可以是雙向的。一個Channel類可能實現定義read()方法的ReadableByteChannel介面,而另一個Channel類也許實現WritableByteChannel介面以提供write()方法。實現這兩種介面其中之一的類都是單向的,只能在一個方向上傳輸資料。如果一個類同時實現這兩個介面,那麼它是雙向的,可以雙向傳輸資料,就像上面的ByteChannel。
通道可以以阻塞(blocking)或非阻塞(nonblocking)模式執行,非阻塞模式的通道永遠不會讓呼叫的執行緒休眠,請求的操作要麼立即完成,要麼返回一個結果表明未進行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。
比方說非阻塞的通道SocketChannel:
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel { ... }
public abstract class AbstractSelectableChannel extends SelectableChannel { ... }
可以看出,socket通道類從SelectableChannel類引申而來,從SelectableChannel引申而來的類可以和支援有條件的選擇的選擇器(Selectors)一起使用。將非阻塞I/O和選擇器組合起來可以使開發者的程式利用多路複用I/O,選擇器和多路複用將在之後的文章予以說明。
認識檔案通道
通道是訪問I/O服務的導管,I/O可以分為廣義的兩大類:File I/O和Stream I/O。那麼相應的,通道也有兩種型別,它們是檔案(File)通道和套接字(Socket)通道。檔案通道指的是FileChannel,套接字通道則有三個,分別是SocketChannel、ServerSocketChannel和DatagramChannel。
通道可以以多種方式建立。Socket通道可以有直接建立Socket通道的工廠方法,但是一個FileChannel物件卻只能通過在一個開啟的RandomAccessFile、FileInputStream或FileOutputStream物件上呼叫getChannel()方法來獲取,開發者不能直接建立一個FileChannel。
檔案I/O是我們最常使用的I/O,因此這部分先認識一下檔案通道,下一部分再以程式碼形式演示如何使用檔案通道高。用UML圖表示一下檔案通道的類層次關係:
檔案通道總是阻塞式的,因此不能被置於非阻塞模式下。
前面提到過了,FileChannel物件不能直接建立,一個FileChannel例項只能通過在一個開啟的File物件(RandomAccessFile、FileInputStream或FileOutputStream)上呼叫getChannel()方法獲取,呼叫getChannel()方法會返回一個連線到相同檔案的FileChannel物件且該FileChannel物件具有與file物件相同的訪問許可權,然後就可以使用通道物件來利用強大的FileChannel API了。
FileChannel物件是執行緒安全的,多個程式可以在同一個例項上併發呼叫方法而不會引起任何問題,不過並非所有的操作都是多執行緒的。影響通道位置或者影響檔案的操作都是單執行緒的,如果有一個執行緒已經在執行會影響通道位置或檔案大小的操作,那麼其他嘗試進行此類操作之一的執行緒必須等待,併發行為也會受到底層作業系統或檔案系統的影響。
使用檔案通道讀資料
講了這麼多理論,下面來看一下如何使用檔案通道,首先是從檔案中讀出資料:
public static void main(String[] args) throws Exception { File file = new File("D:/files/readchannel.txt"); FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteBuffer bb = ByteBuffer.allocate(35); fc.read(bb); bb.flip(); while (bb.hasRemaining()) { System.out.print((char)bb.get()); } bb.clear(); fc.close(); }
這是最簡單的操作,前面講過檔案通道必須通過一個開啟的RandomAccessFile、FileInputStream、FileOutputStream獲取到,因此這裡使用FileInputStream來獲取FileChannel。接著只要使用read方法將內容讀取到緩衝區內即可,緩衝區內有了資料,就可以使用前文對於緩衝區的操作讀取資料了。檔案裡面的資料是:
控制檯上列印出來的資料是:
channel1! channel2! channel3!
沒有問題。
使用檔案通道寫資料
上面看了使用檔案通道讀資料,接著看一下使用檔案通道寫資料,差不多:
public static void main(String[] args) throws Exception { File file = new File("D:/files/writechannel.txt"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); FileChannel fc = raf.getChannel(); ByteBuffer bb = ByteBuffer.allocate(10); String str = "abcdefghij"; bb.put(str.getBytes()); bb.flip(); fc.write(bb); bb.clear(); fc.close(); }
這裡使用了RandomAccessFile去獲取FileChannel,然後操作其實差不多,write方法寫ByteBuffer中的內容至檔案中,注意寫之前還是要先把ByteBuffer給flip一下。
可能有人覺得這種連續put的方法非常不方便,但是沒有辦法,之前已經提到過了:通道只能使用ByteBuffer。