Java NIO 中的 Channel 分類:
- FileChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
FileChannel: 主要用於文件的讀寫,可以從磁盤上讀取文件,也可以向磁盤上寫入文件。
SocketChannel:用於 Socket 的 TCP 連接的數據讀寫,既可以從 Channel 讀數據,也可以向 Channle 中寫入數據
ServerSocketChannel:通過 ServerSocketChannel 可以監聽 TCP 連接,服務端監聽到連接之後,會爲每個請求創建一個 SocketChannel
DatagramChannel:用於 UDP 協議的數據讀寫
接下來就分別介紹一下。
FileChannel
主要用於操作文件,廢話不多說,直接看例子。
準備文件
test-file.txt
,內容shDEQuanZhanBiJi
輸入 FileInputStream
用於從 FileChannel 中讀取數據,例如將指定文件輸入到 FileChannel 中,我們就能獲取到文件的內容,接下來編寫 FileChannel 的 輸入流 核心代碼:
public static void main(String[] args) throws IOException {
// 創建一個輸入流
FileInputStream fileInputStream = new FileInputStream("test-file.txt");
// 通過輸入流獲取到 channel
FileChannel fileChannel = fileInputStream.getChannel();
// 準備好 ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
// 將 輸入流 的 channel 的數據讀入 buffer 中
fileChannel.read(buffer);
// 簡單打印 buffer 的內容
printBuffer(buffer); // shDEQuanZhanBiJi
}
這裏面的 ByteBuffer 是 channel 進行讀、寫數據的中間媒介。要從 channel 中讀取數據(也就是上面這個例子),需要先將數據讀到 ByteBuffer 中;同理,要想向 channel 中寫入數據,也需要先將數據寫入 ByteBuffer(下面講輸出流的時候會講)。
對 ByteBuffer 不熟悉的可以先看看我之前寫的《玩轉 ByteBuffer》,
printBuffer
的代碼裏面也有
輸出 FileOutputStream
顧名思義,是 FileChannel 要向外輸出數據,例如將數據寫入到磁盤文件上,接下來通過例子看看效果:
public static void main(String[] args) throws IOException {
// 指定需要生成的文件名稱
String generateFileName = "generate-file.txt";
// 創建一個輸出流
FileOutputStream fileOutputStream = new FileOutputStream(generateFileName);
// 通過輸出流獲取到 channel
FileChannel fileChannel = fileOutputStream.getChannel();
// 準備好 ByteBuffer, 並向裏面寫入數據
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("shDEQuanZhanBiJi".getBytes(StandardCharsets.UTF_8));
// 將 輸入流 的 channel 的數據讀入 buffer 中
fileChannel.write(buffer);
fileChannel.close();
}
相應的註釋都已經貼在對應的代碼上了,細節在此不再贅述。唯一需要關注的是,調用 write
寫文件到磁盤上時,也是先傳入的 ByteBuffer。
好了,當你運行完代碼你會發現,雖然文件是生成的了,但是裏面卻是空白的...這其實就涉及到對 ByteBuffer 的熟悉程度了,算是埋的一個坑。
如果不知道爲啥文件是空的,可以去看看上面講 ByteBuffer 的文章,接下來是解答。
這是因爲我們創建一個 ByteBuffer 的時候默認是處於寫模式的,此時如果去通過 position 和 limit 去讀取數據是讀不到的。所以在調用 write
之前,我們需要先將 ByteBuffer 切換到讀模式,完整代碼如下:
public static void main(String[] args) throws IOException {
// 指定需要生成的文件名稱
String generateFileName = "generate-file.txt";
// 創建一個輸出流
FileOutputStream fileOutputStream = new FileOutputStream(generateFileName);
// 通過輸出流獲取到 channel
FileChannel fileChannel = fileOutputStream.getChannel();
// 準備好 ByteBuffer, 並向裏面寫入數據
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("shDEQuanZhanBiJi".getBytes(StandardCharsets.UTF_8));
// 將 ByteBuffer 切換到讀模式
buffer.flip();
// 將 輸入流 的 channel 的數據讀入 buffer 中
fileChannel.write(buffer);
fileChannel.close();
}
可以看到,文件生成了,內容也有了:
但是呢,上面將的兩種要麼只能寫,要麼只能讀。例如 FileInputStream
如果你硬要往 channel 裏懟數據,程序最後會拋出 NonWritableChannelException
異常,告訴你這玩意兒寫不了。
那有沒有一個既能寫,又能讀還能唱跳的實現呢?當然有,那就是 RandomAccessFile
。
這裏提一嘴,調用完 write 並不是立即就寫入磁盤,也可以在操作系統的緩存裏。如果需要立即刷盤,則調用
channel.force(true);
即可。
RandomAccessFile
怎麼用的呢?其實跟之前兩個差不多:
public static void main(String[] args) throws IOException {
// 指定需要生成的文件名稱
String targetFileName = "target-file.txt";
// 創建 RandomAccessFile, 賦予可讀(r)、可寫(w)的權限
RandomAccessFile accessFile = new RandomAccessFile(targetFileName, "rw");
FileChannel fileChannel = accessFile.getChannel();
// 創建 ByteBuffer 並寫入數據
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("shDEQuanZhanBiJi".getBytes(StandardCharsets.UTF_8));
// 切換到 buffer 的讀模式
buffer.flip();
// 調用 write 將 buffer 的數據寫入到 channel, channel 再寫數據到磁盤文件
fileChannel.write(buffer);
// 相當於清空 buffer
buffer.clear();
// 將之前寫入到 channel 的數據再讀入到 buffer
fileChannel.read(buffer);
// 打印 buffer 中的內容
printBuffer(buffer);
fileChannel.close();
}
運行之後的效果就是,會生成一個名爲 target-file.txt
的文件,內容就是 shDEQuanZhanBiJi
。並且控制檯會將之前寫入 channel 的 shDEQuanZhanBiJi
打印出來。
老規矩,細節都在註釋中。值得注意的是 new RandomAccessFile(targetFileName, "rw");
裏的 rw
。註釋裏也寫了,代表賦予可讀、可寫的權限。
再值得注意的是,你不能說把 rw
改成 w
。
不能這麼玩,因爲它就是一個單純的字符串匹配,可供選擇的就這麼些:
可以看到,r
必不可少...:
r
只能讀rw
既能讀,也能寫rws
和rwd
功能和rw
大致是相同的,可讀、可寫。唯一區別是他們會將每次改動強制刷到磁盤,並且rws
會將操作系統對該文件的元數據也一起刷盤,體現就是文件的更新時間會更新,而rwd
不會將文件的元數據刷盤
兩個 SocketChannel
由於這倆一個負責連接傳輸,另一個負責連接的監聽,所以就放在一起來講了。這一小節我們大概要做這件事:
但是爲了能讓大家直接運行起來,客戶端這側就不從磁盤文件讀取了,直接用 ByteBuffer。大家可以運行起來之後,自己嘗試從磁盤上去加載。還是先看代碼,首先是服務器的:
ServerSocketChannel
public static void main(String[] args) throws IOException {
// 打開一個 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 綁定 8080 端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 開始接受客戶端連接
SocketChannel socketChannel = serverSocketChannel.accept();
// 獲取連接成功
System.out.printf("socketChannel %s connected\n", socketChannel);
// 準備 ByteBuffer 以從 socketChannel 中讀取數據
ByteBuffer buffer = ByteBuffer.allocate(16);
// 開始讀取數據
System.out.println("before read");
int read = socketChannel.read(buffer);
System.out.printf("read complete, read bytes length: %s \n", read);
printBuffer(buffer);
}
這裏我們使用的是 Java NIO 中默認的阻塞模式,僅僅作爲一個掩飾,如果想要 ServerSocketChannel 進入非阻塞模式,可在 open
之後,調用:
serverSocketChannel.configureBlocking(false);
由於我們這裏是阻塞模式,所以在代碼運行到 serverSocketChannel.accept();
時,會陷入阻塞狀態,直到有客戶端過來建立連接。同理,read
方法也是阻塞的,如果客戶端一直沒有寫入數據,那麼服務器就會一直阻塞在 read
。
SocketChannel
直接先給代碼:
public static void main(String[] args) throws IOException {
// 打開一個 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
// 連接到 localhost 的 8080 端口
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 準備 ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put(Charset.defaultCharset().encode("test"));
// 將 buffer 切換成讀模式 & 向 channel 中寫入數據
buffer.flip();
socketChannel.write(buffer);
}
先啓動服務器,再啓動客戶端。可以看到服務器側的控制檯有如下的輸出:
socketChannel java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:64373] connected
before read
read complete, read bytes length: 4
BUFFER VALUE: test
Datagram
這個就比較簡單,首先是客戶端的代碼:
public static void main(String[] args) throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
// 構建 buffer 數據
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put(Charset.defaultCharset().encode("test"));
// 切換到 buffer 的讀模式
buffer.flip();
datagramChannel.send(buffer, new InetSocketAddress("localhost", 8080));
}
然後是服務器:
public static void main(String[] args) throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.bind(new InetSocketAddress(8080));
ByteBuffer buffer = ByteBuffer.allocate(16);
datagramChannel.receive(buffer);
printBuffer(buffer);
}
歡迎微信搜索關注【SH的全棧筆記】,如果你覺得這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言。