Google Code Prettify

2015年7月30日 星期四

NIO.2: TCP 網路程式設計 (blocking)

這一篇要說明的是,如何使用 JDK 7 推出的 NIO 2 進行 TCP 程式開發,底下的類別圖只是其中較重要的部份,要詳細了解各類別關係,及各類別的 method,請查閱 API Documents。


NIO 2 提供 blocking (阻斷)、non-blocking (非阻斷) 及 asynchronous (非同步) 三種模式,其中 asynchronous 模式是到 Java 7 才新提供的,我還沒有研究,先不說明。 用 blocking 模式和 non-blocking 模式來寫 server 的話,在一開始 blocking 會停在 accept 而 non-blocking 會停在 select 等待 client 的連線,看起來兩者使用的類別雖有不同,行為模式卻相同,但自此之後兩者就開始不一樣了。至於有什麼不一樣,待討論到 non-blocking 模式時會作說明,現在只要了解,blocking 模式的程式,如果 server 要同時服務多個 client,一個 channel 會有一個 thread,而 non-blocking 模式則是在一個 thread 中服務所有的 client。底下先來看一個 blocking 程式,很簡單的一個 echo 程式,client 傳送隨機的數字給 server,server 於字串前加上"回傳:"後傳回給 client,執行結果如下: (我總共執行了三次)
接下來看程式及說明:
  • client
 1 package idv.steven.sync;
 2 
 3 import java.io.IOException;
 4 import java.net.InetSocketAddress;
 5 import java.net.StandardSocketOptions;
 6 import java.nio.ByteBuffer;
 7 import java.nio.CharBuffer;
 8 import java.nio.channels.SocketChannel;
 9 import java.nio.charset.Charset;
10 import java.nio.charset.CharsetDecoder;
11 import java.util.Random;
12 
13 public class Client {
14 
15     public static void main(String[] args) {
16         final int DEFAULT_PORT = 5555;
17         final String IP = "127.0.0.1";
18 
19         ByteBuffer rcvBuffer = ByteBuffer.allocateDirect(1024);
20         Charset charset = Charset.forName("UTF-8"); // 網路傳輸時以 UTF-8 編碼
21         CharsetDecoder decoder = charset.newDecoder();
22         
23         // 建立 socket channel,在 NIO 2 裡,讀寫由 stream 改為 channel。
24         try (SocketChannel socketChannel = SocketChannel.open()) {
25             // 確認開啟 channel 成功
26             if (socketChannel.isOpen()) {
27                 // NIO 2 為 TCP 網路程式提供 blocking 和 non-blocking 兩種模式
28                 socketChannel.configureBlocking(true);
29                 // 設定 socket 的一些參數
30                 socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
31                 socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
32                     // 要求 JVM 保持 socket 連線,不過,要如何保持和 OS 有關,還是得 OS 決定。
33                 socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
34                     // 當 channel close 時,還有未送出的資料,要等待幾秒? 時間到仍強制關閉!
35                 socketChannel.setOption(StandardSocketOptions.SO_LINGER, 5);
36                 
37                 // 連線到 server socket
38                 socketChannel.connect(new InetSocketAddress(IP, DEFAULT_PORT));
39                 
40                 // 確認連線是否成功
41                 if (socketChannel.isConnected()) {
42                     Random random = new Random();
43                     ByteBuffer sendBuffer = ByteBuffer.wrap(String.valueOf(random.nextInt(100)).getBytes());
44                      socketChannel.write(sendBuffer);
45                     // socket 讀資料,不一定一次就可以完全讀完對方送來的資料,要讀到沒資料為止。
46                     while (socketChannel.read(rcvBuffer) != -1) {
47                         System.out.println("continue ...");
48                      }
49                      rcvBuffer.flip();
50                     CharBuffer charBuffer = decoder.decode(rcvBuffer);
51                      System.out.println(charBuffer);
52                 } else {
53                     System.out.println("連線失敗!");
54                 }
55             } else {
56                 System.out.println("socket channel 開啟失敗!");
57             }
58         } catch (IOException ex) {
59             System.err.println(ex);
60         }
61     }
62 }
  • server
 1 package idv.steven.sync;
 2 
 3 import java.io.IOException;
 4 import java.net.InetSocketAddress;
 5 import java.net.StandardSocketOptions;
 6 import java.nio.ByteBuffer;
 7 import java.nio.CharBuffer;
 8 import java.nio.channels.ServerSocketChannel;
 9 import java.nio.channels.SocketChannel;
