UDP 核心概念
在开始编码前,先理解几个关键点:

- 无连接:UDP 不像 TCP 那样需要先建立连接(三次握手),发送方可以直接将数据包发送到接收方的 IP 地址和端口号,而不需要接收方的确认。
- 数据报:数据传输的基本单位是“数据报”(Datagram),每个数据报都包含完整的源地址和目标地址,像一个独立的包裹。
- 不可靠:UDP 不保证数据包的顺序,也不保证数据包一定能到达目的地,包可能会丢失、重复或乱序。
- 高效:因为没有连接建立、确认、重传等开销,UDP 的传输效率非常高。
核心类
Java 使用 java.net 包中的两个主要类来处理 UDP:
DatagramSocket:代表一个 socket,用于发送和接收DatagramPacket,它就像是发送和接收邮件的邮局。DatagramPacket:代表一个数据报,包含了要发送的数据、目标地址和端口,或者接收到的数据和源地址、端口,它就像一个装好地址的信封和信件。
编程模型
UDP 编程的流程非常简单:
服务器端:
- 创建一个
DatagramSocket并绑定到一个特定的端口,等待接收数据。 - 创建一个空的
DatagramPacket用于接收数据。 - 调用
receive()方法,阻塞式地等待接收数据,当有数据到达时,数据会被填充到DatagramPacket中。 - 从
DatagramPacket中提取数据、客户端地址和端口。 - (可选)处理数据。
- 创建一个新的
DatagramPacket,将处理后的数据(或原数据)和客户端的地址、端口打包。 - 调用
send()方法将数据包发回给客户端。 - 关闭
DatagramSocket。
客户端:

