Google Code Prettify

2015年9月6日 星期日

NIO.2: UDP 網路程式設計 (request / response)

一般人寫網路程式比較少用到 UDP,大部份的書著墨不多,NIO.2 的 UDP API 也一直到 Java 7 出來後,DatagramChannel 相關的 API 才趨於完整,至於眾所期盼的非同步 UDP,一直到 Java 8 都還沒有被加入。
在開始說明 DatagramChannel 之前,先說明一下 UDP 的幾個重要觀念:
  1. IP 封包的 header 長度為 20 bytes、UDP 封包的 header 長度為 8 bytes,所以,每個封包最大為 65507 bytes (65535 - 28 = 65507)。
  2. 以 UDP 進行廣播時,其群組的 IP 位址的範圍 (IPv4) 為 224.0.0.1 ~ 239.255.255.255,其中 224.0.0.1 為保留 IP,一般的程式不可使用。
  3. 當封包數量超過緩衝區的容量時,額外的封包會被拋棄,而且不會有任何通知! (所以,UDP 網路傳輸,量大的時候,掉幾個封包是正常的 …)
上面的第 3 點看起來好像 UDP 不太實用? 其實並不會! UDP 的速度比 TCP 快很多,當傳輸的資料量非常大,且偶而掉幾個封包不影響結果時,是很好用的! 像是台灣證券交易所、櫃買中心,他們每天台股開盤到收盤間,傳送的股價資料就是用 UDP,因為股票交易非常頻繁,在整個交易時間內,股價會一直波動,如果不用 UDP,要即時的傳送那麼大量的資料會有困難,而且因為股價一直波動,偶而掉一兩個封包並不會影響輸出的結果,像是台積電,每天有大量交易,如果在 10:15:20.025 這個時間交易成功一筆,股價是 130.5 元,這個封包掉了,在 10:15:20.030 這個時間又有新的交易,股價為 131 元,在那麼短的瞬間少顯示一個價位,在線圖上並沒有影響。

雖然 UDP 用在廣播、群播的情況較多,但是為了效率點對點傳輸也可以採用 UDP,所以這篇先說明點對點傳輸。那麼,現在進入主題,底下是 DatagramChannel 的類別圖,寫程式時,記得回頭來看這張類別圖,會對於 DatagramChannel 的整體架構更有感覺。






底下的範例程式是一個 echo server 和 echo client,由 echo client 送出一段字串給 echo server,echo server 原封不動的回傳給 client,程式的說明直接寫在註解裡。
  • Echo Server
1 package idv.steven.udp;
 2 
 3 import java.net.InetSocketAddress;
 4 import java.net.SocketAddress;
 5 import java.net.StandardProtocolFamily;
 6 import java.net.StandardSocketOptions;
 7 import java.nio.ByteBuffer;
 8 import java.nio.channels.DatagramChannel;
 9 
10 public class EchoServer {
11     public static void main(String[] args) {
12         final int LOCAL_PORT = 7335;
13         final String LOCAL_IP = "127.0.0.1";
14         final int MAX_PACKET_SIZE = 65507;
15         
16         ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);
17         // DatagramChannel 是一個 abstract class,沒辦法用 new 創建,而要用它的 static method - open。
18         // StandardProtocolFamily 有兩個常數 INET、INET6,分別表示要用 IPv4 或 IPv6。
19         try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {
20             // 檢查 channel 是否成功被開啟
21             if (datagramChannel.isOpen()) {
22                 System.out.println("Echo server was successfully opened!");
23                 // 設定送出與接收的緩衝區大小
24                 datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
25                 datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);
26                 // 在執行 bind 之後,socket 和這個網址產生繫結,一直到 close 為止。
27                 datagramChannel.bind(new InetSocketAddress(LOCAL_IP, LOCAL_PORT));
28 
29                 while (true) {
30                     // 等待接收資料
31                     SocketAddress clientAddress = datagramChannel.receive(buffer);
32                     // 收到資料後, buffer 調整游標,並由寫入模式轉為讀取模式。
33                     buffer.flip();
34                     System.out.println("收到 " + buffer.limit() + " bytes 資料");
35                     // 將資料送回給 client
36                     datagramChannel.send(buffer, clientAddress);
37                     // 清除 buffer
38                     buffer.clear();
39                 }
40             } else {
41                 System.out.println("channel 開啟失敗");
42             }
43         } catch (Exception ex) {
44             ex.printStackTrace();
45         }
46     }
47 }
  • Echo Client
1 package idv.steven.udp;
 2 
 3 import java.io.IOException;
 4 import java.net.InetSocketAddress;
 5 import java.net.StandardProtocolFamily;
 6 import java.net.StandardSocketOptions;
 7 import java.nio.ByteBuffer;
 8 import java.nio.CharBuffer;
 9 import java.nio.channels.DatagramChannel;