10 import java.nio.charset.Charset;
11 import java.nio.charset.CharsetDecoder;
12 import java.nio.charset.CharsetEncoder;
13 
14 public class EchoServer {
15  
16     public static void main(String[] args) {
17         final int DEFAULT_PORT = 5555;
18         final String IP = "127.0.0.1";
19         ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
20         Charset charset = Charset.forName("UTF-8"); // 網路傳輸時以 UTF-8 編碼
21         CharsetEncoder encoder = charset.newEncoder();
22         CharsetDecoder decoder = charset.newDecoder();
23         
24         // 建立一個 server socket channel
25         try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
26             // 確認 server socket channel 開啟成功
27             if (serverSocketChannel.isOpen()) {
28                 // 設定 TCP 傳輸方式為 blocking 模式
29                 serverSocketChannel.configureBlocking(true);
30                 // 設定 channel 的一些參數
31                 serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
32                 serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
33                 // 繫結 server socket 到指定的 IP 和 port
34                 serverSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));
35 
36                 while(true){
37                     System.out.println("等待連線 ...");
38                     try (SocketChannel socketChannel = serverSocketChannel.accept()) {
39                         System.out.println("連線來至: " + socketChannel.getRemoteAddress());
40                         while (socketChannel.read(buffer) <= 0) {
41                             System.out.println("continue ...");
42                          }
43                         
44                          buffer.flip();
45                         CharBuffer charBuffer = CharBuffer.wrap("回傳: " + decoder.decode(buffer));
46                         ByteBuffer response = encoder.encode(charBuffer);
47                          socketChannel.write(response);
48                          buffer.clear();
49                     } 
50                    catch (IOException ex) {
51                         ex.printStackTrace();
52                     }
53                 }
54             } else {
55                 System.out.println("server socket channel 開啟失數!");
56             }
57         } 
58         catch (IOException ex) {
59             System.err.println(ex);
60         }
61     }
62 }
上面的程式,client 和 server 間傳遞的訊息(電文)就只有個數字,一般來說,TCP 程式的電文不會這麼簡單,大部份會有 header 和 body,在電文最前面也會有個特殊字元當作起始,電文最後還會有特殊字元當作結尾。在台灣證券交易所的網站,就有台股相關的電文規格書,是個很好的參考,進到網站後,選擇上方選單「市場公告 > 公文查詢」,找到「資訊傳輸作業手冊」就有完整的規格。



2015年7月22日 星期三

NIO.2: 目錄的處理

