轉載請注明出處:http://blog.csdn.net/ns_code/article/details/14128987
UDP的Java支持
UDP協議提供的服務不同於TCP協議的端到端服務,它是面向非連接的,屬不可靠協議,UDP套接字在使用前不需要進行連接。實際上,UDP協議只實現了兩個功能:
1)在IP協議的基礎上添加了端口;
2)對傳輸過程中可能產生的數據錯誤進行了檢測,並拋棄已經損壞的數據。
Java通過DatagramPacket類和DatagramSocket類來使用UDP套接字,客戶端和服務器端都通過DatagramSocket的send()方法和receive()方法來發送和接收數據,用DatagramPacket來包裝需要發送或者接收到的數據。發送信息時,Java創建一個包含待發送信息的DatagramPacket實例,並將其作為參數傳遞給DatagramSocket實例的send()方法;接收信息時,Java程序首先創建一個DatagramPacket實例,該實例預先分配了一些空間,並將接收到的信息存放在該空間中,然后把該實例作為參數傳遞給DatagramSocket實例的receive()方法。在創建DatagramPacket實例時,要注意:如果該實例用來包裝待接收的數據,則不指定數據來源的遠程主機和端口,只需指定一個緩存數據的byte數組即可(在調用receive()方法接收到數據后,源地址和端口等信息會自動包含在DatagramPacket實例中),而如果該實例用來包裝待發送的數據,則要指定要發送到的目的主機和端口。
UDP的通信建立的步驟
UDP客戶端首先向被動等待聯系的服務器發送一個數據報文。一個典型的UDP客戶端要經過下面三步操作:
1、創建一個DatagramSocket實例,可以有選擇地對本地地址和端口號進行設置,如果設置了端口號,則客戶端會在該端口號上監聽從服務器端發送來的數據;
2、使用DatagramSocket實例的send()和receive()方法來發送和接收DatagramPacket實例,進行通信;
3、通信完成后,調用DatagramSocket實例的close()方法來關閉該套接字。
由於UDP是無連接的,因此UDP服務端不需要等待客戶端的請求以建立連接。另外,UDP服務器為所有通信使用同一套接字,這點與TCP服務器不同,TCP服務器則為每個成功返回的accept()方法創建一個新的套接字。一個典型的UDP服務端要經過下面三步操作:
1、創建一個DatagramSocket實例,指定本地端口號,並可以有選擇地指定本地地址,此時,服務器已經准備好從任何客戶端接收數據報文;
2、使用DatagramSocket實例的receive()方法接收一個DatagramPacket實例,當receive()方法返回時,數據報文就包含了客戶端的地址,這樣就知道了回復信息應該發送到什么地方;
3、使用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()會失敗,此時運行結果如下圖所示:
如果服務器端先運行,而客戶端還沒有運行,則服務端運行結果如下圖所示:
此時,如果客戶端運行,將向服務端發送數據,並接受從服務端發送回來的數據,此時運行結果如下圖所示:
幾個需要注意的地方
1、UDP套接字和TCP套接字的一個微小但重要的差別:UDP協議保留了消息的邊界信息。
DatagramSocket的每一次receive()調用最多只能接收調用一次send()方法所發送的數據,而且,不同的receive()方法調用絕對不會返回同一個send()方法所發送的額數據。
當在TCP套接字的輸出流上調用write()方法返回后,所有調用者都知道數據已經被復制到一個傳輸緩存區中,實際上此時數據可能已經被發送,也有可能還沒有被傳送,而UDP協議沒有提供從網絡錯誤中恢復的機制,因此,並不對可能需要重傳的數據進行緩存。這就意味着,當send()方法調用返回時,消息已經被發送到了底層的傳輸信道中。
2、UDP數據報文所能負載的最多數據,亦及一次傳送的最大數據為65507個字節
當消息從網絡中到達后,其所包含的數據被TCP的read()方法或UDP的receive()方法返回前,數據存儲在一個先進先出的接收數據隊列中。對於已經建立連接的TCP套接字來說,所有已接受但還未傳送的字節都看作是一個連續的字節序列。然而,對於UDP套接字來說,接收到的數據可能來自不同的發送者,一個UDP套接字所接受的數據存放在一個消息隊列中,每個消息都關聯了其源地址信息,每次receive()調用只返回一條消息。如果receive()方法在一個緩存區大小為n的DatagramPacket實例中調用,而接受隊里中的第一條消息的長度大於n,則receive()方法只返回這條消息的錢n個字節,超出部分會被自動放棄,而且對接收程序沒有任何消息丟失的提示!
出於這個原因,接受者應該提供一個有足夠大的緩存空間的DatagramPacket實例,以完整地存放調用receive()方法時應用程序協議所允許的最大長度的消息。一個DatagramPacket實例中所允許傳輸的最大數據量為65507個字節,也即是UDP數據報文所能負載的最多數據。因此,可以用一個65600字節左右的緩存數組來接受數據。
3、DatagramPacket的內部消息長度值在接收數據后會發生改變,變為實際接收到的數據的長度值。
每一個DatagramPacket實例都包含一個內部消息長度值,其初始值為byte緩存數組的長度值,而該實例一旦接受到消息,這個長度值便會變為接收到的消息的實際長度值,這一點可以用DatagramPacket類的getLength()方法來測試。如果一個應用程序使用同一個DatagramPacket實例多次調用receive()方法,每次調用前就必須顯式地將其內部消息長度重置為緩存區的實際長度,以免接受的數據發生丟失(見上面客戶端代碼第53行,服務端代碼第29行)。
以上面的程序為例,若在服務端的receiver()后加入如下代碼:System.out.println(dp_receive.getLength());則得到的輸出結果為:15,即接收到的字符串數據“Hello UDPserver”的長度。
4、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對象的方法(見上面客戶端代碼第48行,服務端代碼第21行):
new String(dp_receive.getData(),dp_receive.getOffset(),
dp_receive.getOffset() + dp_receive.getLength());
以上幾個比較重要的知識點,筆者均已做過測試。