Java NIO系列教程(二) Channel通道介紹及FileChannel詳解

目錄:html

Java NIO系列教程(二) Channeljava

Java NIO系列教程(三) Channel之Socket通道spring

 

Channel是一個通道,能夠經過它讀取和寫入數據,它就像自來水管同樣,網絡數據經過Channel讀取和寫入。通道與流的不一樣之處在於通道是雙向的,流只是在一個方向上移動(一個流必須是InputStream或者OutputStream的子類),並且通道能夠用於讀、寫或者同事用於讀寫。由於Channel是全雙工的,因此它能夠比流更好地映射底層操做系統的API。特別是在UNIX網絡編程模型中,底層操做系統的通道都是全雙工的,同時支持讀寫操做。編程

NIO中經過channel封裝了對數據源的操做,經過channel 咱們能夠操做數據源,但又沒必要關心數據源的具體物理結構。
這個數據源多是多種的。好比,能夠是文件,也能夠是網絡socket。在大多數應用中,channel與文件描述符或者socket是一一對應的。Channel用於在字節緩衝區和位於通道另外一側的實體(一般是一個文件或套接字)之間有效地傳輸數據。
channel接口源碼:緩存

package java.nio.channels;
public interface Channel;
{
    public boolean isOpen();
    public void close() throws IOException;
}

與緩衝區不一樣,通道API主要由接口指定。不一樣的操做系統上通道實現(Channel Implementation)會有根本性的差別,因此通道API僅僅描述了能夠作什麼。所以很天然地,通道實現常用操做系統的本地代碼。通道接口容許您以一種受控且可移植的方式來訪問底層的I/O服務。安全

 

Channel是一個對象,能夠經過它讀取和寫入數據。拿 NIO 與原來的 I/O 作個比較,通道就像是流。全部數據都經過 Buffer 對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩衝區。一樣,您不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。服務器

 

Java NIO的通道相似流,但又有些不一樣:網絡

  • 既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向的。
  • 通道能夠異步地讀寫。
  • 通道中的數據老是要先讀到一個Buffer,或者老是要從一個Buffer中寫入。

正如上面所說,從通道讀取數據到緩衝區,從緩衝區寫入數據到通道。以下圖所示:session

 

Channel的實現

這些是Java NIO中最重要的通道的實現:多線程

  • FileChannel:從文件中讀寫數據
  • DatagramChannel:經過UDP讀寫網絡中的數據
  • SocketChannel:經過TCP讀寫網絡中的數據
  • ServerSocketChannel:能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。

正如你所看到的,這些通道涵蓋了UDP 和 TCP 網絡IO,以及文件IO。

FileChannel

FileChannel類能夠實現經常使用的read,write以及scatter/gather操做,同時它也提供了不少專用於文件的新方法。這些方法中的許多都是咱們所熟悉的文件操做。
FileChannel類的JDK源碼:
    package java.nio.channels;
    public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
    {
        // This is a partial API listing
        // All methods listed here can throw java.io.IOException
        public abstract int read (ByteBuffer dst, long position);
        public abstract int write (ByteBuffer src, long position);
        public abstract long size();
        public abstract long position();
        public abstract void position (long newPosition);
        public abstract void truncate (long size);
        public abstract void force (boolean metaData);
        public final FileLock lock();
        public abstract FileLock lock (long position, long size, boolean shared);
        public final FileLock tryLock();
        public abstract FileLock tryLock (long position, long size, boolean shared);
        public abstract MappedByteBuffer map (MapMode mode, long position, long size);
        public static class MapMode;
        public static final MapMode READ_ONLY;
        public static final MapMode READ_WRITE;
        public static final MapMode PRIVATE;
        public abstract long transferTo (long position, long count, WritableByteChannel target);
        public abstract long transferFrom (ReadableByteChannel src, long position, long count);
    } 

文件通道老是阻塞式的,所以不能被置於非阻塞模式。現代操做系統都有複雜的緩存和預取機制,使得本地磁盤I/O操做延遲不多。網絡文件系統通常而言延遲會多些,不過卻也因該優化而受益。面向流的I/O的非阻塞範例對於面向文件的操做並沒有多大意義,這是由文件I/O本質上的不一樣性質形成的。對於文件I/O,最強大之處在於異步I/O(asynchronous I/O),它容許一個進程能夠從操做系統請求一個或多個I/O操做而沒必要等待這些操做的完成發起請求的進程以後會收到它請求的I/O操做已完成的通知

  FileChannel對象是線程安全(thread-safe)的。多個進程能夠在同一個實例上併發調用方法而不會引發任何問題,不過並不是全部的操做都是多線程的(multithreaded)。影響通道位置或者影響文件大小的操做都是單線程的(single-threaded)。若是有一個線程已經在執行會影響通道位置或文件大小的操做,那麼其餘嘗試進行此類操做之一的線程必須等待。併發行爲也會受到底層的操做系統或文件系統影響。

  每一個FileChannel對象都同一個文件描述符(file descriptor)有一對一的關係,因此上面列出的API方法與在您最喜歡的POSIX(可移植操做系統接口)兼容的操做系統上的經常使用文件I/O系統調用緊密對應也就不足爲怪了。本質上講,RandomAccessFile類提供的是一樣的抽象內容。在通道出現以前,底層的文件操做都是經過RandomAccessFile類的方法來實現的。FileChannel模擬一樣的I/O服務,所以它的API天然也是很類似的。

  三者之間的方法對比:

  
