UDP 单发单收 | JavaSE
UDP 单发单收
一、学习目标
学完本章,你应该能够:
- 能够独立完成一个 UDP 客户端发送一条消息。
- 能够独立完成一个 UDP 服务端接收一条消息。
- 能够正确使用
DatagramSocket、DatagramPacket和InetAddress。 - 能够正确完成字符串的 UTF-8 编码与解码。
- 能够从接收到的数据包中获取发送方 IP 和端口。
- 能够解释为什么 UDP 客户端即使
send()正常返回,也不能证明服务端收到消息。 - 能够 Debug 端口错误、服务端未启动、缓冲区过小、解码长度错误等常见问题。
二、核心知识
2.1 什么叫 UDP 单发单收
本章只实现最简单的通信模型:
客户端
发送一次
│
▼
服务端
接收一次
也就是:
One Send
+
One Receive
程序完成一次通信后结束。
后续 09-05 再升级成:
多发
多收
循环通信
因此本章的目标不是做聊天软件,而是彻底打通:
Java 对象
↓
UDP 数据报
↓
网络
↓
UDP 数据报
↓
Java 对象
这条完整链路。
2.2 单发单收整体结构
假设服务端监听:
UDP 8080
客户端和服务端运行在同一台电脑。
那么:
UDPClient
随机端口
│
│ "你好,UDP"
▼
127.0.0.1:8080
│
▼
UDPServer
客户端需要知道:
服务器 IP
服务器端口
服务端需要:
绑定端口
准备接收
2.3 客户端核心步骤
客户端完整流程:
1. 创建 DatagramSocket
2. 准备字符串
3. 字符串 → UTF-8 byte[]
4. 获取服务端 InetAddress
5. 创建 DatagramPacket
6. send(packet)
7. 关闭 DatagramSocket
核心链:
String
↓
byte[]
↓
DatagramPacket
↓
DatagramSocket.send()
↓
UDP
2.4 服务端核心步骤
服务端:
1. 创建 DatagramSocket(8080)
2. 创建 byte[] 缓冲区
3. 创建接收 DatagramPacket
4. receive(packet)
5. 获取实际长度
6. byte[] → String
7. 获取发送方 IP
8. 获取发送方端口
9. 关闭 Socket
核心链:
UDP
↓
DatagramSocket.receive()
↓
DatagramPacket
↓
byte[]
↓
String
三、使用方法
3.1 开发 UDP 服务端
先编写:
UDPServer
完整代码:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
public class UDPServer {
public static void main(String[] args) throws Exception {
System.out.println("=== UDP 服务端启动 ===");
try (DatagramSocket socket =
new DatagramSocket(8080)) {
// 1. 准备接收缓冲区
byte[] buffer = new byte[1024];
// 2. 创建接收数据包
DatagramPacket packet =
new DatagramPacket(
buffer,
buffer.length
);
// 3. 等待一个 UDP 数据报
socket.receive(packet);
// 4. 获取真正收到的数据
String message =
new String(
packet.getData(),
packet.getOffset(),
packet.getLength(),
StandardCharsets.UTF_8
);
// 5. 获取发送方信息
String clientIp =
packet.getAddress()
.getHostAddress();
int clientPort =
packet.getPort();
System.out.println(
"收到消息:" + message
);
System.out.println(
"发送方:" +
clientIp +
":" +
clientPort
);
}
}
}
3.2 第一步:创建接收端
DatagramSocket socket =
new DatagramSocket(8080);
含义:
创建 UDP Socket
+
绑定当前主机 UDP 8080 端口
此时程序相当于告诉操作系统:
发往本机 UDP 8080 的数据报,可以交给我处理。
3.3 第二步:创建缓冲区
byte[] buffer =
new byte[1024];
它用于:
保存接收到的数据
现在里面还没有真正的网络消息。
3.4 第三步:创建接收 Packet
DatagramPacket packet =
new DatagramPacket(
buffer,
buffer.length
);
此时:
packet
只是:
一个准备接收 UDP 数据的容器
3.5 第四步:receive()
socket.receive(packet);
这是整个服务端最核心的一句。
执行到这里:
如果没有数据:
阻塞
如果收到数据:
填充 packet
然后继续运行
因此如果你先运行服务端,控制台可能停在:
=== UDP 服务端启动 ===
看起来“没反应”。
实际上:
服务端正在正常等待客户端发送数据。
3.6 第五步:解析数据
接收到数据之后:
packet.getLength()
就是:
实际收到的数据长度
因此使用:
new String(
packet.getData(),
packet.getOffset(),
packet.getLength(),
StandardCharsets.UTF_8
);
进行解码。
这里同时复习了之前的:
字符编码
UTF-8
byte[]
String
3.7 第六步:获取发送方信息
UDP 接收完成后,DatagramPacket 不只是保存数据。
还保存:
这份数据是谁发来的?
获取 IP:
packet.getAddress()
进一步:
packet.getAddress()
.getHostAddress()
获得 IP 文本。
获取端口:
packet.getPort()
于是可以输出:
发送方:
127.0.0.1:52341
其中:
52341
就是客户端被操作系统分配的某个临时 UDP 端口。
四、开发 UDP 客户端
4.1 完整客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class UDPClient {
public static void main(String[] args) throws Exception {
System.out.println("=== UDP 客户端启动 ===");
try (DatagramSocket socket =
new DatagramSocket()) {
// 1. 准备发送数据
String message =
"你好,我是 UDP 客户端";
byte[] data =
message.getBytes(
StandardCharsets.UTF_8
);
// 2. 获取服务端地址
InetAddress serverAddress =
InetAddress.getLoopbackAddress();
// 3. 创建发送数据包
DatagramPacket packet =
new DatagramPacket(
data,
data.length,
serverAddress,
8080
);
// 4. 发送
socket.send(packet);
System.out.println(
"发送完成,本地端口:" +
socket.getLocalPort()
);
}
}
}
4.2 创建客户端 Socket
DatagramSocket socket =
new DatagramSocket();
客户端没有手工指定端口。
操作系统会自动选择:
Ephemeral Port
临时端口
例如:
52678
所以:
客户端:
127.0.0.1:52678
服务端:
127.0.0.1:8080
4.3 字符串编码
发送:
String message =
"你好,我是 UDP 客户端";
网络需要:
byte[]
因此:
byte[] data =
message.getBytes(
StandardCharsets.UTF_8
);
形成:
String
↓
UTF-8 编码
↓
byte[]
4.4 为什么显式使用 UTF-8
也可以写:
message.getBytes();
但是它会使用:
默认字符集
如果发送端和接收端字符集不一致:
编码 UTF-8
↓
解码 GBK
↓
乱码
因此网络协议中更推荐明确约定:
UTF-8
客户端:
getBytes(StandardCharsets.UTF_8)
服务端:
new String(
...,
StandardCharsets.UTF_8
)
形成:
UTF-8 编码
↓
网络
↓
UTF-8 解码
4.5 获取服务端地址
本机测试时使用:
InetAddress serverAddress =
InetAddress.getLoopbackAddress();
表示:
发给当前计算机自身
如果服务端运行在另一台局域网电脑:
InetAddress serverAddress =
InetAddress.getByName(
"192.168.1.20"
);
这正是上一章:
InetAddress
在真正网络通信中的第一次使用。
4.6 创建发送数据包
DatagramPacket packet =
new DatagramPacket(
data,
data.length,
serverAddress,
8080
);
这一个对象已经完整表达:
发什么:
data
发多少:
data.length
发给谁:
serverAddress
发到哪个服务:
8080
然后:
socket.send(packet);
就可以发送。
五、完整运行流程
5.1 第一次测试建议先启动服务端
先运行:
UDPServer
控制台:
=== UDP 服务端启动 ===
然后程序停在那里等待。
这不是卡死,而是:
socket.receive(packet);
正在阻塞。
5.2 再启动客户端
运行:
UDPClient
客户端可能输出:
=== UDP 客户端启动 ===
发送完成,本地端口:52678
5.3 服务端解除阻塞
数据到达:
Client
│
│ Datagram
▼
Server:8080
于是:
receive()
返回。
服务端可能输出:
收到消息:你好,我是 UDP 客户端
发送方:127.0.0.1:52678
然后程序结束。
5.4 整体流程图
UDPClient
│
│ new DatagramSocket()
▼
随机本地端口
│
│ String
▼
UTF-8 编码
│
▼
byte[]
│
▼
DatagramPacket
│
│ 目标:
│ 127.0.0.1:8080
▼
socket.send()
│
════════════ UDP ════════════
│
▼
UDPServer : 8080
│
│ socket.receive()
▼
DatagramPacket
│
├── byte[]
├── length
├── sender IP
└── sender port
│
▼
UTF-8 解码
│
▼
String
这就是 Java UDP 最基础、也最重要的一条完整通信链。
六、原理与进阶
6.1 为什么最好先启动服务端
TCP 后面会发现:
服务端没启动
↓
客户端连接失败
UDP 不一样。
因为 UDP:
无连接
客户端:
socket.send(packet);
并不需要先跟服务器进行 TCP 式连接建立。
因此即使服务端当前没有运行:
socket.send(packet);
也可能正常执行完成。
但是:
数据报可能直接丢失
客户端不能仅根据:
send() 没报错
判断:
服务端已经收到消息。
这是理解 UDP 非常关键的一点。
6.2 DatagramSocket 的发送端也有地址
假设客户端:
new DatagramSocket();
操作系统分配:
52341
那么数据报大致存在:
源地址:
127.0.0.1:52341
目标地址:
127.0.0.1:8080
服务端正因为收到了源信息,才能通过:
packet.getAddress();
packet.getPort();
知道:
是谁发来的
后面如果需要“回复客户端”,这些信息会非常重要。
6.3 try-with-resources 又出现了
本章代码使用:
try (DatagramSocket socket =
new DatagramSocket()) {
}
因为:
DatagramSocket
实现了可关闭资源相关接口。
因此可以结合之前 IO 学习的:
try-with-resources
自动关闭资源。
这说明 JavaSE 的知识已经不是孤岛:
异常
+
IO
+
字符编码
+
InetAddress
+
网络编程
正在逐渐组合成完整程序。
七、常见问题
7.1 服务端运行后为什么一直不结束?
因为:
socket.receive(packet);
正在阻塞等待数据。
运行客户端发送一次以后,它才会继续。
7.2 客户端为什么一闪就结束?
因为本章客户端只有:
创建
发送一次
关闭
没有循环。
这是:
单发单收
的正常结果。
09-05 才会加入:
while
实现持续发送与接收。
7.3 客户端端口为什么每次可能不同?
因为:
new DatagramSocket();
让操作系统自动选择可用端口。
所以可能:
第一次:52341
第二次:53892
第三次:60127
这是正常现象。
7.4 为什么服务端端口通常固定为 8080?
因为客户端必须提前知道:
数据应该发送到哪里
如果服务端端口每次随机变化,客户端无法稳定定位服务。
7.5 为什么服务端出现 BindException?
一种典型原因是:
UDP 8080
已经被其他 Socket 占用。
例如:
UDPServer A
绑定 8080
此时再启动第二个相同程序并尝试绑定相同本地 UDP 端点,就可能失败。
首先检查:
是否已经启动了另一个服务端实例。
7.6 客户端发送了,服务端为什么没有消息?
依次检查:
1. 服务端是否启动?
2. IP 是否正确?
3. 客户端发送端口是不是 8080?
4. 服务端绑定端口是不是 8080?
5. 是否被防火墙阻止?
6. 两台机器是否真的网络互通?
7. 如果跨机器通信,是否错误使用了 127.0.0.1?
尤其注意:
127.0.0.1
永远指:
当前这台机器自己。
如果:
客户端在电脑 A
服务端在电脑 B
客户端写:
127.0.0.1:8080
访问的是:
电脑 A 自己
而不是电脑 B。
7.7 为什么会乱码?
通常检查:
发送端字符集
和
接收端字符集
是否一致。
推荐:
UTF-8 → UTF-8
而不是依赖各自系统默认字符集。
7.8 为什么不能使用 buffer.length 作为实际消息长度?
因为:
buffer.length
表示:
整个缓冲区容量
而:
packet.getLength()
才表示:
本次 UDP 数据报实际收到的长度
7.9 UDP 服务端知道是谁发送的吗?
知道。
接收成功后:
packet.getAddress()
获得:
发送方 IP
packet.getPort()
获得:
发送方 UDP 端口
这也是后续实现:
服务端回复客户端
的基础。
八、练习与验收
8.1 知识问答
- UDP 单发单收是什么?
- 客户端为什么通常使用
new DatagramSocket()? - 服务端为什么使用
new DatagramSocket(8080)? - 客户端发送数据必须提供哪些信息?
receive()为什么会阻塞?- 接收完成后如何获得实际消息长度?
- 如何得到发送方 IP?
- 如何得到发送方端口?
- 为什么客户端
send()成功不能证明服务端收到? - 为什么推荐网络程序显式使用 UTF-8?
- 为什么跨电脑通信不能把服务端地址写成客户端自己的
127.0.0.1?
8.2 代码阅读
不运行,阅读:
byte[] data =
"Hello"
.getBytes(
StandardCharsets.UTF_8
);
DatagramPacket packet =
new DatagramPacket(
data,
data.length,
InetAddress.getLoopbackAddress(),
8080
);
回答:
data保存什么?data.length表示什么?getLoopbackAddress()表示什么?8080是客户端还是服务端端口?- 这个 Packet 是发送 Packet 还是接收 Packet?
8.3 手写代码
关闭 AI 自动补全。
任务一:客户端
从零实现:
UDPClient
要求:
发送:
"星雨笔录 UDP Test"
到:
127.0.0.1:9999
要求显式使用:
UTF-8
任务二:服务端
从零实现:
UDPServer
绑定:
9999
接收一次消息,并输出:
消息正文
发送方 IP
发送方端口
实际字节数
8.4 Debug
下面程序为什么可能无法与另一台电脑通信?
DatagramPacket packet =
new DatagramPacket(
data,
data.length,
InetAddress.getLoopbackAddress(),
8080
);
已知:
客户端在电脑 A
服务端在电脑 B:
192.168.1.20
请指出问题并修复。
再观察:
byte[] buffer =
new byte[1024];
DatagramPacket packet =
new DatagramPacket(
buffer,
buffer.length
);
socket.receive(packet);
String message =
new String(
buffer,
StandardCharsets.UTF_8
);
程序虽然可能打印出内容,但设计上存在什么问题?
要求使用:
packet.getLength()
完成修正。
8.5 综合训练
设计如下实验:
服务端:
UDP 9999
客户端:
随机端口
客户端发送:
姓名
当前时间
一句自定义消息
服务端输出:
============================
来自:127.0.0.1:xxxxx
长度:xx bytes
内容:...
============================
要求独立完成,不查看本章完整代码。
8.6 本章验收
如果你能够关闭资料,从零手写:
UDPClient.java
UDPServer.java
并正确使用:
DatagramSocket
DatagramPacket
InetAddress
StandardCharsets.UTF_8
send()
receive()
getLength()
getAddress()
getPort()
同时能够完整解释:
“一条字符串消息到底是怎样从客户端 Java 程序变成 UDP 数据报,并最终在服务端恢复为字符串的?”
那么 UDP 单发单收已经真正掌握。
最终模型必须能闭卷画出来:
客户端 String
↓
UTF-8 Encode
↓
byte[]
↓
DatagramPacket
↓
DatagramSocket.send()
↓
UDP
↓
DatagramSocket.receive()
↓
DatagramPacket
↓
byte[]
↓
UTF-8 Decode
↓
服务端 String