如何創(chuàng)建 Java UDP Socket
1. 前言
UDP 的英文全稱是:User Datagram Protocol,翻譯成中文叫用戶數(shù)據(jù)報協(xié)議,它是 TCP/IP 協(xié)議族中一個非常重要的傳輸層協(xié)議。UDP 是一個無連接的、不可靠的傳輸層協(xié)議,沒有丟包重傳機(jī)制、沒有流控機(jī)制、沒有擁塞控制機(jī)制。UDP 不保證數(shù)據(jù)包的順序,UDP 傳輸經(jīng)常出現(xiàn)亂序,UDP 不對重復(fù)包進(jìn)行過濾。
既然 UDP 這么多缺點,我們還有學(xué)習(xí)的必要嗎?其實不然,正是因為 UDP 沒有提供復(fù)雜的各種保障機(jī)制,才使得它具有實時、高效的傳輸特性。那么 UDP 到底有哪些優(yōu)勢呢?
- 第一,UDP 是面向消息的傳輸協(xié)議,保證數(shù)據(jù)包的邊界,不需要進(jìn)行消息解析,處理邏輯非常簡單。
- 第二,UDP 具有實時、高效的傳輸特性。
- 第三,協(xié)議棧沒有對 UDP 進(jìn)行過多的干預(yù),這給應(yīng)用層帶來了很多便利,應(yīng)用程序可以根據(jù)自己的需要對傳輸進(jìn)行控制。比如,自己實現(xiàn)優(yōu)先級控制、流量控制、可靠性機(jī)制等。當(dāng)然還有其他一些優(yōu)勢,我就不再一一列舉。
UDP 在音視頻會議、VOIP、音視頻實時通信等行業(yè)有著廣泛的應(yīng)用。為此,我們是非常有必要學(xué)好 UDP 的。由于 UDP 相對簡單,學(xué)習(xí)起來也會輕松很多。
2. 傳統(tǒng) UDP 客戶端和服務(wù)器建立過程
同樣,我們展示了通過 C 語言 Socket API 編寫 UDP 客戶端和服務(wù)器程序的步驟,如下:
圖中的矩形方框都是 C 函數(shù)。對比 TCP 客戶端、服務(wù)器的建立過程,我們發(fā)現(xiàn) UDP 客戶端可以調(diào)用 connect 函數(shù),但是并不會去連接服務(wù)器,只是和本地接口做綁定;UDP 服務(wù)器是沒有 listen 和 accept 調(diào)用的。對于 UDP 客戶端來說,connect 函數(shù)的調(diào)用是可選的。接下來,我們就探討一下如何用 Java 語言編寫 UDP 客戶端和服務(wù)器程序。
3. Java DatagramSocket 分析
Java 語言抽象了 java.net.DatagramSocket 類,表示一個 UDP Socket,既可以用在客戶端,又可以用在服務(wù)器端。java.net.DatagramSocket 是一個包裝類,對外抽象了一組方法,具體實現(xiàn)是在 java.net.DatagramSocketImpl 類中完成的,它允許用戶自定義具體實現(xiàn)。java.net.DatagramSocket 類包含的主要功能如下:
- 創(chuàng)建 UDP Socket,具體就是創(chuàng)建一個 java.net.DatagramSocket 類的對象。
- 將 Socket 綁定到本地接口 IP 地址或者端口,可以調(diào)用 java.net.DatagramSocket 類的構(gòu)造方法或 bind 方法完成。
- 將客戶端 UDP Socket 和遠(yuǎn)端 Socket 做綁定,可以通過 java.net.DatagramSocket 類的 connect 方法完成。
提示:
UDP 客戶端調(diào)用 connect 方法,僅僅是將本地 Socket 和遠(yuǎn)端 Socket 做綁定,并不會有類似 TCP 三次握手的過程。
-
關(guān)閉連接,可以調(diào)用 java.net.DatagramSocket 類的 close 方法完成。
-
接收數(shù)據(jù),可以通過 java.net.DatagramSocket 類的 receive 方法實現(xiàn)數(shù)據(jù)接收。
-
發(fā)送數(shù)據(jù),可以通過 java.net.DatagramSocket 類的 send 方法實現(xiàn)數(shù)據(jù)發(fā)送。
java.net.Socket 類提供了一組重載的構(gòu)造方法,方便程序員選擇,大體分為四類:
- 無參
public DatagramSocket() throws SocketException
綁定到任意可用的端口和通配符 IP 地址,比如 IPv4 的 0.0.0.0。一般用作 UDP 客戶端 Socket 的創(chuàng)建。
- 傳入 port 參數(shù)
public DatagramSocket(int port) throws SocketException
綁定到由 port 指定的端口和通配符 IP 地址,比如 IPv4 的 0.0.0.0。一般用作 UDP 服務(wù)端 Socket 的創(chuàng)建。
- 傳入指定的 IP 和 Port 參數(shù)
public DatagramSocket(SocketAddress bindaddr) throws SocketException
public DatagramSocket(int port, InetAddress laddr) throws SocketException
綁定到指定的端口和指定的網(wǎng)絡(luò)接口。如果你的主機(jī)有多個網(wǎng)卡,并且你指向在某個指定的網(wǎng)卡上收發(fā)數(shù)據(jù),可以用此構(gòu)造方法。既可以用作 UDP 客戶端 Socket,也可以用作 UDP 服務(wù)端 Socket。
4. Java UDP 客戶端
我們創(chuàng)建一個簡單的 UDP 客戶端程序,代碼如下:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
public class UDPClient {
private static final int PORT = 9002;
private static final String DST_HOST = "127.0.0.1";
private static final int RECV_BUFF_LEN = 1500;
private static byte[] inBuff = new byte[RECV_BUFF_LEN];
public static void main(String[] args) {
// 創(chuàng)建 UDP 客戶端 Socket,選擇無參構(gòu)造方法,由系統(tǒng)分配本地端口號和網(wǎng)絡(luò)接口
try (DatagramSocket udpClient = new DatagramSocket()){
// 構(gòu)造發(fā)送的目標(biāo)地址,指定目標(biāo) IP 和目標(biāo)端口號
SocketAddress to = new InetSocketAddress(DST_HOST, PORT);
while (true){
String req = "Hello Server!";
// 構(gòu)造發(fā)送數(shù)據(jù)包,需要傳入消息內(nèi)容和目標(biāo)地址結(jié)構(gòu) SocketAddress
DatagramPacket message = new DatagramPacket(req.getBytes(), req.length(), to);
// 發(fā)送消息
udpClient.send(message);
System.out.println("Send UDP message:"
+ req + " to server:" + message.getSocketAddress().toString());
// 構(gòu)造接收消息的數(shù)據(jù)包,需要傳入 byte 數(shù)組
DatagramPacket inMessage = new DatagramPacket(inBuff, inBuff.length);
// 接收消息
udpClient.receive(inMessage);
System.out.println("Recv UDP message:"
+ new String(inMessage.getData(), 0, inMessage.getLength())
+ " from server:" + inMessage.getSocketAddress().toString());
// 每隔 2 秒發(fā)送一次消息
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5. Java UDP 服務(wù)端
我們創(chuàng)建一個簡單的 UDP 服務(wù)端程序,代碼如下:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServer {
private static final int BIND_PORT = 9002;
private static final String BIND_HOST = "127.0.0.1";
private static final int RECV_BUFF_LEN = 1500;
private static byte[] inBuff = new byte[RECV_BUFF_LEN];
public static void main(String[] args) {
// 構(gòu)造服務(wù)器 Socket,綁定到一個固定的端口,監(jiān)聽的 IP 是 0.0.0.0
try (DatagramSocket udpServer = new DatagramSocket(BIND_PORT)) {
// 構(gòu)造接收消息的數(shù)據(jù)包,需要傳入 byte 數(shù)組。
// 我們將這條語句放在循環(huán)外,不需要每次消息收發(fā)都構(gòu)造此結(jié)構(gòu)
DatagramPacket inMessage = new DatagramPacket(inBuff, inBuff.length);
while (true){
// 接收客戶端消息
udpServer.receive(inMessage);
System.out.println("Recv UDP message:"
+ new String(inMessage.getData(), 0, inMessage.getLength())
+ " from Client:"
+ inMessage.getSocketAddress().toString());
String rsp = "Hello Client!";
// 構(gòu)造發(fā)送的消息結(jié)構(gòu)
// 注意?。?!對于服務(wù)器來說,發(fā)送的目標(biāo)地址一定是接收消息時的源地址,所以從 inMessage 結(jié)構(gòu)獲取
DatagramPacket message = new DatagramPacket(rsp.getBytes(), rsp.length(),
inMessage.getSocketAddress());
// 發(fā)送消息
udpServer.send(message);
System.out.println("Send UDP message:"
+ rsp + " to Client:" + message.getSocketAddress().toString());
// 重置接收數(shù)據(jù)包消息長度,準(zhǔn)備接收下一個消息
inMessage.setLength(inBuff.length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
6. 小結(jié)
用 Java 語言編寫 UDP 客戶端和服務(wù)器程序非常簡單,你只需要創(chuàng)建一個 java.net.DatagramSocket 實例。如果你構(gòu)造的是服務(wù)器 Socket,需要傳入監(jiān)聽的端口號,監(jiān)聽的接口 IP 是可選的,默認(rèn)是監(jiān)聽通配符 IP。如果你創(chuàng)建的是客戶端 Socket,你可以傳入綁定的本地 Port 和接口地址,也可以不傳入任何參數(shù)。對于客戶端 UDP Socket 來說,你也可以調(diào)用 connect 方法,只是和遠(yuǎn)端 Socket 綁定,沒有類似 TCP 的三次握手過程。
示例代碼我們采用的是 try-with-resources 寫法,對于 Java 7 以后的版本,可以不顯式調(diào)用 close 方法。如果是非此寫法,需要顯式調(diào)用 close 方法。