FILECHANNEL RANDOMACCESSFILE POSIX SYSTEM CALL
read( ) read( ) read( )
write( ) write( ) write( )
size( ) length( ) fstat( )
position( ) getFilePointer( ) lseek( )
position (long newPosition) seek( ) lseek( )
truncate( ) setLength( ) ftruncate( )
force( ) getFD().sync( ) fsync( )

 

下面是一個使用FileChannel讀取數據到Buffer中的示例:

package com.dxz.springsession.nio.demo1;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelTest {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("d:\\soft\\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();
        System.out.println("wan");
    }

}

文件內容:

1234567qwertrewq
uytrewq
hgfdsa
nbvcxz
iop89

輸出結果:

Read 48
1234567qwertrewq
uytrewq
hgfdsa
nbvcxz
iop89wan

注意 buf.flip() 的調用,首先讀取數據到Buffer,而後反轉Buffer,接着再從Buffer中讀取數據。下一節會深刻講解Buffer的更多細節。

一、打開FileChannel

在使用FileChannel以前,必須先打開它。可是,咱們沒法直接打開一個FileChannel,須要經過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是經過RandomAccessFile打開FileChannel的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

二、從FileChannel讀取數據

調用多個read()方法之一從FileChannel中讀取數據。如:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。

而後,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。若是返回-1,表示到了文件末尾。

三、向FileChannel寫數據

使用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

用完FileChannel後必須將其關閉。如:

channel.close();

五、FileChannel的position方法

有時可能須要在FileChannel的某個特定位置進行數據的讀/寫操做。能夠經過調用position()方法獲取FileChannel的當前位置。

也能夠經過調用position(long pos)方法設置FileChannel的當前位置。

這裏有兩個例子:

long pos = channel.position();
channel.position(pos +123);

若是將位置設置在文件結束符以後,而後試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標誌。

若是將位置設置在文件結束符以後,而後向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能致使「文件空洞」,磁盤上物理文件中寫入的數據間有空隙。

六、FileChannel的size方法

FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:

long fileSize = channel.size();

七、FileChannel的truncate方法

可使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度後面的部分將被刪除。如:

channel.truncate(1024);

這個例子截取文件的前1024個字節。

八、FileChannel的force方法

FileChannel.force()方法將通道里還沒有寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操做系統會將數據緩存在內存中,因此沒法保證寫入到FileChannel裏的數據必定會即時寫到磁盤上。要保證這一點,須要調用force()方法。

force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。

下面的例子同時將文件數據和元數據強制寫到磁盤上:

channel.force(true);

示例:

複製代碼
package com.dxz.nio;

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelRead {
    static public void main(String args[]) throws Exception {
        FileInputStream fin = new FileInputStream("e:\\logs\\test.txt");
        // 獲取通道
        FileChannel fc = fin.getChannel();
        // 建立緩衝區
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 讀取數據到緩衝區
        fc.read(buffer);
        buffer.flip();

        while (buffer.remaining() > 0) {
            byte b = buffer.get();
            System.out.print(((char) b));
        }
        fin.close();
    }
}
複製代碼

寫入:

package com.dxz.nio;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelWrite {
    static private final byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 };

    static public void main(String args[]) throws Exception {
        FileOutputStream fout = new FileOutputStream("e:\\logs\\test2.txt");
        FileChannel fc = fout.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        for (int i = 0; i < message.length; ++i) {
            buffer.put(message[i]);
        }
        buffer.flip();
        fc.write(buffer);
        fout.close();
    }
}

九、FileChannel的transferTo和transferFrom方法--通道之間的數據傳輸

若是兩個通道中有一個是FileChannel,那你能夠直接將數據從一個channel(譯者注:channel中文常譯做通道)傳輸到另一個channel。

transferFrom()

FileChannel的transferFrom()方法能夠將數據從源通道傳輸到FileChannel中(譯者注:這個方法在JDK文檔中的解釋爲將字節從給定的可讀取字節通道傳輸到此通道的文件中)。下面是一個簡單的例子:

經過FileChannel完成文件間的拷貝:

package com.dxz.springsession.nio.demo1;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

public class FileChannelTest2 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("d:\\soft\\fromFile.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();
        
        RandomAccessFile bFile = new RandomAccessFile("d:\\soft\\toFile.txt", "rw");
        FileChannel toChannel = bFile.getChannel();
        long position = 0;
        long count = fromChannel.size();
        toChannel.transferFrom(fromChannel, position, count);
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }

}

方法的輸入參數position表示從position處開始向目標文件寫入數據,count表示最多傳輸的字節數。若是源通道的剩餘空間小於 count 個字節,則所傳輸的字節數要小於請求的字節數。
此外要注意,在SoketChannel的實現中,SocketChannel只會傳輸此刻準備好的數據(可能不足count字節)。所以,SocketChannel可能不會將請求的全部數據(count個字節)所有傳輸到FileChannel中。

transferTo()

transferTo()方法將數據從FileChannel傳輸到其餘的channel中。下面是一個簡單的例子:

package com.dxz.springsession.nio.demo1;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

public class FileChannelTest3 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("d:\\soft\\fromFile.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();
        
        RandomAccessFile bFile = new RandomAccessFile("d:\\soft\\toFile.txt", "rw");
        FileChannel toChannel = bFile.getChannel();
        long position = 0;
        long count = fromChannel.size();
        fromChannel.transferTo(position, count, toChannel);
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }

}

是否是發現這個例子和前面那個例子特別類似?除了調用方法的FileChannel對象不同外,其餘的都同樣。上面所說的關於SocketChannel的問題在transferTo()方法中一樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿。

相關文章
相關標籤/搜索