UDP 报文格式(共 8 字节头部 + 数据)
| 字段(Field) | 长度(bits) | 说明 |
|---|---|---|
| 源端口号(Source Port) | 16 | 发送方的端口号(可选,若不使用可设为 0) |
| 目的端口号(Destination Port) | 16 | 接收方的端口号(必须) |
| 长度(Length) | 16 | 整个 UDP 报文的长度(包括头部和数据),最小值为 8(仅头部) |
| 校验和(Checksum) | 16 | 用于差错检测(可选,在 IPv4 中可设为 0;在 IPv6 中为必选) |
注意:
-
所有字段均以 网络字节序(大端序)存储。
-
UDP 头部固定为 8 字节。
-
数据部分(Data/Payload)紧随头部之后,长度 = Length 字段值 - 8。
各字段详解
-
源端口(Source Port)
- 标识发送进程的端口号。
- 如果不需要对方回信,可以设为 0。
-
目的端口(Destination Port)
- 标识接收进程的端口号(如 DNS 使用 53,DHCP 使用 67/68 等)。
-
长度(Length)
- 表示整个 UDP 数据报的字节数(头部 + 数据)。
- 最小值为 8(无数据),最大理论值为 65535 字节。
- 实际受限于 IP 层 MTU(通常约 1500 字节),过大会被分片或丢弃。
-
校验和(Checksum)
- 覆盖范围:伪头部(pseudo-header)+ UDP 头部 + 数据。
- 伪头部包含:源 IP 地址、目的 IP 地址、协议号(UDP 为 17)、UDP 长度。
- 在 IPv4 中校验和是可选的(设为 0 表示未使用);在 IPv6 中必须使用。
使用 UDP 的注意事项
在 Java 中使用 UDP(通过 java.net.DatagramSocket 和 DatagramPacket)进行网络编程时,虽然比 TCP 简单,但仍有许多关键注意事项 需要关注,以确保程序的健壮性、安全性和可维护性。以下是主要注意事项的总结:
1. UDP 是无连接、不可靠的协议
-
不保证数据送达:发送的数据包可能丢失、重复或乱序。
-
无重传机制:Java 不会自动重发丢失的包。
-
应对策略:应用层需自行实现超时重传、确认机制(如简单 ACK)。对可靠性要求高的场景,考虑改用 TCP。
2. 报文大小限制
-
单个 UDP 数据报最大为 65507 字节(IPv4):
-
IP 层最大 65535 字节;
-
减去 IP 头部(20 字节)和 UDP 头部(8 字节) → 65507。
-
实际建议远小于此值(通常 ≤ 1472 字节):
-
避免 IP 分片(MTU 通常为 1500,减去 IP+UDP 头 = 1472)。
-
分片后任一片丢失会导致整个 UDP 报文丢弃。
3. 接收缓冲区必须足够大
在 Java UDP 编程中,接收缓冲区(receive buffer) 是指用于存储从网络接收到的 UDP 数据报的字节数组。
-
如果接收缓冲区小于实际收到的 UDP 报文,多余字节会被截断且无法恢复。
-
务必检查
packet.getLength()获取实际接收到的数据长度:javasocket.receive(packet); int len = packet.getLength(); // 实际数据长度 byte[] data = Arrays.copyOf(packet.getData(), len);
java
import java.net.*;
import java.nio.charset.StandardCharsets;
public class UdpReceiver {
public static void main(String[] args) throws Exception {
byte[] buffer = new byte[1024]; // 接收缓冲区
DatagramSocket socket = new DatagramSocket(9999);
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 阻塞直到收到数据
// ⭐ 关键:使用 getLength() 获取实际接收到的字节数
int actualLen = packet.getLength();
String message = new String(packet.getData(), 0, actualLen, StandardCharsets.UTF_8);
System.out.println("Received: " + message);
System.out.println("From: " + packet.getAddress() + ":" + packet.getPort());
}
}
}
4. DatagramPacket 的"状态复用"陷阱
-
DatagramPacket对象在多次receive()调用中复用内部缓冲区。 -
若未正确处理
getLength()和getOffset(),可能读到旧数据。 -
建议 :每次接收后基于
getLength()提取有效数据。
5. 绑定端口与权限
-
绑定 1024 以下端口 (如 53、67)通常需要 root/Admin 权限。
-
若端口已被占用,
new DatagramSocket(port)会抛出BindException。 -
可让系统自动分配端口:
java
DatagramSocket socket = new DatagramSocket(); // 随机可用端口
6. 多线程安全问题
-
DatagramSocket不是线程安全的。 -
多个线程同时调用
send()或receive()可能导致异常或数据错乱。 -
解决方案:
-
使用同步块;
-
每个线程使用独立 socket(适用于客户端);
-
使用单接收线程 + 队列处理(推荐服务器模型)。
-
7. 超时设置避免永久阻塞
-
socket.receive() 默认永久阻塞。
-
应设置超时(setSoTimeout())避免程序卡死:
java
socket.setSoTimeout(5000); // 5秒超时
try {
socket.receive(packet);
} catch (SocketTimeoutException e) {
// 超时处理
}
8. 关闭资源
-
使用完毕必须调用
socket.close(),否则端口可能被占用。 -
建议使用 try-with-resources(但
DatagramSocket仅从 Java 9 开始实现AutoCloseable):
java
// Java 9+
try (DatagramSocket socket = new DatagramSocket(port)) {
// ...
}
总结
| 注意点 | 关键建议 |
|---|---|
| 可靠性 | 自行处理丢包、重传 |
| 报文大小 | ≤1472 字节,避免分片 |
| 缓冲区 | 用 getLength() 获取真实长度 |
| 阻塞 | 设置 SoTimeout |
| 线程安全 | 避免多线程共享 socket |
| 资源管理 | 及时 close() |