1. 程式人生 > >通道(Channel)

通道(Channel)

nbsp pipes 最簡 分別是 傳輸 close 同時 開發 底層

層次結構圖

技術分享圖片

從上圖可以看出,Channel是所有類的父類,它定義了通道的基本操作。從Channel引申出的其他接口都是面向字節的子接口,這也意味著通道只能在字節緩沖區(ByteBuffer)上操作。

Channel和Buffer

Channel和Buffer之間的關系,如下圖所示:

技術分享圖片

Channel中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。

通道基礎

  先來看一下基本的Channel接口,下面代碼是Channel接口的完整源碼:

 1 public interface Channel extends Closeable {
 2 
 3     /**
 4
* Tells whether or not this channel is open. </p> 5 * 6 * @return <tt>true</tt> if, and only if, this channel is open 7 */ 8 public boolean isOpen(); 9 10 /** 11 * Closes this channel. 12 * 13 * <p> After a channel is closed, any further attempt to invoke I/O
14 * operations upon it will cause a {@link ClosedChannelException} to be 15 * thrown. 16 * 17 * <p> If this channel is already closed then invoking this method has no 18 * effect. 19 * 20 * <p> This method may be invoked at any time. If some other thread has
21 * already invoked it, however, then another invocation will block until 22 * the first invocation is complete, after which it will return without 23 * effect. </p> 24 * 25 * @throws IOException If an I/O error occurs 26 */ 27 public void close() throws IOException; 28 29 }

  和緩沖區不同,Channel的API主要由接口來指定。不同的操作系統上通道的實現會有根本性的差異,所以通道API僅僅描述了可以做什麽,因此很自然的,通道實現經常使用操作系統的本地代碼,通道接口允許開發者以一種受控且可移植的方式來訪問底層的I/O服務。從上面最基礎的源代碼可以看出,所有的通道只有兩種共同的操作:檢查一個通道是否打開 isOpen() 方法和關閉一個打開了的通道 close()方法,其余所有的東西都是那些實現Channel接口以及它的子接口的類。

  從最基礎的Channel引申出的其他接口都是面向字節的子接口:在Channel的眾多實現中,有一個SelectableChannel實現,其表示可被選擇的通道。任何一個SelectableChannel都可以將自己註冊到一個Selector中,這樣,這個Channel就能被Selector(如果對Selector不了解,可看文章I/O 模型中Selector部分)所管理,而一個Selector可以管理多個SelectableChannel。當這個SelectableChannel的數據準備好時,Selector就會接到通知,去獲取那些準備好的數據。而SocketChannel就是SelectableChannel的一種。

  同時,通道只能在字節緩沖區上操作。層次接口表明其他數據類型的通道也可以從Channel接口引申而來。這是一種很好的映射,不過非字節實現是不可能的,因為操作系統都是以字節的形式實現底層I/O接口的。

Channel的主要實現

  FileChannel:用於讀取、寫入、映射和操作文件的通道。

  DatagramChannel:通過UDP讀寫網絡中的數據通道。

  SocketChannel:通過tcp讀寫網絡中的數據。

  ServerSocketChannel:可以監聽新進來的tcp連接,對每一個連接都創建一個SocketChannel。

獲取通道的方式

  (1)通過getChannel()方法獲取;

    前提是該類支持該方法。支持該類的方法有:FileInputStream、FileOutputStream、RandomAccessFile、Socket、ServerSocket、DatagramSocket。

  (2)通過靜態方法open();

  (3)通過JDK1.7中的Files的newByteChannel()方法;

Scatter(分散)/Gather(聚集)

  ? 分散:從Channel中讀取是指在讀操作時將讀取的數據寫入多個Buffer中,因此,Channel將從Channel中讀取的數據分散到多個Buffer中;

  ? 聚集:指將數據寫入到Channel中時將多個Buffer的數據寫入同一個Channel,因此,Channel將多個Buffer中的數據聚集後發送到Channel。

下面例子是分散:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿後,channel緊接著向另一個buffer中寫。

認識了解Channel

看一下基本的Channel接口:

ByteChannel:

1 public interface ByteChannel
2     extends ReadableByteChannel, WritableByteChannel
3 {
4 
5 }

WritableByteChannel:

public interface WritableByteChannel
    extends Channel
{
    public int write(ByteBuffer src) throws IOException;

}

ReadableByteChannel :