檔案系統的 I/O,除了前幾篇所說明的檔案相關處理外,另一個主題就是目錄的處理 - 讀取、走訪、過濾檔案等,說明如下:
  • 取得根目錄
 1 package idv.steven.nio2.filedir;
 2 
 3 import static java.lang.System.out;
 4 import java.nio.file.FileSystems;
 5 import java.nio.file.Path;
 6 import java.util.ArrayList;
 7 
 8 public class ListRoots {
 9 
10     public static void main(String[] args) {
11         Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
12         
13         ArrayList<Path> list = new ArrayList<Path>();
14         for(Path name : dirs) {
15             list.add(name);
16         }
17         Path[] array = new Path[list.size()];
18         list.toArray(array);
19         
20         for(Path path : array) {
21             System.out.println(path);
22         }
23         
24         //dirs.forEach(out::println); //Java 8
25     }
26 }
如上的程式,第 11 行可以取得檔案系統的根目錄,在單根檔案系統,例如 Unix/Linux,當然就只會輸出 /,在 Windows 上執行,就看作業系統裡有幾個磁碟,像是我的電腦,執行出來就會是 .....
C:\
D:\
E:\
F:\
H:\
程式 13~22 行是傳統 Java 的寫法,使用 Lambda 的話,只要第 24 行這樣一行程式就可以了。
  • 檢查檔案或目錄是否存在? 
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.nio.file.FileSystems;
 4 import java.nio.file.Files;
 5 import java.nio.file.LinkOption;
 6 import java.nio.file.Path;
 7 
 8 public class CheckExist {
 9     public static void main(String[] args) {
10         Path path = FileSystems.getDefault().getPath("D:/TEST");
11         boolean isNotExist = Files.notExists(path, new LinkOption[] {LinkOption.NOFOLLOW_LINKS});
12         System.out.println(isNotExist);
13     }
14 }
不管是檢查檔案或目錄是否存在,使用的方法是一樣的,上面的程式是檢查 D:/ 下是否有 TEST 這個子目錄,注意看一下第 11 行,這裡使用的是 notExists 這個 method,Files 類別也提供有 exists method,notExists 就等於 !exists。
  • 讀取目錄
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.file.DirectoryStream;
 5 import java.nio.file.Files;
 6 import java.nio.file.Path;
 7 import java.nio.file.Paths;
 8 
 9 public class ListDirectory {
10 
11     public static void main(String[] args) {
12         Path path = Paths.get("/home/steven/TEST");
13         try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, "[rR]eadme*.txt")) {
14             for(Path file : ds) {
15                 System.out.println(file.getFileName());
16             }
17         }
18         catch (IOException e) {
19             System.err.println(e);
20         }
21     }
22 }
如上是 java.nio 提供的讀取目錄內容的方法之一,第二個參數不填,則會列出目錄裡的所有檔案、子目錄、連結等所有內容。第二個參數有那些選項呢? 如下:
  1. *: 比對所有字元。
  2. **: 跨目錄比對所有字元。
  3. ?: 比對一個字元,如果用這個選項,例如: a?.txt,那麼就一定是 a 後面要接一個字元,a.txt 不算在這個條件裡。
  4. { }: 比對大括號內所有 pattern,例如: {a?.txt, *.jpg},那麼可能就會列出 ab.txt、face.jpg、leg.jpg …
  5. [ ]: 比對中括號內所有列出的字元,像是上面的程式,會找出 readme*.txt 及 Readme*.txt 的檔案,中括號內,可以放入如下選項:
(1) [0-9]: 0 到 9 的所有數字,當然,也可以是[3-7],只搜尋 3 到 7 的數字。
(2) [A-Z]: 大寫 A 到 Z。
(3) [a-z,A-Z]: 大小寫的所有英文字母。
(4) [2345RT]: 比對到括號中任一字元。
6. 大括號、中括號內也可以有 *、? 等萬用字元,並複合使用,例如: {*[0-9].jpg},輸出的結果可能就會是 face1.jpg、leg3.jpg …
  •  走訪目錄
上面的說明有提到讀取目錄的方法,如果我們要走訪目錄呢? java.nio 提到了一個介面 FileVisitor,實作這個介面後,會有四個 callback method,在走訪時,java.nio 會在各個時間點呼叫這四個 method,讓我們可以進行各種處理,四個介面說明如下:
  1. preVisitDirectory: 走訪一個目錄前。
  2. postVisitDirectory: 走訪一個目錄之後。
  3. visitFile: 走訪一個檔案時。
  4. visitFileFailed: 走訪檔案失敗,通常是因為無存取權限。
接下來看一下程式:
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.file.FileVisitResult;
 5 import java.nio.file.FileVisitor;
 6 import java.nio.file.Files;
 7 import java.nio.file.Path;
 8 import java.nio.file.Paths;
 9 import java.nio.file.attribute.BasicFileAttributes;
