UDP 的 Java 支持
UDP 協議提供的服務不同於 TCP 協議的端到端服務,它是面向非連接的,屬不可靠協議,UDP 套接字在使用前不需要進行連接。實際上,UDP 協議只實現了兩個功能:
- 在 IP 協議的基礎上添加了端口;
- 對傳輸過程中可能產生的數據錯誤進行了檢測,並拋棄已經損壞的數據。
Java 通過 DatagramPacket 類和 DatagramSocket 類來使用 UDP 套接字,客戶端和服務器端都通過DatagramSocket 的 send()方法和 receive()方法來發送和接收數據,用 DatagramPacket 來包裝需要發送或者接收到的數據。發送信息時,Java 創建一個包含待發送信息的 DatagramPacket 實例,並將其作為參數傳遞給DatagramSocket實例的send()方法;接收信息時,Java 程序首先創建一個 DatagramPacket 實例,該實例預先分配了一些空間,並將接收到的信息存放在該空間中,然后把該實例作為參數傳遞給 DatagramSocket 實例的 receive()方法。在創建 DatagramPacket 實例時,要注意:如果該實例用來包裝待接收的數據,則不指定數據來源的遠程主機和端口,只需指定一個緩存數據的 byte 數組即可(在調用 receive()方法接收到數據后,源地址和端口等信息會自動包含在 DatagramPacket 實例中),而如果該實例用來包裝待發送的數據,則要指定要發送到的目的主機和端口。
UDP 的通信建立的步驟
UDP 客戶端首先向被動等待聯系的服務器發送一個數據報文。一個典型的 UDP 客戶端要經過下面三步操作:
- 創建一個 DatagramSocket 實例,可以有選擇對本地地址和端口號進行設置,如果設置了端口號,則客戶端會在該端口號上監聽從服務器端發送來的數據;
- 使用 DatagramSocket 實例的 send()和 receive()方法來發送和接收 DatagramPacket 實例,進行通信;
- 通信完成后,調用 DatagramSocket 實例的 close()方法來關閉該套接字。
由於 UDP 是無連接的,因此UDP服務端不需要等待客戶端的請求以建立連接。另外,UDP服務器為所有通信使用同一套接字,這點與TCP服務器不同,TCP服務器則為每個成功返回的accept()方法創建一個新的套接字。一個典型的UDP服務端要經過下面三步操作:
- 創建一個 DatagramSocket 實例,指定本地端口號,並可以有選擇地指定本地地址,此時,服務器已經准備好從任何客戶端接收數據報文;
- 使用 DatagramSocket 實例的 receive()方法接收一個 DatagramPacket 實例,當 receive()方法返回時,數據報文就包含了客戶端的地址,這樣就知道了回復信息應該發送到什么地方;
- 使用 DatagramSocket 實例的 send()方法向服務器端返回 DatagramPacket 實例。
UDP Socket Demo
這里有一點需要注意:UDP 程序在 receive()方法處阻塞,直到收到一個數據報文或等待超時。由於 UDP 協議是不可靠協議,如果數據報在傳輸過程中發生丟失,那么程序將會一直阻塞在 receive()方法處,這樣客戶端將永遠都接收不到服務器端發送回來的數據,但是又沒有任何提示。為了避免這個問題,我們在客戶端使用 DatagramSocket 類的 setSoTimeout()方法來制定 receive()方法的最長阻塞時間,並指定重發數據報的次數,如果每次阻塞都超時,並且重發次數達到了設置的上限,則關閉客戶端。
下面給出一個客戶端服務端 UDP 通信的 Demo(沒有用多線程),該客戶端在本地 9000 端口監聽接收到的數據,並將字符串"Hello UDPserver"發送到本地服務器的 3000 端口,服務端在本地 3000 端口監聽接收到的數據,如果接收到數據,則返回字符串"Hello UDPclient"到該客戶端的 9000 端口。在客戶端,由於程序可能會一直阻塞在 receive()方法處,因此這里我們在客戶端用 DatagramSocket 實例的 setSoTimeout()方法來指定 receive()的最長阻塞時間,並設置重發數據的次數,如果最終依然沒有接收到從服務端發送回來的數據,我們就關閉客戶端。
客戶端代碼如下:
package zyb.org.UDP; import java.io.IOException; import java.io.InterruptedIOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; public class UDPClient { private static final int TIMEOUT = 5000; //設置接收數據的超時時間 private static final int MAXNUM = 5; //設置重發數據的最多次數 public static void main(String args[])throws IOException{ String str_send = "Hello UDPserver"; byte[] buf = new byte[1024]; //客戶端在9000端口監聽接收到的數據 DatagramSocket ds = new DatagramSocket(9000); InetAddress loc = InetAddress.getLocalHost(); //定義用來發送數據的DatagramPacket實例 DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),loc,3000); //定義用來接收數據的DatagramPacket實例 DatagramPacket dp_receive = new DatagramPacket(buf, 1024); //數據發向本地3000端口 ds.setSoTimeout(TIMEOUT); //設置接收數據時阻塞的最長時間 int tries = 0; //重發數據的次數 boolean receivedResponse = false; //是否接收到數據的標志位 //直到接收到數據,或者重發次數達到預定值,則退出循環 while(!receivedResponse && tries<MAXNUM){ //發送數據 ds.send(dp_send); try{ //接收從服務端發送回來的數據 ds.receive(dp_receive); //如果接收到的數據不是來自目標地址,則拋出異常 if(!dp_receive.getAddress().equals(loc)){ throw new IOException("Received packet from an umknown source"); } //如果接收到數據。則將receivedResponse標志位改為true,從而退出循環 receivedResponse = true; }catch(InterruptedIOException e){ //如果接收數據時阻塞超時,重發並減少一次重發的次數 tries += 1; System.out.println("Time out," + (MAXNUM - tries) + " more tries..." ); } } if(receivedResponse){ //如果收到數據,則打印出來 System.out.println("client received data from server:"); String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort(); System.out.println(str_receive); //由於dp_receive在接收了數據之后,其內部消息長度值會變為實際接收的消息的字節數, //所以這里要將dp_receive的內部消息長度重新置為1024 dp_receive.setLength(1024); }else{ //如果重發MAXNUM次數據后,仍未獲得服務器發送回來的數據,則打印如下信息 System.out.println("No response -- give up."); } ds.close(); } }
服務端代碼如下:
package zyb.org.UDP; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; public class UDPServer { public static void main(String[] args)throws IOException{ String str_send = "Hello UDPclient"; byte[] buf = new byte[1024]; //服務端在3000端口監聽接收到的數據 DatagramSocket ds = new DatagramSocket(3000); //接收從客戶端發送過來的數據 DatagramPacket dp_receive = new DatagramPacket(buf, 1024); System.out.println("server is on,waiting for client to send data......"); boolean f = true; while(f){ //服務器端接收來自客戶端的數據 ds.receive(dp_receive); System.out.println("server received data from client:"); String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort(); System.out.println(str_receive); //數據發動到客戶端的3000端口 DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),dp_receive.getAddress(),9000); ds.send(dp_send); //由於dp_receive在接收了數據之后,其內部消息長度值會變為實際接收的消息的字節數, //所以這里要將dp_receive的內部消息長度重新置為1024 dp_receive.setLength(1024); } ds.close(); } }
如果服務器端沒有運行,則 receive()會失敗,此時運行結果如下圖所示:
如果服務器端先運行,而客戶端還沒有運行,則服務端運行結果如下圖所示:
此時,如果客戶端運行,將向服務端發送數據,並接受從服務端發送回來的數據,此時運行結果如下圖所示:
需要注意的地方
UDP 套接字和 TCP 套接字的一個微小但重要的差別:UDP 協議保留了消息的邊界信息。
DatagramSocket 的每一次 receive()調用最多只能接收調用一次 send()方法所發送的數據,而且,不同的 receive()方法調用絕對不會返回同一個 send()方法所發送的額數據。
當在 TCP 套接字的輸出流上調用 write()方法返回后,所有調用者都知道數據已經被復制到一個傳輸緩存區中,實際上此時數據可能已經被發送,也有可能還沒有被傳送,而 UDP 協議沒有提供從網絡錯誤中恢復的機制,因此,並不對可能需要重傳的數據進行緩存。這就意味着,當send()方法調用返回時,消息已經被發送到了底層的傳輸信道中。
UDP 數據報文所能負載的最多數據,亦及一次傳送的最大數據為 65507 個字節。
當消息從網絡中到達后,其所包含的數據被 TCP 的 read()方法或 UDP 的 receive()方法返回前,數據存儲在一個先進先出的接收數據隊列中。對於已經建立連接的 TCP 套接字來說,所有已接受但還未傳送的字節都看作是一個連續的字節序列。然而,對於 UDP 套接字來說,接收到的數據可能來自不同的發送者,一個 UDP 套接字所接受的數據存放在一個消息隊列中,每個消息都關聯了其源地址信息,每次 receive()調用只返回一條消息。如果 receive()方法在一個緩存區大小為 n 的 DatagramPacket 實例中調用,而接受隊里中的第一條消息的長度大於 n,則 receive()方法只返回這條消息的前 n 個字節,超出部分會被自動放棄,而且對接收程序沒有任何消息丟失的提示!
出於這個原因,接受者應該提供一個有足夠大的緩存空間的 DatagramPacket 實例,以完整地存放調用 receive()方法時應用程序協議所允許的最大長度的消息。一個 DatagramPacket 實例中所允許傳輸的最大數據量為 65507 個字節,也即是 UDP 數據報文所能負載的最多數據。因此,可以用一個 65600 字節左右的緩存數組來接受數據。
DatagramPacket 的內部消息長度值在接收數據后會發生改變,變為實際接收到的數據的長度值。
每一個 DatagramPacket 實例都包含一個內部消息長度值,其初始值為 byte 緩存數組的長度值,而該實例一旦接受到消息,這個長度值便會變為接收到的消息的實際長度值,這一點可以用 DatagramPacket 類的 getLength()方法來測試。如果一個應用程序使用同一個 DatagramPacket 實例多次調用 receive()方法,每次調用前就必須顯式地將其內部消息長度重置為緩存區的實際長度,以免接受的數據發生丟失。
以上面的程序為例,若在服務端的 receiver()后加入如下代碼:
System.out.println(dp_receive.getLength());
則得到的輸出結果為:15,即接收到的字符串數據“Hello UDPserver”的長度。
DatagramPacket 的 getData()方法總是返回緩沖區的原始大小,忽略了實際數據的內部偏移量和長度信息。
由於 DatagramPacket 的 getData()方法總是返回緩沖數組的原始大小,即剛開始創建緩沖數組時指定的大小,在上面程序中,該長度為 1024,因此如果我們要獲取接收到的數據,就必須截取 getData()方法返回的數組中只含接收到的數據的那一部分。 在 Java1.6 之后,我們可以使用 Arrays.copyOfRange()方法來實現,只需一步便可實現以上功能:
byte[] destbuf = Arrays.copyOfRange(dp_receive.getData(),dp_receive.getOffset(), dp_receive.getOffset() + dp_receive.getLength());
當然,如果要將接收到的字節數組轉換為字符串的話,也可以采用本程序中直接 new 一個 String 對象的方法:
new String(dp_receive.getData(),dp_receive.getOffset(), dp_receive.getOffset() + dp_receive.getLength());