10 import java.nio.charset.Charset;
11 import java.nio.charset.CharsetDecoder;
12 
13 public class EchoClient {
14     public static void main(String[] args) throws IOException {
15         final int REMOTE_PORT = 7335;
16         final String REMOTE_IP = "127.0.0.1"; //modify this accordingly if you want to test remote
17         final int MAX_PACKET_SIZE = 65507;
18         
19         CharBuffer charBuffer = null;
20         // 網路傳輸時,資訊基本上就是 byte array,但是同樣的資料用不同編碼當然會不一樣,
21         // 所以,client 和 server 一定會約定好編碼,上面的 server 因為只是直接回傳同樣的資料,
22         // 才沒有處理編碼的問題。 
23         Charset charset = Charset.defaultCharset();
24         CharsetDecoder decoder = charset.newDecoder();
25         ByteBuffer text = ByteBuffer.wrap("島國程式員!".getBytes()); // 將傳到 server 的字串
26         ByteBuffer echoedText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE); // 回傳的字串
27         
28         try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {
29             if (datagramChannel.isOpen()) {
30                 datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
31                 datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);
32 
33                 // 將資料送出到 server
34                 int sent = datagramChannel.send(text, new InetSocketAddress(REMOTE_IP, REMOTE_PORT));
35                 // 等待 server 回應
36                 datagramChannel.receive(echoedText);
37                 echoedText.flip();
38 
39                 // 收到回應的資料後,將它輸出到 console。
40                 charBuffer = decoder.decode(echoedText);
41                 System.out.println(charBuffer.toString());
42                 echoedText.clear();
43             } else {
44                 System.out.println("channel 開啟失敗");
45             }
46         }
47         catch (Exception ex) {
48             ex.printStackTrace();
49         }
50     }
51 }
這個範例也可以換成用 TCP 來寫,用 UDP 和 TCP 的差異在於,TCP 有確實建立 client 到 server 間的連線,而 UDP 並沒有! 另一個要注意的是,在類別圖中可以看到 DatagramChannel 也有實作 ReadableByteChannel 及 WritableByteChannel 兩個介面,在檔案的 I/O 中正是透過這兩個介面的 read() 和 write() 來讀寫資料 (詳見: NIO.2 開檔、讀檔、寫檔),那麼 DatagramChannel 既然實作了這兩個介面,是否也可以使用 read() 和 write() method 來接收和送出資料呢? 答案是當然可以! 不然為什麼要實作這兩個介面?? 現在來改寫一下 client 程式。
1 package idv.steven.udp;
 2 
 3 import java.io.IOException;
 4 import java.net.InetSocketAddress;
 5 import java.net.SocketAddress;
 6 import java.net.StandardProtocolFamily;
 7 import java.net.StandardSocketOptions;
 8 import java.nio.ByteBuffer;
 9 import java.nio.CharBuffer;
10 import java.nio.channels.DatagramChannel;
11 import java.nio.charset.Charset;
12 import java.nio.charset.CharsetDecoder;
13 
14 public class EchoClient2 {
15     public static void main(String[] args) throws IOException {
16         final int REMOTE_PORT = 7335;
17         final String REMOTE_IP = "127.0.0.1"; //modify this accordingly if you want to test remote
18         final int MAX_PACKET_SIZE = 65507;
19         
20         CharBuffer charBuffer = null;
21         // 網路傳輸時,資訊基本上就是 byte array,但是同樣的資料用不同編碼當然會不一樣,
22         // 所以,client 和 server 一定會約定好編碼,上面的 server 因為只是直接回傳同樣的資料,
23         // 才沒有處理編碼的問題。 
24         Charset charset = Charset.defaultCharset();
25         CharsetDecoder decoder = charset.newDecoder();
26         ByteBuffer text = ByteBuffer.wrap("島國程式員!".getBytes()); // 將傳到 server 的字串
27         ByteBuffer echoedText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE); // 回傳的字串
28         
29         try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {
30             if (datagramChannel.isOpen()) {
31                 datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
32                 datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);
33                 
34                 // 使用 read()、write() 前,一定要先呼叫 connect() 建立與 server 間的連線。
35                 SocketAddress remote = new InetSocketAddress(REMOTE_IP, REMOTE_PORT);
36                 datagramChannel.connect(remote);
37                 
38                 datagramChannel.write(text);
39                 datagramChannel.read(echoedText);
40                 echoedText.flip();
41 
42                 // 收到回應的資料後,將它輸出到 console。
43                 charBuffer = decoder.decode(echoedText);
44                 System.out.println(charBuffer.toString());
45                 echoedText.clear();
46             } else {
47                 System.out.println("channel 開啟失敗");
48             }
49         }
50         catch (Exception ex) {
51             ex.printStackTrace();
52         }
53     }
54 }

沒有留言:

張貼留言