- 创建一个
DatagramSocket(通常不需要绑定端口,系统会自动分配一个临时端口)。 - 创建一个
DatagramPacket,包含要发送的数据、服务器的 IP 地址和端口。 - 调用
send()方法将数据包发送出去。 - 创建一个新的空的
DatagramPacket用于接收服务器的响应。 - 调用
receive()方法,阻塞式地等待服务器的响应。 - 从
DatagramPacket中提取服务器返回的数据。 - 关闭
DatagramSocket。
完整代码示例
下面是一个经典的“回显”应用,客户端发送任何消息给服务器,服务器都将原样返回。
服务器端代码 (UDPEchoServer.java)
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPEchoServer {
// 服务器监听的端口号
private static final int PORT = 9876;
public static void main(String[] args) {
// try-with-resources 语句,确保 DatagramSocket 在使用后被自动关闭
try (DatagramSocket serverSocket = new DatagramSocket(PORT)) {
System.out.println("服务器已启动,正在监听端口 " + PORT + "...");
// 创建一个缓冲区,用于接收数据
byte[] receiveBuffer = new byte[1024];
while (true) {
// 1. 创建一个空的 DatagramPacket 用于接收数据
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
// 2. 阻塞式等待接收数据,当有数据到达时,数据被填充到 receivePacket 中
serverSocket.receive(receivePacket);
// 3. 从接收到的数据包中提取数据
String receivedMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("收到来自 " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort() + " 的消息: " + receivedMessage);
// 4. 创建要发送回去的数据包
// 使用客户端的地址和端口作为目标
byte[] sendBuffer = receivedMessage.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, receivePacket.getAddress(), receivePacket.getPort());
// 5. 发送数据包
serverSocket.send(sendPacket);
System.out.println("已将消息回显给客户端。");
}
} catch (SocketException e) {
System.err.println("无法创建或绑定 DatagramSocket: " + e.getMessage());
} catch (IOException e) {
System.err.println("发生 I/O 错误: " + e.getMessage());
}
}
}
客户端代码 (UDPEchoClient.java)
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UDPEchoClient {
// 服务器的 IP 地址(本地回环地址)
private static final String SERVER_HOST = "127.0.0.1";
// 服务器的端口号,必须与服务器的端口号一致
private static final int SERVER_PORT = 9876;
public static void main(String[] args) {
// try-with-resources 语句
try (DatagramSocket clientSocket = new DatagramSocket();
Scanner scanner = new Scanner(System.in)) {
// 获取服务器的 IP 地址
InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
System.out.println("客户端已启动,连接到服务器 " + SERVER_HOST + ":" + SERVER_PORT);
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
while (true) {
System.out.print("> ");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 1. 创建要发送的数据包
byte[] sendBuffer = message.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, serverAddress, SERVER_PORT);
// 2. 发送数据包
clientSocket.send(sendPacket);
System.out.println("消息已发送: " + message);
// 3. 创建一个空的 DatagramPacket 用于接收服务器的响应
byte[] receiveBuffer = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
// 4. 阻塞式等待接收服务器的响应
clientSocket.receive(receivePacket);
// 5. 从接收到的数据包中提取数据
String echoMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("服务器回显: " + echoMessage);
}
} catch (SocketException e) {
System.err.println("无法创建 DatagramSocket: " + e.getMessage());
} catch (IOException e) {
System.err.println("发生 I/O 错误: " + e.getMessage());
}
System.out.println("客户端已关闭。");
}
}
如何运行
-
编译代码:打开终端或命令提示符,进入两个
.java文件所在的目录,然后运行:javac UDPEchoServer.java UDPEchoClient.java
-
启动服务器:在一个终端窗口中运行服务器,服务器会启动并等待连接。
java UDPEchoServer
你会看到输出:
服务器已启动,正在监听端口 9876...
(图片来源网络,侵删) -
启动客户端:在另一个终端窗口中运行客户端。
java UDPEchoClient
你会看到输出:
客户端已启动,连接到服务器 127.0.0.1:9876,并提示你输入消息。 -
交互:
- 在客户端输入
Hello, Server!然后按回车。 - 客户端会发送消息,并显示
服务器回显: Hello, Server!。 - 服务器端终端会显示收到的消息和回显操作。
- 可以继续输入消息进行交互,直到在客户端输入
exit退出。
- 在客户端输入
关键点解析与最佳实践
- 缓冲区大小:
DatagramPacket的缓冲区大小(byte[])需要根据你的应用场景来设定,如果发送的数据超过了缓冲区大小,数据会被截断。1024字节是一个常见的起始大小。 - 阻塞式操作:
receive()方法是阻塞的,如果没有数据到达,程序会一直等待,如果你希望实现非阻塞,可以使用setSoTimeout(int timeout)方法来设置一个超时时间,超时后,receive()会抛出SocketTimeoutException。 DatagramPacket的重用:在服务器端,我们循环使用同一个receivePacket对象来接收数据,这是高效的,因为每次receive()调用都会用新数据覆盖这个对象,发送时则需要为每个客户端创建新的DatagramPacket。- 异常处理:网络编程充满了不确定性,
IOException及其子类(如SocketException)必须被妥善处理。 try-with-resources:强烈建议使用try-with-resources语句来自动管理DatagramSocket的生命周期,确保它在程序结束时或发生异常时被正确关闭,避免资源泄漏。- 多线程:上面的例子是单线程的,如果服务器需要同时为多个客户端服务,你需要将
receive()和send()的逻辑(或至少是receive()之后的部分)放到一个循环中,并使用多线程来处理每个客户端的请求,否则,一个客户端的交互会阻塞其他客户端。
UDP vs. TCP (快速对比)
| 特性 | UDP (User Datagram Protocol) | TCP (Transmission Control Protocol) |
|---|---|---|
| 连接 | 无连接 | 面向连接(需要三次握手) |
| 可靠性 | 不可靠(不保证顺序、不保证不丢失) | 可靠(通过确认、重传、校验和保证数据完整) |
| 速度 | 快(开销小) | 慢(开销大) |
| 数据单位 | 数据报 | 字节流 |
| 头部大小 | 8 字节 | 20 字节(不含选项) |
| 应用场景 | 实时视频/音频、DNS、在线游戏、广播 | 网页浏览、文件传输、电子邮件 |
选择使用 UDP 还是 TCP,完全取决于你的应用需求,如果你需要快速传输,并能容忍少量数据丢失,UDP 是绝佳选择,如果你需要确保数据准确无误地、按顺序到达,TCP 是不二之选。
