通道(Channel)
層次結構圖
從上圖可以看出,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/O14 * 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 has21 * 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)