Java NIO Channel 使用

detectiveHLH發表於2022-01-12

Java NIO 中的 Channel 分類:

  • FileChannel
  • SocketChannel
  • ServerSocketChannel
  • DatagramChannel

channel 分類

FileChannel: 主要用於檔案的讀寫,可以從磁碟上讀取檔案,也可以向磁碟上寫入檔案。

SocketChannel:用於 Socket 的 TCP 連線的資料讀寫,既可以從 Channel 讀資料,也可以向 Channle 中寫入資料

ServerSocketChannel:通過 ServerSocketChannel 可以監聽 TCP 連線,服務端監聽到連線之後,會為每個請求建立一個 SocketChannel

DatagramChannel:用於 UDP 協議的資料讀寫

接下來就分別介紹一下。

FileChannel

主要用於操作檔案,廢話不多說,直接看例子。

準備檔案 test-file.txt ,內容 shDEQuanZhanBiJi

test-file.txt 檔案

輸入 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 的時候預設是處於寫模式的,此時如果去通過 positionlimit 去讀取資料是讀不到的。所以在呼叫 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

不能這麼玩,因為它就是一個單純的字串匹配,可供選擇的就這麼些:

mode 型別

可以看到,r 必不可少...:

  • r 只能讀
  • rw 既能,也能
  • rwsrwd 功能和 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的全棧筆記】,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章