10 import static java.nio.file.FileVisitResult.*;
11 
12 public class WalkFolder implements FileVisitor<Path> {
13 
14     @Override
15     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
16             throws IOException {
17         
18         System.out.println("preVisitDirectory: " + dir.getFileName());
19 
20         return CONTINUE;
21     }
22 
23     @Override
24     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
25             throws IOException {
26         
27         System.out.println("visitFile: " + file.getFileName());
28         
29         return CONTINUE;
30     }
31 
32     @Override
33     public FileVisitResult visitFileFailed(Path file, IOException exc)
34             throws IOException {
35         
36         System.out.println("visitFileFailed: " + file.getFileName());
37         
38         return CONTINUE;
39     }
40 
41     @Override
42     public FileVisitResult postVisitDirectory(Path dir, IOException exc)
43             throws IOException {
44 
45         System.out.println("postVisitDirectory: " + dir.getFileName());
46         
47         return CONTINUE;
48     }
49     
50     public static void main(String[] args) throws IOException {
51         WalkFolder walkFolder = new WalkFolder();
52         
53         Files.walkFileTree(Paths.get("D:/TEST"), walkFolder);
54     }
55 }
這個程式會走訪 D:/TEST  下的所有子目錄及檔案,在 method 中可進行需要的處理,特別要提出來說明的是返回值,上面四個 method 都返回 CONTINE,表示繼續走訪,java.nio 提供了四個返回值,如下:
  1. CONTINUE: 繼續走訪
  2. SKIP_SIBLINGS: 跳過同一階層的檔案或子目錄後繼續走訪
  3. SKIP_SUBTREE: 跳過正走訪的這個目錄後繼續走訪
  4. TERMINATE: 停止走訪
在許多的情況下,我們並不會需要同時實作四個 method,所以 java.nio 提供了一個 SimpleFileVisitor 類別,繼承這個類別,我們只需要 override 要特別處理的 method,這樣會方便些。

  • 拷貝、搬移、刪除
 1 package idv.steven.nio2.action;
 2 
 3 import java.io.IOException;
 4 import java.nio.file.Files;
 5 import java.nio.file.Path;
 6 import java.nio.file.Paths;
 7 
 8 public class Copy {
 9 
10     public static void main(String[] args) throws IOException {
11         Path from = Paths.get("D:/TEST/readme.txt");
12         Path to = Paths.get("D:/readme.txt");
13         Files.copy(from, to);
14     }
15 }
上面的程式,很簡單的都用預設值,將檔案由 from 拷貝一份到 to,Files.copy(...) 是可以帶參數,以指出要如何拷貝檔案,例如將它改寫如下:
import static java.nio.file.StandardCopyOption.*;
import static java.nio.file.LinkOption.*;
…
Files.copy(from, to, REPLACE_EXISTING, COPY_ATTRIBUTES, NOFOLLOW_LINKS);
第三個參數指出檔案的屬性也要拷貝到新檔,第四個參數則指出,如果這個檔案是 symbol link 就不要拷貝,這些參數定義在 StandardCopyOption 及 LinkOption 套件中。同樣的程式,可以用來拷貝目錄嗎? 可以,不過,目錄下的檔案不會被拷貝。Files 這個類別還提供有 move、delete 等 method,可以用來搬移、刪除檔案、目錄,詳細可以參考 Java Doc。

2015年7月20日 星期一

NIO.2: 開檔、讀檔、寫檔

Java 一直到 JDK 1.3 為止,都是使用 java.io 下的類別進行 I/O 的處理,對這有興趣的鄉民,可以參考「Java I/O」,那是我 13 年前整理的 ... XD。JDK 1.4 之後提供了 NIO,到了 JDK 1.7 又加上些新的功能,稱為 NIO.2,這些類別都被放在 java.nio 下,一般來說,使用 java.nio 類別操作檔案系統,會比使用 java.io 效率來的高且方便。
在進入主題前,各位不妨先看一下我之前整理的「Working with the Path Class」及「Metadata File Attributes」,這算是進入主題的前菜,這篇開始要說明怎麼開檔、讀檔、寫檔。
  • 開檔
