目錄:
《Java NIO系列教程(三) Channel之Socket通道》
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中寫入。
正如上面所說,從通道讀取數據到緩沖區,從緩沖區寫入數據到通道。如下圖所示:

Channel的實現

這些是Java NIO中最重要的通道的實現:
- FileChannel:從文件中讀寫數據
- DatagramChannel:通過UDP讀寫網絡中的數據
- SocketChannel:通過TCP讀寫網絡中的數據
- ServerSocketChannel:可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。
正如你所看到的,這些通道涵蓋了UDP 和 TCP 網絡IO,以及文件IO。
FileChannel
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的更多細節。
1、打開FileChannel
在使用FileChannel之前,必須先打開它。但是,我們無法直接打開一個FileChannel,需要通過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是通過RandomAccessFile打開FileChannel的示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
2、從FileChannel讀取數據
調用多個read()方法之一從FileChannel中讀取數據。如:
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);
首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。
然后,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。如果返回-1,表示到了文件末尾。
3、向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中已經沒有尚未寫入通道的字節。
4、關閉FileChannel
用完FileChannel后必須將其關閉。如:
channel.close();
5、FileChannel的position方法
有時可能需要在FileChannel的某個特定位置進行數據的讀/寫操作。可以通過調用position()方法獲取FileChannel的當前位置。
也可以通過調用position(long pos)方法設置FileChannel的當前位置。
這里有兩個例子:
long pos = channel.position(); channel.position(pos +123);
如果將位置設置在文件結束符之后,然后試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標志。
如果將位置設置在文件結束符之后,然后向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能導致“文件空洞”,磁盤上物理文件中寫入的數據間有空隙。
6、FileChannel的size方法
FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:
long fileSize = channel.size();
7、FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度后面的部分將被刪除。如:
channel.truncate(1024);
這個例子截取文件的前1024個字節。
8、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(); } }
9、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被填滿。

