UDP協議提供的服務不一樣於TCP協議的端到端服務,它是面向非鏈接的,屬不可靠協議,UDP套接字在使用前不須要進行鏈接。實際上,UDP協議只實現了兩個功能:java
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客戶端要通過下面三步操做:網絡
一、建立一個DatagramSocket實例,能夠有選擇地對本地地址和端口號進行設置,若是設置了端口號,則客戶端會在該端口號上監遵從服務器端發送來的數據;多線程
二、使用DatagramSocket實例的send()和receive()方法來發送和接收DatagramPacket實例,進行通訊;oop
三、通訊完成後,調用DatagramSocket實例的close()方法來關閉該套接字。測試
因爲UDP是無鏈接的,所以UDP服務端不須要等待客戶端的請求以創建鏈接。另外,UDP服務器爲全部通訊使用同一套接字,這點與TCP服務器不一樣,TCP服務器則爲每一個成功返回的accept()方法建立一個新的套接字。一個典型的UDP服務端要通過下面三步操做:大數據
一、建立一個DatagramSocket實例,指定本地端口號,並能夠有選擇地指定本地地址,此時,服務器已經準備好從任何客戶端接收數據報文;spa
二、使用DatagramSocket實例的receive()方法接收一個DatagramPacket實例,當receive()方法返回時,數據報文就包含了客戶端的地址,這樣就知道了回覆信息應該發送到什麼地方;
三、使用DatagramSocket實例的send()方法向服務器端返回DatagramPacket實例。
這裏有一點須要注意:
UDP程序在receive()方法處阻塞,直到收到一個數據報文或等待超時。因爲UDP協議是不可靠協議,若是數據報在傳輸過程當中發生丟失,那麼程序將會一直阻塞在receive()方法處,這樣客戶端將永遠都接收不到服務器端發送回來的數據,可是又沒有任何提示。爲了不這個問題,咱們在客戶端使用DatagramSocket類的setSoTimeout()方法來制定receive()方法的最長阻塞時間,並指定重發數據報的次數,若是每次阻塞都超時,而且重發次數達到了設置的上限,則關閉客戶端。
下面給出一個客戶端服務端UDP通訊的Demo(沒有用多線程),該客戶端在本地9000端口監聽接收到的數據,並將字符串"Hello UDPserver"發送到本地服務器的3000端口,服務端在本地3000端口監聽接收到的數據,若是接收到數據,則返回字符串"Hello UDPclient"到該客戶端的9000端口。在客戶端,因爲程序可能會一直阻塞在receive()方法處,所以這裏咱們在客戶端用DatagramSocket實例的setSoTimeout()方法來指定receive()的最長阻塞時間,並設置重發數據的次數,若是最終依然沒有接收到從服務端發送回來的數據,咱們就關閉客戶端。
客戶端代碼以下:
1 package zyb.org.UDP; 2 3 import java.io.IOException; 4 import java.io.InterruptedIOException; 5 import java.net.DatagramPacket; 6 import java.net.DatagramSocket; 7 import java.net.InetAddress; 8 9 public class UDPClient { 10 private static final int TIMEOUT = 5000; //設置接收數據的超時時間 11 private static final int MAXNUM = 5; //設置重發數據的最屢次數 12 public static void main(String args[])throws IOException{ 13 String str_send = "Hello UDPserver"; 14 byte[] buf = new byte[1024]; 15 //客戶端在9000端口監聽接收到的數據 16 DatagramSocket ds = new DatagramSocket(9000); 17 InetAddress loc = InetAddress.getLocalHost(); 18 //定義用來發送數據的DatagramPacket實例 19 DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),loc,3000); 20 //定義用來接收數據的DatagramPacket實例 21 DatagramPacket dp_receive = new DatagramPacket(buf, 1024); 22 //數據發向本地3000端口 23 ds.setSoTimeout(TIMEOUT); //設置接收數據時阻塞的最長時間 24 int tries = 0; //重發數據的次數 25 boolean receivedResponse = false; //是否接收到數據的標誌位 26 //直到接收到數據,或者重發次數達到預約值,則退出循環 27 while(!receivedResponse && tries<MAXNUM){ 28 //發送數據 29 ds.send(dp_send); 30 try{ 31 //接收從服務端發送回來的數據 32 ds.receive(dp_receive); 33 //若是接收到的數據不是來自目標地址,則拋出異常 34 if(!dp_receive.getAddress().equals(loc)){ 35 throw new IOException("Received packet from an umknown source"); 36 } 37 //若是接收到數據。則將receivedResponse標誌位改成true,從而退出循環 38 receivedResponse = true; 39 }catch(InterruptedIOException e){ 40 //若是接收數據時阻塞超時,重發並減小一次重發的次數 41 tries += 1; 42 System.out.println("Time out," + (MAXNUM - tries) + " more tries..." ); 43 } 44 } 45 if(receivedResponse){ 46 //若是收到數據,則打印出來 47 System.out.println("client received data from server:"); 48 String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + 49 " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort(); 50 System.out.println(str_receive); 51 //因爲dp_receive在接收了數據以後,其內部消息長度值會變爲實際接收的消息的字節數, 52 //因此這裏要將dp_receive的內部消息長度從新置爲1024 53 dp_receive.setLength(1024); 54 }else{ 55 //若是重發MAXNUM次數據後,仍未得到服務器發送回來的數據,則打印以下信息 56 System.out.println("No response -- give up."); 57 } 58 ds.close(); 59 } 60 }
服務端代碼以下:
1 package zyb.org.UDP; 2 3 import java.io.IOException; 4 import java.net.DatagramPacket; 5 import java.net.DatagramSocket; 6 7 public class UDPServer { 8 public static void main(String[] args)throws IOException{ 9 String str_send = "Hello UDPclient"; 10 byte[] buf = new byte[1024]; 11 //服務端在3000端口監聽接收到的數據 12 DatagramSocket ds = new DatagramSocket(3000); 13 //接收從客戶端發送過來的數據 14 DatagramPacket dp_receive = new DatagramPacket(buf, 1024); 15 System.out.println("server is on,waiting for client to send data......"); 16 boolean f = true; 17 while(f){ 18 //服務器端接收來自客戶端的數據 19 ds.receive(dp_receive); 20 System.out.println("server received data from client:"); 21 String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + 22 " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort(); 23 System.out.println(str_receive); 24 //數據發動到客戶端的3000端口 25 DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),dp_receive.getAddress(),9000); 26 ds.send(dp_send); 27 //因爲dp_receive在接收了數據以後,其內部消息長度值會變爲實際接收的消息的字節數, 28 //因此這裏要將dp_receive的內部消息長度從新置爲1024 29 dp_receive.setLength(1024); 30 } 31 ds.close(); 32 } 33 }
一、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()方法,每次調用前就必須顯式地將其內部消息長度重置爲緩存區的實際長度,以避免接受的數據發生丟失(見上面客戶端代碼第53行,服務端代碼第29行)。
以上面的程序爲例,若在服務端的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對象的方法(見上面客戶端代碼第48行,服務端代碼第21行):
new String(dp_receive.getData(),dp_receive.getOffset(),
dp_receive.getOffset() + dp_receive.getLength());