不管是要讀檔或寫檔,第一個步驟總得要開檔,java.nio 提供了以下的開檔方式:
  1. READ: 要讀取檔案的內容
  2. WRITE: 要寫資料到檔案
  3. CREATE: 建立一個新檔,如果檔案已存在,將它刪除,重新建立新檔。
  4. CREATE_NEW: 建立一個新檔,當檔案已存在,拋出 exception。
  5. APPEND: 附加內容到已存在的檔案。
  6. DELETE_ON_CLOSE: 這個選項是用在暫存檔上,當檔案關閉時,將這個檔案刪除。
  7. TRUNCATE_EXISTING: 將檔案內容清除,然後再開始寫入。
  8. SPARSE: 
  9. SYNC: 保持檔案內容和 metadata 不變。
  10. DSYNC: 保持檔案內容不變。
 底下是一個最典型的範例:
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.channels.ReadableByteChannel;
 5 import java.nio.file.Files;
 6 import java.nio.file.Path;
 7 import java.nio.file.Paths;
 8 import java.nio.file.StandardOpenOption;
 9 
10 public class NIO2File {
11     public static void main(String[] args) throws IOException {
12         Path path = Paths.get("C:/Java/poi-3.11/NOTICE");
13         ReadableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ);
14         //...
15     }
16 }
在 java.io 中,I/O 都是在串流 (stream) 中操作,到了 java.nio 都改為渠道 (channel),上面的程式是開啟一個讀取的檔案,所有開檔模式都定義在 StandOpenOption 這個自定型別中。如果要開啟一個寫入的檔案,第 13 行可能就改為如下:
WritableByteChannel channel = Files.newByteChannel(path, new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE});
寫檔時不能只有 WRITE 這個參數,還要再加入 CREATE、CREATE_NEW 等參數,所有參數放在一個 OpenOption 陣列裡。
  • 渠道 (Channel)
java.nio 定義了那些 channel ? 如下類別圖所示:
上圖只列出常用的介面及其 method,要了解詳細的類別繼承關係,請看 JDK Doc。操作檔案時,最常用的就是 ReadableByteChannel、WritableByteChannel 及 SeekableByteChannel,前兩者顧名思義,是分別用在讀檔及寫檔,第三個是同時可用在讀與寫的渠道,且可以在讀寫過程裡移動檔案指標。至於 NetworkChannel 和 MulticastChannel 是用在網路的 TCP 和 UDP 傳輸上,會另外說明。
在 java.nio 中定義了渠道取代 java.io 中的 stream,同時渠道操作的物件也不是 byte[]、char[]… 改成 java.nio 自行定義的 ByteBuffer、CharBuffer 等類別,最常用的是 ByteBuffer,關於 ByteBuffer 的說明,請看「ByteBuffer 指標說明」。
那麼,渠道和串流 (stream) 倒底有什麼不同? 整理如下:
  1. 串流一般來說都是 one-way,也就是只能讀或只能寫,渠道支援同時讀寫。
  2. 渠道可以非同步的讀寫。
  3. 渠道如果讀資料,一定是讀入 Buffer (ByteBuffer、CharBuffer...),寫資料的話,也一定要先把資料放入 Buffer 再透過渠道寫出。
  • 讀檔
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.ByteBuffer;
 5 import java.nio.channels.ReadableByteChannel;
 6 import java.nio.file.Files;
 7 import java.nio.file.Path;
 8 import java.nio.file.Paths;
 9 import java.nio.file.StandardOpenOption;
10 
11 public class NIO2File {
12     public static void main(String[] args) throws IOException {
13         Path path = Paths.get("C:/Java/poi-3.11/NOTICE");
14         ReadableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ);
15         
16         ByteBuffer buffer = ByteBuffer.allocate(1024);
17         
18         while (channel.read(buffer) > 0) {
19             System.out.println(new String(buffer.array()));
20             buffer.flip();
21         }
22         
23         channel.close();
24     }
25 }
上面的程式,於 18 行每次讀取 1024 個 byte,直到檔尾為止,讀取後放入 buffer 中,在 19 行將讀取的內容輸出,讀完後當然要記得關檔 (23行)。
  • 寫檔
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.ByteBuffer;
 5 import java.nio.channels.ReadableByteChannel;
 6 import java.nio.channels.WritableByteChannel;
 7 import java.nio.file.Files;
 8 import java.nio.file.OpenOption;
 9 import java.nio.file.Path;
