Java NIO3:通道和檔案通道

五月的倉頡發表於2015-12-29

通道是什麼

通道式(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

相關文章