【Java TCP/IP Socket】UDP Socket(含程式碼)
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());
以上幾個比較重要的知識點,筆者均已做過測試。