10 import java.nio.file.Paths;
11 import java.nio.file.StandardOpenOption;
12 
13 public class NIO2File {
14     public static void main(String[] args) throws IOException {
15         Path path = Paths.get("C:/Java/poi-3.11/NOTICE");
16         ReadableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ);
17         
18         Path pathTo = Paths.get("C:/Java/poi-3.11/NOTICE.txt");
19         WritableByteChannel channelTo = Files.newByteChannel(pathTo, 
20                 new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE});
21         
22         ByteBuffer buffer = ByteBuffer.allocate(1024);
23         while ((channel.read(buffer)) > 0) {
24             buffer.flip();
25             channelTo.write(buffer);
26             buffer.flip();
27         }
28         
29         channel.close();
30         channelTo.close();
31     }
32 }
這個程式只是將上一個程式擴充,將由 NOTICE 讀出的內容,寫入 NOTICE.txt 檔裡,在 19~20 行開啟一個寫入的檔案,於 24~26 行將讀到的內容寫入指定的檔案。
  • 小型檔案的讀寫 
上面的讀寫檔案的方法可同時適用在大檔案、小檔案,如果我們預先就知道,要讀寫的檔案很小,有更簡單的方式,如下:
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.file.Files;
 5 import java.nio.file.OpenOption;
 6 import java.nio.file.Path;
 7 import java.nio.file.Paths;
 8 import java.nio.file.StandardOpenOption;
 9 
10 public class SmallFile {
11 
12     public static void main(String[] args) throws IOException {
13         Path path = Paths.get("C:/Java/poi-3.11/NOTICE");
14         Path pathTo = Paths.get("C:/Java/poi-3.11/NOTICE.txt");
15         
16         byte[] smallArray = Files.readAllBytes(path);
17         Files.write(pathTo, smallArray, 
18                 new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE});
19     }
20 }
如上,在 16 行的地方,一次將所有內容讀入一個 byte array,在 17 行的地方,再一次將 byte array 的內容寫入檔案裡。
  • 讀寫文字檔 
到目前為止,我們讀寫檔案的方式,都可適用在各種類型的檔案,如上面,我們處理的是文字檔,事實上,圖檔、執行檔 … 也都不會有問題。如果我們要處理的就是文字檔呢? 文字檔通常就是每一行後面會有列尾符號,所以讀取時可以一次讀一行,寫出時也應該一次寫一行,將上面的程式改寫如下:
 1 package idv.steven.nio2.filedir;
 2 
 3 import java.io.IOException;
 4 import java.nio.charset.Charset;
 5 import java.nio.file.Files;
 6 import java.nio.file.OpenOption;
 7 import java.nio.file.Path;
 8 import java.nio.file.Paths;
 9 import java.nio.file.StandardOpenOption;
10 import java.util.List;
11 
12 public class RWText {
13 
14     public static void main(String[] args) throws IOException {
15         Path path = Paths.get("D:/novel.txt");
16         Path pathTo = Paths.get("D:/novel_1.txt");
17         
18         Charset charset = Charset.forName("MS950");
19         List<String> lines = Files.readAllLines(path, charset);
20         Files.write(pathTo, lines, charset,
21                 new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE});
22     }
23 }
文字檔一般會有編碼問題,這裡用來測試的文字檔 novel.txt 是繁體漢字的一個文字檔,採用  MS950 編碼,所以 19 行讀取的時候要指定檔案的編碼,20 行寫出時也一樣,要指出是什麼編碼。讀取或寫入時不指定編碼,預設就是 UTF-8。