public interface ReadableByteChannel extends Channel {
    public int read(ByteBuffer dst) throws IOException;

}

  通道可以是單向的也可以是雙向的。一個Channel類可能只實現了定義read() 方法的 ReadableByteChannel接口,而另一個Channel類也許只是實現了定義write() 方法的 WritableChannel接口,那麽實現這兩種接口之一的類都是單向的,就只能在一個方向上傳輸數據。如果一個類同時實現了這兩個接口,那麽這個類它就是雙向的,可以進行雙向的傳輸數據,就像上面的ByteChannel。

  通道不僅可以單向雙向,也可以是阻塞和非阻塞的,非阻塞模式的通道永遠不會讓調用的線程休眠,請求的操作要麽立即完成,要麽返回一個結果表明未進行任何操作。只有面向流(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式(例如:從SelectableChannel引申而來的類可以和支持選擇的選擇器(Selector)一起使用)。

文件通道

由於我們在開發中文件I/O用到的地方比較多,所以對於文件通道必須要詳細了解。

  通道是訪問I/O服務的導管,I/O可以廣義的分為兩大類:File I/O和Stream I/O。那麽相應的,通道也可以廣義上的分外兩種類型,分別是文件(File)Channel和套接字(Socket)通道。文件通道指的是 FileChannel,套接字通道則有三個,分別是SocketChannel、ServerSocketChannel和DatagramChannel。

  通道可以通過多種方式創建。Socket通道可以通過Socket通道的工廠方法直接創建,但是一個FileChannel對象卻只能通過一個打開的RandomAccessFile、FileInputStream或FileOutputStream對象上調用getChannel()方法來獲取,開發者不能直接創建一個FileChannel。

接下來通過UML圖來了解一下文件通道的類層次關系:

技術分享圖片

文件通道總是阻塞的,因此不能被置於非阻塞模式下。

  前面提到過,FileChannel對象不能直接創建,一個FileChannel實例只能通過一個打開的File對象(RandomAccessFile、FileInputStream或FileOutputStream)上調用getChannel()方法獲取,通過調用getChannel()方法會返回一個連接到相同文件的FileChannel對象且該FileChannel對象具有與File對象相同的訪問權限,然後就可以使用通道對象來利用強大的FileChannel API了。

  FileChannel對象是線程安全的,多個線程可以在同一個實例上並發調用方法而不會引起任何問題,不過並非所有操作都是多線程的。影響通道位置或者影響文件的操作都是單線程的,如果有一個線程已經在執行會影響通道位置或文件大小的操作,那麽其他想嘗試進行此類操作之一的線程必須等待,並發行為也會受到底層操作系統或者文件系統的影響。

使用文化通道

  下面通過使用文件通道,讀取文件中的數據:

 1 public static void main(String[] args) throws Exception{
 2         File file = new File("D:/ceshi.txt");
 3         FileInputStream fis = new FileInputStream(file);
 4         FileChannel fc = fis.getChannel();
 5         ByteBuffer bb = ByteBuffer.allocate(35);
 6         fc.read(bb);
 7         bb.flip();
 8         while (bb.hasRemaining())
 9         {
10             System.out.print((char)bb.get());
11         }
12         bb.clear();
13         fc.close();
14     }

輸出結果

Hello !
    FileChannel.

這是最簡單的操作,前面講過文件通道必須通過一個打開的RandomAccessFile、FileInputStream、FileOutputStream獲取到,因此這裏使用FileInputStream來獲取FileChannel。接著只要使用read方法將內容讀取到緩沖區內即可,緩沖區內有了數據,就可以使用前文對於緩沖區的操作讀取數據了。

接著看一下使用文件通道寫數據:

 1 public static void main(String[] args) throws Exception{
 2         File file = new File("D:/ceshi.txt");
 3         RandomAccessFile raf = new RandomAccessFile(file, "rw");
 4         FileChannel fc = raf.getChannel();
 5         ByteBuffer bb = ByteBuffer.allocate(60);
 6         String str = "Hello,FileChannel!";
 7         bb.put(str.getBytes());
 8         bb.flip();
 9         fc.write(bb);
10         bb.clear();
11         fc.close();
12     }

輸出結果

技術分享圖片

這裏使用了RandomAccessFile去獲取FileChannel,然後操作其實差不多,write方法寫ByteBuffer中的內容至文件中,註意寫之前還是要先把ByteBuffer給flip一下。

可能有人覺得這種連續put的方法非常不方便,但是沒有辦法,之前已經提到過了:通道只能使用ByteBuffer

參考:https://www.cnblogs.com/xrq730/p/5080503.html

通道(Channel)