【Java TCP/IP Socket程式設計】----套接字----UDP Socket
目錄
簡介
UDP是面向無連線的協議,在資料傳輸時,資料的傳送端和接收端不建立邏輯上的連線。當一臺計算機向另外一臺計算機發送資料時,傳送端不會確認接收端是否存在,同樣接收端接收到資料時,也不會發送反饋給傳送端。實際上UDP協議只實現了兩個功能:
1)在IP協議的基礎上添加了另一層地址(埠)。
2)對資料傳輸過程中可能產品的資料錯誤進行了檢測,並拋棄已經損壞的資料。
UDP協議開銷比較小,並且傳輸效率比較高,經常用於傳送音訊,視訊或者普通檔案,即使存在丟失一兩個包的情況,影響不會太大。
Java中提供了DatagramPacket類和DatagramSocket類來使用UDP套接字,客戶端和伺服器端都使用的是DatagramSocket來發送資料,使用DatagramPacket來接收資料。
傳送資訊時,需要建立一個包含了待發送資訊的DatagramPacket例項,並將其作為引數傳遞給DatagramSocket類的send()方法。
接收資訊時,同樣需要建立一個DatagramPacket例項,該例項中預先分配了一些空間(一個位元組陣列byte[]),並將接收到的資訊存放在該空間中,然後將該例項作為引數傳遞給DatagramSocket類的receive()方法。
需要注意的是:除了輸出的資訊外,DatagramPacket例項裡面還包含了地址和埠資訊,如果是傳送資料報文,DatagramPacket裡面地址指明瞭目的地址和埠號,如果是接收到的資料報文,DatagramPacket例項中的地址指明瞭所接收資訊的源地址。
UDP通訊
UDP客戶端:
UDP客戶端首先向被動等待聯絡的伺服器端傳送一個數據報文。一個典型的UDP客戶端主要執行以下三步:
1.建立一個DatagramSocket例項,可以選擇對本地地址和埠號進行設定。
2.使用DatagramSocket類的send()和receive()方法來發送和接收DatagramPacket例項,進行通訊。
3.通訊完成後,使用DatagramSocket類的close()方法來銷燬該套接字。
UDP伺服器端:
與TCP伺服器一樣,UDP伺服器的工作是建立一個通訊終端,並被動等待客戶端發起連線。但由於UDP是無連線的,UDP通訊通過客戶端的資料報文初始化,並沒有TCP中建立連線那一步。典型的UDP伺服器要執行以下三步:
1.建立一個DatagramSocket例項,指定本地埠號,並可以選擇指定本地地址。此時伺服器已經準備好從任何客戶端接收資料報文。
2.使用DatagramSocket類receive()方法來接收一個DatagramPacket例項。當receive()方法返回時,資料報文裡面包含了客戶端的地址,由此可知反饋資訊的目的地。
3.使用DatagramSocket類的send()和receive()方法來發送和接收DatagramPacket例項,進行通訊。
UDP通訊案例
public class UDPEchoClient {
private static final int MAXTRIES =5;
private static final String INFO = "hello server";
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(4321);
socket.setSoTimeout(1000);
byte bytes[] = new byte[1024];
InetAddress address = InetAddress.getLocalHost();
DatagramPacket sendPacket = new DatagramPacket(INFO.getBytes(), INFO.length(), address, 1234);
DatagramPacket receivePacket = new DatagramPacket(bytes, 1024);
boolean receiveResponse = false;
int tries = 0;
do {
socket.send(sendPacket);
try {
socket.receive(receivePacket);
if (!receivePacket.getAddress().equals(address)) {
throw new IOException("Receive Packet from unknown source");
}
receiveResponse = true;
} catch (Exception e) {
tries+=1;
System.out.println("time out,"+(MAXTRIES-tries)+"more tries");
}
} while ((tries < 5) && (!receiveResponse));
if (receiveResponse) {
System.out.println("Received from Server:");
System.out.println(new String(receivePacket.getData(),0,receivePacket.getLength())+" from:"+receivePacket.getAddress().getHostAddress()+"---"+receivePacket.getPort());
receivePacket.setLength(1024);
}else {
System.out.println("no response -----giving up");
}
socket.close();
}
}
public class UDPEhoServer {
private static final int LENGTH = 255;
private static final String MESSAGE = "hello client";
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(1234);
byte[] buffer = new byte[LENGTH];
DatagramPacket receivePacket = new DatagramPacket(buffer, LENGTH);
while (true) {
socket.receive(receivePacket);
System.out.println("Received from Client:");
System.out.println(new String(receivePacket.getData(), 0, receivePacket.getLength()) + " from :"
+ receivePacket.getAddress().getHostAddress() + "---" + receivePacket.getPort());
DatagramPacket sendPacket = new DatagramPacket(MESSAGE.getBytes(), MESSAGE.length(), receivePacket.getAddress(), 4321);
socket.send(sendPacket);
receivePacket.setLength(LENGTH);
}
}
}
執行結果:
Received from Server:
hello client from:10.170.234.245---1234
Received from Client:
hello server from :10.170.234.245---4321
可見UDP伺服器端要顯式設定本地埠,並使客戶端知道該埠。設定DatagramPacket引數的埠是為了傳送到伺服器端的指定埠,伺服器端設定DatagramSocket的引數為對應埠以接收指定埠過來的資料。
UDP套接字注意點
1.UDP套接字與TCP套接字重要但微小區別是:UDP協議保留了訊息的邊界資訊。
DatagramSocket的每一次receive()呼叫最多接收呼叫一次send()方法傳送的資料,不同的receive()方法呼叫絕不會返回同一個send()方法呼叫所傳送的資料。
當在TCP套接字的輸出流上呼叫write()方法返回後,所有呼叫者都知道資料已經被複制到一個傳輸快取區中,實際上此時資料可能已經被髮送,也有可能還沒有被傳送,而UDP協議沒有提供從網路錯誤中恢復的機制,因此,並不對可能需要重傳的資料進行快取。這就意味著,當send()方法呼叫返回時,訊息已經被髮送到了底層的傳輸通道中。
2.為了保證已接收但未到傳送的資料不被丟棄,接收端提供快取空間應為UDP資料報文最大負載量(65507位元組)
訊息從網路中到達後,其所包含的資料被read()方法或receive()方法返回前,資料儲存在一個先進先出的接收資料佇列中。對於已連線的TCP套接字來說,所有已接受但還未傳送的位元組都看作是一個連續的位元組序列。然而,對於UDP套接字來說,接收到的資料可能來自不同的傳送者,一個UDP套接字所接收的資料存放在一個訊息佇列中,每個訊息都關聯了其源地址資訊,每次receive()呼叫只返回一條訊息。如果receive()方法在一個快取區大小為n的DatagramPacket例項中呼叫,而接收佇列裡中的第一條訊息的長度大於n,則receive()方法只返回這條訊息的前n個位元組,超出部分會被自動放棄,而且對接收程式沒有任何訊息丟失的提示。
出於這個原因,接受者應該提供一個有足夠大的快取空間的DatagramPacket例項,以完整地存放呼叫receive()方法時應用程式協議所允許的最大長度的訊息。一個DatagramPacket例項中所允許傳輸的最大資料量為65507個位元組,也即是UDP資料報文所能負載的最多資料。因此,可以用一個65600位元組左右的快取陣列來接受資料。
3.DatagramPacket包含一個內部訊息的長度值,該例項一接收訊息,此長度值就會變化,因此每次呼叫receive()方法前將內部訊息的長度值重置為快取區的實際長度。
如果一個應用程式多次使用同一個DatagramPacket例項多次呼叫receive()方法,每次呼叫前顯式將訊息的內部長度重置為緩衝區的原始的長度。如:
packet.setLength(LENGTH);
4.DatagramPacket類的getData()方法總是返回緩衝區原始大小,忽略了實際資料的內部偏移量和長度資訊。
由於DatagramPacket的getData()方法總是返回緩衝陣列的原始大小,因此如果我們要獲取接收到的資料,就必須擷取getData()方法返回的陣列中只含接收到的資料的那一部分。JDK1.6中可以使用Arrays.copyOfRange()方法來實現。
byte[] destbuf = Arrays.copyOfRange(dp_receive.getData(),dp_receive.getOffset(),
dp_receive.getOffset() + dp_receive.getLength());