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,在電文最前面也會有個特殊字元當作起始,電文最後還會有特殊字元當作結尾。在台灣證券交易所的網站,就有台股相關的電文規格書,是個很好的參考,進到網站後,選擇上方選單「市場公告 > 公文查詢」,找到「資訊傳輸作業手冊」就有完整的規格。



沒有留言:

張貼留言