NIO複習(2):channel

  • 2020 年 2 月 25 日
  • 筆記

上篇學習了NIO的buffer,繼續來學習channel,類圖如下(註:為了不讓圖看起來太複雜,隱藏了一些中間的介面)

Channel派生了很多子介面,其中最常用的有FileChannel(用於文件操作)以及SocketChannel、ServerSocketChannel(用於網路通訊),下面用幾段示例程式碼學習其基本用法:

一、文件寫入

1.1 入門示例

public static void fileWriteReadSimpleDemo() throws IOException {      String filePath = "/tmp/yjmyzz.txt";      //文件寫入      String fileContent = "菩提樹下的楊過";      FileOutputStream outputStream = new FileOutputStream(filePath);      FileChannel writeChannel = outputStream.getChannel();        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);      byteBuffer.put(fileContent.getBytes());      byteBuffer.flip();//別忘記了,反轉position,否則此時position已經移到最後1個有效字元處,下一行將讀不到數據        //緩衝區的數據,通過channel寫入文件      writeChannel.write(byteBuffer);        writeChannel.close();        //文件讀取      File file = new File(filePath);      FileInputStream inputStream = new FileInputStream(file);      FileChannel readChannel = inputStream.getChannel();        //註:這裡要重新指定實際大小,否則byteBuffer前面初始化成1024長度,文件內容不足1024位元組,      // 後面的空餘部分全是默認0填充,最終轉換成字元串時,填充的0,也會轉換成不可見字元輸出      byteBuffer = ByteBuffer.allocate((int) file.length());      readChannel.read(byteBuffer);      System.out.println(new String(byteBuffer.array()));      readChannel.close();  }

FileOutputStream類中內嵌了一個FileChannel的實例,通過getChannel()方法可以獲取引用。寫文件緩衝區初始化時,如何設置正確的大小,這個不太好掌握,設置太大浪費記憶體,設置太小又裝不下,正確姿勢可參考下面的示例2

1.2 緩衝區不夠大時循環寫入

    public static void writeFileDemo() throws IOException {          String fileContent = "菩提樹下的楊過(http://yjmyzz.cnblogs.com/)nn" +                  "送柴侍御n" +                  "【作者】王昌齡 【朝代】唐n" +                  "沅水通波接武岡,送君不覺有離傷。n" +                  "青山一道同雲雨,明月何曾是兩鄉。n";          //故意設置一個很小的緩衝區,演示緩衝區不夠大的情況          ByteBuffer byteBuffer = ByteBuffer.allocate(5);          String filePath = "/tmp/yjmyzz.txt";          FileOutputStream outputStream = new FileOutputStream(filePath);          FileChannel writeChannel = outputStream.getChannel();            //將文件內容,按緩衝區大小拆分成一段段寫入          byte[] src = fileContent.getBytes();          int pages = (src.length % byteBuffer.capacity() == 0) ? (src.length / byteBuffer.capacity())                                                                  : (src.length / byteBuffer.capacity() + 1);          for (int i = 0; i < pages; i++) {              int start = i * byteBuffer.capacity();              int end = Math.min(start + byteBuffer.capacity() - 1, src.length - 1);              for (int j = start; j <= end; j++) {                  byteBuffer.put(src[j]);              }              byteBuffer.flip();              writeChannel.write(byteBuffer);              //記得清空              byteBuffer.clear();          }          writeChannel.close();      }

注意:文件讀取時,直接通過File對象的length可以提前知道緩衝的大小,能精確指定Buffer大小,不需要類似這麼複雜的循環處理。

二、文件複製

    public static void copyFileDemo() throws IOException {          String srcFilePath = "/tmp/yjmyzz.txt";          File srcFile = new File(srcFilePath);          String targetFilePath = "/tmp/yjmyzz.txt.bak";          FileInputStream inputStream = new FileInputStream(srcFile);          FileOutputStream outputStream = new FileOutputStream(targetFilePath);            FileChannel inputChannel = inputStream.getChannel();          FileChannel outputChannel = outputStream.getChannel();            //文件複製          ByteBuffer buffer = ByteBuffer.allocate((int) srcFile.length());          inputChannel.read(buffer);          buffer.flip();          outputChannel.write(buffer);            //也可以用這一行,搞定文件複製(推薦使用)  //        outputChannel.transferFrom(inputChannel, 0, srcFile.length());            inputChannel.close();          outputChannel.close();      }

三、文件修改

場景:某個文件需要把最後1個漢字,修改成其它字。先寫一段程式碼,生成測試用的文件

    public static void writeLargeFile() throws IOException {          String content = "12345678-abcdefg-菩提樹下的楊過n";          String filePath = "/tmp/yjmyzz.txt";          FileOutputStream outputStream = new FileOutputStream(filePath);          FileChannel writeChannel = outputStream.getChannel();            ByteBuffer buffer = ByteBuffer.allocate(128);          buffer.put(content.getBytes());          for (int i = 0; i < 10; i++) {              buffer.flip();              writeChannel.write(buffer);          }          writeChannel.close();      }

運行完後,測試文件中的內容如下:

12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過  12345678-abcdefg-菩提樹下的楊過

3.1 常規方法示例

    public static void modify1() throws IOException {          String filePath = "/tmp/yjmyzz.txt";          File file = new File(filePath);          FileInputStream inputStream = new FileInputStream(file);          FileChannel inputChannel = inputStream.getChannel();          ByteBuffer buffer = ByteBuffer.allocate((int) file.length());            byte[] tempBytes = "佛".getBytes();          inputChannel.read(buffer);          buffer.flip();          //修改最後1個漢字          for (int i = 0; i < tempBytes.length; i++) {              //最後有一個回車符,然後漢字utf-8佔3個位元組,所以這裡要減4,才是最後1個漢字              int j = buffer.limit() - 4 + i;              buffer.put(j, tempBytes[i]);          }            FileOutputStream outputStream = new FileOutputStream(filePath);          FileChannel outputChannel = outputStream.getChannel();          outputChannel.write(buffer);            inputChannel.close();          outputChannel.close();      }

運行完後,從下面的截圖可以看到,測試最後1個字,從「過」變成了「佛」:

這個方法,對於小文件而言沒什麼問題,但如果文件是一個幾G的巨無霸,會遇到2個問題:

首先是allocate方法,只接受int型參數,對於幾個G的大文件,File.length很有可能超過int範圍,無法分配足夠大的緩衝。其次,就算放得下,幾個G的內容全放到記憶體中,也很可能造成OOM,所以需要其它辦法。

3.2 利用RandomAccessFile及Channel.map修改文件

    public static void modify2() throws IOException {          String filePath = "/tmp/yjmyzz.txt";          RandomAccessFile file = new RandomAccessFile(filePath, "rw");          FileChannel channel = file.getChannel();          //將最後一個漢字映射到記憶體中          MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, file.length() - 4, 3);            byte[] lastWordBytes = "新".getBytes();            //這樣就直接在記憶體中修改了文件,不再需要調用channel.write          mappedByteBuffer.put(lastWordBytes);            channel.close();      }

這個方法相對就高級多了,RandomAccessFile類是File類的加強版,允許以游標的方式,直接讀取文件的某一部分,另外Channel.map方法,可以直接將文件中的某一部分映射到記憶體,在記憶體中直接修MappedByteBuffer後,文件內容就相應的修改了。

值得一提的是,從上面調試的截圖來看,FileChannel.map方法返回的MappedByteBuffer,真實類型是它下面派生的子類DirectByteBuffer,這是「堆外」記憶體,不在JVM 自動垃圾回收的管轄範圍。

參考文章:

https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/channels/Channel.html

http://ifeve.com/channels/