在開始說明 DatagramChannel 之前,先說明一下 UDP 的幾個重要觀念:
- IP 封包的 header 長度為 20 bytes、UDP 封包的 header 長度為 8 bytes,所以,每個封包最大為 65507 bytes (65535 - 28 = 65507)。
- 以 UDP 進行廣播時,其群組的 IP 位址的範圍 (IPv4) 為 224.0.0.1 ~ 239.255.255.255,其中 224.0.0.1 為保留 IP,一般的程式不可使用。
- 當封包數量超過緩衝區的容量時,額外的封包會被拋棄,而且不會有任何通知! (所以,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
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
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 程式。
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 }
沒有留言:
張貼留言