Channel 類(通道)
- Channel(通道):Channel 是一個對象,可以通過它讀取和寫入數據。可以把它看做是 IO 中的流,不同的是:
- 爲所有的原始類型提供(Buffer)緩存支持;
- 字符集編碼解決方案(Charset);
- Channel : 一個新的原始 I/O 抽象;
- 支持鎖和內存映射文件的文件訪問接口;
- 提供多路(non-bloking)非阻塞式的高伸縮性網路 I/O;
- 正如上面提到的,所有數據都通過 Buffer 對象處理,所以,你永遠不會將字節直接寫入到 Channel 中,相反,您是將數據寫入到 Buffer 中;同樣,您也不會從 Channel 中讀取字節,而是將數據從 Channel 讀入 Buffer,再從 Buffer 獲取這個字節;
- 因爲 Channel 是雙向的,所以 Channel 可以比流更好地反映出底層操作系統的真實情況。特別是在 Unix 模型中,底層操作系統通常都是雙向的;
- 在 Java NIO 中的 Channel 主要有如下幾種類型:
- FileChannel:從文件讀取數據;
- DatagramChannel:讀寫 UDP 網絡協議數據;
- SocketChannel:讀寫 TCP 網絡協議數據;
- ServerSocketChannel:可以監聽 TCP 連接;
1. FileChannel 類的基本使用
- java.nio.channels.FileChannel:是用於讀、寫文件的通道;
- FileChannel 是抽象類,可以通過 FileInputStream 和 FileOutputStream 的 getChannel() 方法方便的獲取一個它的子類對象;
FileInputStream fi = new FileInputStream(new File(src));
FileOutputStream fo = new FileOutputStream(new File(dst));
FileChannel inChannel = fi.getChannel();
FileChannel outChannel = fo.getChannel();
- 通過 CopyFile 的例子可以更好體會 NIO 的操作過程。CopyFile 執行三個基本的操作:創建一個 Buffer,然後從源文件讀取數據到緩衝區,然後再將緩衝區寫入目標文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class Test {
public static void main(String[] args) throws Exception {
FileInputStream fi = new FileInputStream("C:\\Users\\80626\\Desktop\\1.png");
FileOutputStream fo = new FileOutputStream("C:\\Users\\80626\\Desktop\\1_copy.png");
FileChannel inChannel = fi.getChannel();
FileChannel outChannel = fo.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int eof = 0;
while ((eof = inChannel.read(buffer)) != -1) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
inChannel.close();
outChannel.close();
fi.close();
fo.close();
}
}
2. FileChannel 結合 MappedByteBuffer 實現高效讀寫
- 上例直接使用 FileChannel 結合 ByteBuffer 實現的管道讀寫,但並不能提高文件的讀寫效率,ByteBuffer 有個子類:MappedByteBuffer,它可以創建一個“直接緩衝區”,並可以將文件直接映射至內存,可以提高大文件的讀寫效率;
- ByteBuffer 是抽象類,MappedByteBuffer 也是抽象類;
- 可以調用 FileChannel 的 map() 方法獲取一個 MappedByteBuffer,map() 方法的原型:
MappedByteBuffer map(MapMode mode, long position, long size);
:將節點中從 position 開始的 size 個字節映射到返回的 MappedByteBuffer 中;
a. 複製 2 GB 以下的文件
- 此例不能複製大於 2 G 的文件,因爲 map 的第三個參數被限制在
Integer.MAX_VALUE(字節) = 2G
;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class Test {
public static void main(String[] args) throws Exception {
try {
RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
FileChannel in = source.getChannel();
FileChannel out = target.getChannel();
long size = in.size();
MappedByteBuffer mbbi = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
MappedByteBuffer mbbo = out.map(FileChannel.MapMode.READ_WRITE, 0, size);
long start = System.currentTimeMillis();
System.out.println("開始...");
for (int i = 0; i < size; i++) {
byte b = mbbi.get(i);
mbbo.put(i, b);
}
long end = System.currentTimeMillis();
System.out.println("用時: " + (end - start) + " 毫秒");
source.close();
target.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- map() 方法的第一個參數 mode:映射的三種模式,在這三種模式下得到的將是三種不同的 MappedByteBuffer:三種模式都是 Channel 的內部類 MapMode 中定義的靜態常量,這裏以 FileChannel 舉例:
(1)FileChannel.MapMode.READ_ONLY:得到的鏡像只能讀不能寫(只能使用 get 之類的讀取 Buffer 中的內容);
(2)FileChannel.MapMode.READ_WRITE:得到的鏡像可讀可寫(既然可寫了必然可讀),對其寫會直接更改到存儲節點;
(3)FileChannel.MapMode.PRIVATE:得到一個私有的鏡像,其實就是一個 (position, size) 區域的副本罷了,也是可讀可寫,只不過寫不會影響到存儲節點,就是一個普通的 ByteBuffer 了;
- 爲什麼使用 RandomAccessFile?
(1) 使用 InputStream 獲得的 Channel 可以映射,使用 map 時只能指定爲 READ_ONLY 模式,不能指定爲 READ_WRITE 和 PRIVATE,否則會拋出運行時異常;
(2) 使用 OutputStream 得到的 Channel 不可以映射!並且 OutputStream 的 Channel 也只能 write不能 read;
(3) 只有 RandomAccessFile 獲取的 Channel 才能開啓任意的這三種模式;
b. 複製 2 GB 以上的文件
- 下例使用循環,將文件分塊,可以高效的複製大於 2 G 的文件;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class Test {
public static void main(String[] args) throws Exception {
try {
RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
FileChannel in = source.getChannel();
FileChannel out = target.getChannel();
long size = in.size();
long count = 1;
long copySize = size;
long everySize = 1024 * 1024 * 512;
if (size > everySize) {
count = (int) (size % everySize != 0 ? size / everySize + 1 : size / everySize);
copySize = everySize;
}
MappedByteBuffer mbbi = null;
MappedByteBuffer mbbo = null;
long startIndex = 0;
long start = System.currentTimeMillis();
System.out.println("開始...");
for (int i = 0; i < count; i++) {
mbbi = in.map(FileChannel.MapMode.READ_ONLY, startIndex, copySize);
mbbo = out.map(FileChannel.MapMode.READ_WRITE, startIndex, copySize);
for (int j = 0; j < copySize; j++) {
byte b = mbbi.get(i);
mbbo.put(i, b);
}
startIndex += copySize;
copySize = in.size() - startIndex > everySize ? everySize : in.size() - startIndex;
}
long end = System.currentTimeMillis();
source.close();
target.close();
System.out.println("用時: " + (end - start) + " 毫秒");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. ServerSocketChannel 和 SocketChannel 創建連接
a. 服務器端 ServerSocketChannel
- 該類用於連接的服務器端,它相當於:ServerSocket;(1) 調用 ServerSocketChannel 的靜態方法 open():
ServerSocketChannel serverChannel = ServerSocketChannel.open();
,打開一個通道,新頻道的套接字最初未綁定;必須通過其套接字的 bind 方法將其綁定到特定地址,才能接受連接;
(2) 調用 ServerSocketChannel 的實例方法 bind(SocketAddress add):serverChannel.bind(new InetSocketAddress(8888));
,綁定本機監聽端口,準備接受連接。注意,java.net.SocketAddress 是一個抽象類,代表一個 Socket 地址,可以使用它的子類:java.net.InetSocketAddress 類,其構造方法 InetSocketAddress(int port)
可以指定本機監聽端口;
(3) 調用 ServerSocketChannel 的實例方法 accept():SocketChannel accept = serverChannel.accept(); System.out.println("後續代碼...");
,等待連接;
i. 示例:服務器端等待連接(默認-阻塞模式)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Test {
public static void main(String[] args) {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("【服務器】等待客戶端連接...");
SocketChannel accept = serverChannel.accept();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ii. 通過 ServerSocketChannel 的 configureBlocking(boolean b) 方法設置 accept() 是否阻塞
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Test {
public static void main(String[] args) throws Exception {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("【服務器】等待客戶端連接...");
serverChannel.configureBlocking(false);
SocketChannel accept = serverChannel.accept();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 可以看到,accept() 方法並沒有阻塞,而是直接執行後續代碼,返回值爲 null;
- 這種非阻塞的方式,通常用於"客戶端"先啓動,"服務器端"後啓動,來查看是否有客戶端連接,有,則接受連接;沒有,則繼續工作;
b. 客戶端 SocketChannel
- 該類用於連接的客戶端,它相當於:Socket;
(1) 先調用 SocketChannel 的 open() 方法打開通道:ServerSocketChannel serverChannel = ServerSocketChannel.open();
;
(2) 調用 SocketChannel 的實例方法 connect(SocketAddress add) 連接服務器:socket.connect(new InetSocketAddress("localhost", 8888));
;
i. 示例:客戶端連接服務器
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
public class Test {
public static void main(String[] args) {
try (SocketChannel socket = SocketChannel.open()) {
socket.connect(new InetSocketAddress("localhost", 8888));
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客戶端完畢!");
}
}
4. ServerSocketChannel 和 SocketChannel 收發信息
a. 創建服務器端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Server {
public static void main(String[] args) {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress("localhost", 8888));
System.out.println("【服務器】等待客戶端連接...");
SocketChannel accept = serverChannel.accept();
System.out.println("【服務器】有連接到達...");
ByteBuffer outBuffer = ByteBuffer.allocate(100);
outBuffer.put("你好客戶端,我是服務器".getBytes());
outBuffer.flip();
accept.write(outBuffer);
ByteBuffer inBuffer = ByteBuffer.allocate(100);
accept.read(inBuffer);
inBuffer.flip();
String msg = new String(inBuffer.array(), 0, inBuffer.limit());
System.out.println("【服務器】收到信息:" + msg);
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
b. 創建客戶端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) {
try (SocketChannel socket = SocketChannel.open()) {
socket.connect(new InetSocketAddress("localhost", 8888));
ByteBuffer buf = ByteBuffer.allocate(100);
buf.put("你好服務器,我是客戶端".getBytes());
buf.flip();
socket.write(buf);
ByteBuffer inBuffer = ByteBuffer.allocate(100);
socket.read(inBuffer);
inBuffer.flip();
String msg = new String(inBuffer.array(), 0, inBuffer.limit());
System.out.println("【客戶端】收到信息:" + msg);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客戶端完畢!");
}
}
c. 效果展示
- 服務器端打印結果:
- 客戶端打印結果: