UDP 通信原理与 Datagram API | JavaSE
UDP 通信原理与 Datagram API
一、学习目标
学完本章,你应该能够:
- 能够解释 UDP 的核心通信模型以及“无连接、面向数据报”的含义。
- 能够说明 UDP 为什么不能保证数据一定到达、按序到达或只到达一次。
- 能够区分
DatagramSocket与DatagramPacket的职责。 - 能够掌握 UDP 发送端和接收端创建
DatagramSocket的基本方式。 - 能够掌握发送数据包和接收数据包的两种
DatagramPacket基本构造方式。 - 能够解释
send()、receive()的工作流程以及receive()的阻塞特征。 - 能够从收到的
DatagramPacket中获得实际数据长度、发送方 IP 和发送方端口。
二、核心知识
2.1 什么是 UDP
UDP 全称:
User Datagram Protocol
用户数据报协议
它是计算机网络传输层的重要协议之一。
上一章已经建立:
网络通信
=
IP 地址
+
端口
+
通信协议
现在开始真正选择:
通信协议 = UDP
UDP 最核心的通信思想可以理解为:
应用程序把一份完整的数据封装成一个数据报,然后直接交给网络发送。
模型:
发送方
│
│ Datagram
▼
网络
│
▼
接收方
发送之前不需要像 TCP 那样先建立一条可靠连接。
2.2 UDP 的核心特点
UDP 最重要的几个特点:
无连接
面向数据报
协议机制简单
开销较小
不保证可靠交付
无连接
发送 UDP 数据之前:
不需要:
客户端 ←→ 服务端
先建立 TCP 连接
发送方知道:
目标 IP
目标端口
之后就可以直接发送。
类似:
写好一张明信片
↓
写上收件地址
↓
直接寄出
不需要先打电话问:
“你准备好接收了吗?”
2.3 什么叫“不可靠”
这里的“不可靠”不是说:
UDP 完全不能用。
而是说:
UDP 协议本身不负责向应用程序提供可靠交付保证。
例如发送:
A
B
C
应用层不能单纯依靠 UDP 假设一定收到:
A
B
C
现实中可能出现:
A
C
即:
B 丢失
也可能:
B
A
C
即发生乱序。
应用程序也不能依赖 UDP 提供 TCP 那样的:
- 确认机制
- 按序字节流
- 丢失自动重传
- 重复数据保护
如果业务需要这些能力:
方案一:
应用层自行设计
方案二:
选择 TCP 等提供可靠传输能力的协议
2.4 什么叫“面向数据报”
UDP 是:
Message / Datagram Oriented
可以简单理解为:
一次发送的一份数据,在 UDP 层具有明确的数据报边界。
例如:
第一次 send:
"Hello"
第二次 send:
"Java"
第三次 send:
"UDP"
对应三个 UDP 数据报:
[Hello]
[Java]
[UDP]
这和后面 TCP 的:
连续字节流
模型存在明显区别。
因此学习 UDP 时,一个非常重要的对象就是:
DatagramPacket
即:
数据报包。
2.5 UDP 数据报中有什么
从应用程序角度,一个 UDP 数据报至少需要关注:
数据
目标 IP
目标端口
发送出去后,接收方还可以知道:
发送方 IP
发送方端口
可以建立下面这个模型:
┌─────────────────────┐
│ UDP 数据报 │
├─────────────────────┤
│ 数据 │
│ 源地址 / 源端口 │
│ 目标地址 / 目标端口 │
└─────────────────────┘
UDP 协议头本身包含:
源端口
目标端口
长度
校验和
Java 初学阶段不需要手动构造 UDP 协议头。
这些底层工作由:
JDK
操作系统网络协议栈
负责。
我们主要操作:
DatagramSocket
DatagramPacket
2.6 Java 如何实现 UDP
Java 在:
java.net
包中提供两个核心类:
DatagramSocket
DatagramPacket
关系非常重要:
DatagramSocket
│
│ send / receive
▼
DatagramPacket
可以简单类比:
DatagramSocket = 发快递、收快递的窗口
DatagramPacket = 快递包裹
不过真正需要记住的是职责:
| 类 | 主要职责 |
| ---------------- | ---------------------------------- |
| DatagramSocket | UDP 通信端点,负责发送和接收数据报 |
| DatagramPacket | 封装一次发送或接收的数据报信息 |
2.7 DatagramSocket
导包:
import java.net.DatagramSocket;
DatagramSocket 表示:
一个用于发送和接收 UDP 数据报的 Socket。
2.8 创建发送端 DatagramSocket
最常见:
DatagramSocket socket = new DatagramSocket();
这个构造器会:
创建 UDP Socket
↓
绑定本机某个可用端口
端口通常由操作系统自动选择。
例如可能得到:
54321
因此发送端并不是“没有端口”。
它同样拥有:
本地 IP
+
本地端口
只不过我们没有手动指定端口,而是让操作系统选择。
可以查看:
socket.getLocalPort();
2.9 创建指定端口的 DatagramSocket
接收端通常需要别人知道:
我要往哪个端口发?
因此常见写法:
DatagramSocket socket =
new DatagramSocket(8080);
表示:
当前 UDP Socket
绑定本机 UDP 8080 端口
之后发送方就可以把数据发送到:
目标 IP:8080
2.10 为什么接收端通常指定端口
假设接收端每次启动都随机端口:
这次:53121
下次:60234
那么发送方很难提前知道:
应该发送到哪里?
因此服务器通常使用约定好的端口:
8080
8888
9999
而客户端发送端可以让操作系统分配临时端口。
于是最基本模型是:
客户端
随机本地端口
│
│ UDP
▼
服务端
固定端口 8080
2.11 DatagramSocket 的核心方法
两个最重要的方法:
void send(DatagramPacket packet)
作用:
发送一个 UDP 数据报
以及:
void receive(DatagramPacket packet)
作用:
接收一个 UDP 数据报
因此 UDP 程序最核心的代码最终就是:
socket.send(packet);
和:
socket.receive(packet);
2.12 DatagramPacket
导包:
import java.net.DatagramPacket;
DatagramPacket 用于表示:
一个 UDP 数据报。
但是发送和接收时,创建方式不同。
2.13 创建发送数据包
发送数据时必须知道:
我要发什么?
发多少字节?
发给谁?
发到哪个端口?
因此常见构造器:
DatagramPacket(
byte[] buf,
int length,
InetAddress address,
int port
)
例如:
byte[] data = ...;
DatagramPacket packet =
new DatagramPacket(
data,
data.length,
targetAddress,
8080
);
四个参数分别表示:
data
↓
真正的数据
data.length
↓
本次发送多少字节
targetAddress
↓
目标 IP
8080
↓
目标 UDP 端口
2.14 为什么网络数据通常使用 byte[]
你会再次看到:
byte[]
因为网络传输最终处理的是:
字节
因此字符串:
String message = "你好";
需要先编码:
byte[] data =
message.getBytes(StandardCharsets.UTF_8);
知识链:
String
↓
字符编码
↓
byte[]
↓
DatagramPacket
↓
网络
这正是之前:
字符集
编码与解码
IO
知识在网络编程中的实际应用。
2.15 创建接收数据包
接收数据之前,我们并不知道对方到底会发送什么内容。
因此需要先准备:
接收缓冲区
例如:
byte[] buffer = new byte[1024];
然后创建接收数据包:
DatagramPacket packet =
new DatagramPacket(
buffer,
buffer.length
);
这里暂时不需要:
目标 IP
目标端口
因为这个对象现在的任务不是:
告诉 Java 发给谁
而是:
准备一个空间接收别人发来的数据
2.16 receive() 为什么会阻塞
调用:
socket.receive(packet);
如果当前没有 UDP 数据到达:
线程不会立即继续执行
而是:
等待
等待
等待
...
直到:
收到数据报
这就是:
阻塞(Blocking)
程序结构:
执行 receive()
↓
有没有数据?
┌──┴──┐
│ │
没有 有
│ │
等待 ▼
填充 packet
↓
方法返回
这与之前学习线程状态时涉及的阻塞思想已经开始连接起来。
2.17 接收之后 packet 发生了什么
执行:
socket.receive(packet);
之前:
packet
只是准备好的空接收容器
执行之后:
packet
↓
收到的数据
+
发送方 IP
+
发送方端口
+
实际数据长度
因此可以使用:
packet.getLength();
获取:
本次实际收到多少字节
使用:
packet.getAddress();
获取:
发送方 IP 地址
使用:
packet.getPort();
获取:
发送方端口
2.18 为什么不能直接把整个 buffer 转成字符串
假设:
byte[] buffer = new byte[1024];
对方实际只发送:
12 字节
那么:
1024
只是缓冲区容量。
真正有效的数据只有:
0 ~ 11
因此错误思路:
new String(buffer, StandardCharsets.UTF_8);
可能把没有有效数据的缓冲区剩余部分也考虑进去。
应该获取:
int length = packet.getLength();
然后:
String message =
new String(
packet.getData(),
packet.getOffset(),
length,
StandardCharsets.UTF_8
);
核心思想:
缓冲区容量
≠
本次真正收到的数据长度
三、使用方法
3.1 UDP 发送端基本流程
发送 UDP 数据的标准思路:
1. 创建 DatagramSocket
2. 准备数据
3. 获取目标 InetAddress
4. 创建 DatagramPacket
5. socket.send(packet)
6. 关闭 Socket
伪代码:
DatagramSocket socket = ...
byte[] data = ...
DatagramPacket packet = ...
socket.send(packet);
socket.close();
完整程序将在下一章实现。
3.2 UDP 接收端基本流程
接收端:
1. 创建 DatagramSocket
并绑定固定端口
2. 创建 byte[] 接收缓冲区
3. 创建 DatagramPacket
4. socket.receive(packet)
5. 获取实际长度
6. 解码数据
7. 获取发送方地址、端口
8. 关闭 Socket
核心模型:
DatagramSocket
│
receive(packet)
│
▼
DatagramPacket
│
├── 数据
├── 长度
├── 发送方 IP
└── 发送方端口
3.3 DatagramPacket 常用 API
| API | 作用 |
| -------------- | ------------------------------ |
| getData() | 获取底层字节数组 |
| getLength() | 获取数据报当前有效数据长度 |
| getOffset() | 获取数据在缓冲区中的起始偏移量 |
| getAddress() | 获取发送方或目标方 IP 地址 |
| getPort() | 获取发送方或目标方端口 |
其中最常用:
packet.getLength();
packet.getAddress();
packet.getPort();
四、原理与进阶
4.1 UDP 数据报具有边界
假设发送端执行:
send(A)
send(B)
send(C)
这是三个独立 UDP 数据报。
接收端每次:
receive(packet);
接收的是:
一个数据报。
UDP 不像 TCP 那样向 Java 程序提供一个连续的字节流。
这是后续区分 UDP 和 TCP 的核心之一:
UDP
面向数据报
TCP
面向字节流
4.2 UDP 不代表完全没有校验
“UDP 不可靠”不应该理解成:
UDP 对数据什么都不做。
UDP 协议头本身存在:
Checksum
校验和
“不可靠”真正强调的是:
UDP 不向应用层提供 TCP 那种完整的可靠、有序、重传式交付保证。
4.3 UDP 数据大小到底是不是 64KB
入门课程经常简化成:
UDP 一次最多发送 64KB
它表达了一个正确方向:
UDP 单个数据报的大小存在限制,不能无限大。
更严谨地看,UDP 头中的 Length 字段为 16 位,并且该长度包括:
UDP Header
+
UDP Data
UDP Header 本身占:
8 Byte
在普通 IPv4 数据报中,还要考虑 IPv4 头部,因此理论最大 UDP 应用数据通常写作:
65507 Byte
而不是应用层可以随意发送完整的:
65535 Byte
更重要的是:
实际开发中通常不应该追求接近 UDP 理论最大值。
因为较大的 IP 数据报可能产生:
IP 分片
而任意一个分片丢失都可能导致整个数据报无法正常重组。
因此本阶段重点不是背:
65507
而是理解:
UDP 是一包一包发送
+
单个数据报大小有限
+
实际应用通常应该控制数据报大小
4.4 DatagramSocket 为什么也有 connect()
UDP 本身是:
Connectionless
无连接
但 Java 的:
DatagramSocket
仍然提供:
connect(...)
这并不意味着 UDP 变成了 TCP。
UDP Socket 的 connect() 主要是:
把 DatagramSocket
逻辑关联到一个固定远端地址
从而限制:
向谁发送
从谁接收
它不会执行 TCP 的:
三次握手
因此:
UDP 协议无连接
与:
DatagramSocket 有 connect API
并不矛盾。
JavaSE 入门阶段仍然以:
未连接 DatagramSocket
+
每个 DatagramPacket 自己携带目标地址
为主。
4.5 接收缓冲区太小会怎样
例如:
byte[] buffer = new byte[10];
但发送方发送了:
100 字节
当:
receive(packet);
接收时,如果消息超过 DatagramPacket 当前允许接收的长度:
超出的部分会被截断。
因此:
buffer 太小
并不是一个无关紧要的问题。
接收端必须根据应用层协议合理设计缓冲区。
五、实践应用
UDP 可以用于某些更关注:
低延迟
实时性
简单请求/响应
并且应用能够接受、处理部分数据丢失的场景。
但是否选择 UDP 不能简单根据:
“UDP 快”
做决定。
真正应该判断:
业务能否容忍丢失?
是否需要严格顺序?
是否需要自动重传?
消息规模如何?
延迟是否敏感?
例如实时音视频系统即使底层采用 UDP,也通常会在应用层或其他协议层加入大量复杂机制。
所以:
UDP
≠
不做可靠性设计
UDP
=
可靠性不是由基础 UDP 协议自动替应用完成
六、常见问题
6.1 DatagramSocket 和 DatagramPacket 有什么区别?
记住:
DatagramSocket
负责通信
DatagramPacket
负责封装一个数据报
或者:
Socket = 通信端点
Packet = 数据包
6.2 发送端为什么不用指定自己的端口?
不是没有端口。
执行:
new DatagramSocket();
之后系统会自动:
选择一个可用的本地端口
发送端依然具有自己的:
IP + Port
6.3 接收端为什么通常指定端口?
因为发送方必须知道:
数据应该发给哪个 UDP 服务
所以服务端一般需要稳定的:
Port
6.4 receive() 没有数据时会返回 null 吗?
不会简单返回 null。
普通情况下:
socket.receive(packet);
会:
阻塞等待数据
直到收到数据、发生异常或受到相应 Socket 配置影响。
6.5 为什么接收后一定要 getLength()?
因为:
buffer.length
表示:
缓冲区有多大
而:
packet.getLength()
表示:
本次实际收到多少有效字节
两个概念完全不同。
6.6 UDP 发送成功就代表对方收到吗?
不代表。
例如:
socket.send(packet);
正常返回,只意味着数据已经交给本地网络栈处理。
它不意味着:
对方应用一定收到了
UDP 本身没有 TCP 那种可靠确认机制。
6.7 UDP 会自动重传吗?
基础 UDP 协议不会提供 TCP 式可靠重传。
如果业务必须做到:
消息必须收到
则需要:
应用层 ACK
超时
重传
序号
去重
等机制,或者直接考虑 TCP。
七、练习与验收
7.1 知识问答
- UDP 的全称是什么?
- 什么叫 UDP 无连接?
- 什么叫 UDP 面向数据报?
- UDP 为什么被称为不可靠传输协议?
DatagramSocket与DatagramPacket分别负责什么?new DatagramSocket()是否意味着发送端没有端口?new DatagramSocket(8080)表示什么?send()与receive()分别属于哪个类?- 接收端为什么需要准备
byte[]? buffer.length和packet.getLength()有什么区别?receive()没有数据时有什么执行特征?- UDP 发送成功为什么不能证明对方一定收到?
7.2 代码阅读
阅读:
DatagramSocket socket =
new DatagramSocket(8080);
byte[] buffer =
new byte[1024];
DatagramPacket packet =
new DatagramPacket(
buffer,
buffer.length
);
socket.receive(packet);
回答:
8080是谁的端口?buffer有什么作用?- 创建
packet时为什么没有目标 IP? receive()没有数据时程序会怎样?receive()返回之后packet中新增了哪些有效信息?
7.3 手写代码
只写 API 骨架,不要求组成完整程序:
- 创建随机本地端口的
DatagramSocket。 - 创建绑定 UDP 9999 端口的
DatagramSocket。 - 将
"Hello UDP"使用 UTF-8 转换成byte[]。 - 创建向
127.0.0.1:9999发送数据的DatagramPacket。 - 创建容量为 2048 字节的接收
DatagramPacket。 - 写出发送数据的方法调用。
- 写出接收数据的方法调用。
7.4 Debug
下面代码有什么问题?
byte[] buffer = new byte[1024];
DatagramPacket packet =
new DatagramPacket(
buffer,
buffer.length
);
System.out.println(
new String(buffer)
);
要求说明:
- 为什么此时
buffer中还没有收到网络数据? - 缺少哪个核心操作?
- 收到数据后为什么也不应该简单按照整个
buffer.length解码?
7.5 综合训练
请画出:
UDP Client
↓
DatagramSocket
↓
DatagramPacket
↓
Network
↓
DatagramSocket
↓
DatagramPacket
↓
UDP Server
并在每一个节点旁边写出:
它负责什么?
要求能够解释整个数据从:
String
一直到:
对方 String
需要经历哪些转换。
7.6 本章验收
关闭资料后,能够完整口述:
UDP
↓
Datagram
↓
DatagramSocket
↓
DatagramPacket
↓
send / receive
并能够闭卷写出:
new DatagramSocket();
new DatagramSocket(8080);
new DatagramPacket(
data,
data.length,
address,
port
);
new DatagramPacket(
buffer,
buffer.length
);
socket.send(packet);
socket.receive(packet);
同时解释:
IP
端口
数据
实际长度
发送方地址
阻塞
之间的关系,即可认为本章核心知识已经掌握。