UDP(User Datagram Protocol)以其无连接、低延迟、轻量级的特性,在特定场景下比 TCP 更加高效。理解 UDP 的适用边界与工程实践,是高并发系统的必备技能。
一、UDP 协议特点
1.1 UDP vs TCP 对比
| 特性 | UDP | TCP |
|---|---|---|
| 连接 | 无连接 | 面向连接 |
| 可靠性 | 不保证 | 可靠传输 |
| 顺序 | 不保证 | 有序交付 |
| 拥塞控制 | 无 | 有 |
| 头部开销 | 8 bytes | 20-60 bytes |
| 延迟 | 低(无握手) | 稍高 |
| 传输效率 | 高 | 中等 |
| 应用场景 | 视频、游戏、DNS | 文件、网页、API |
1.2 UDP 头部结构
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source Port | Dest Port |
+--------+--------+--------+--------+
| Length | Checksum |
+--------+--------+--------+--------+
| Data (Variable) |
+--------+--------+--------+--------+
- Source Port: 发送方端口(可选)
- Dest Port: 接收方端口
- Length: UDP 头部 + 数据总长度
- Checksum: 校验和(IPv4 可选,IPv6 强制)
二、Java UDP 编程
2.1 基础通信
// UDP 服务端
public class UdpServer {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(9876);
byte[] buffer = new byte[1024];
System.out.println("UDP 服务端启动,监听端口 9876...");
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 阻塞接收
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("收到来自 " + packet.getAddress() + ":" + packet.getPort()
+ " 的消息: " + received);
// 发送响应
String response = "Echo: " + received;
DatagramPacket reply = new DatagramPacket(
response.getBytes(),
response.length(),
packet.getAddress(),
packet.getPort()
);
socket.send(reply);
}
}
}
// UDP 客户端
public class UdpClient {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
InetAddress serverAddress = InetAddress.getByName("localhost");
String message = "Hello UDP";
DatagramPacket packet = new DatagramPacket(
message.getBytes(),
message.length(),
serverAddress,
9876
);
socket.send(packet);
byte[] buffer = new byte[1024];
DatagramPacket response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
System.out.println("收到响应: " + new String(response.getData(), 0, response.getLength()));
socket.close();
}
}
2.2 基于 Netty 的高性能 UDP
public class NettyUdpServer {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.handler(new ChannelInitializer<NioDatagramChannel>() {
@Override
protected void initChannel(NioDatagramChannel ch) {
ch.pipeline().addLast(new UdpServerHandler());
}
});
ChannelFuture future = bootstrap.bind(9876).sync();
future.channel().closeFuture().await();
} finally {
group.shutdownGracefully();
}
}
}
@ChannelHandler.Sharable
public class UdpServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) {
ByteBuf content = packet.content();
String message = content.toString(CharsetUtil.UTF_8);
System.out.println("收到: " + message + " 来自 " + packet.sender());
// 回复
ByteBuf response = Unpooled.copiedBuffer("Echo: " + message, CharsetUtil.UTF_8);
ctx.writeAndFlush(new DatagramPacket(response, packet.sender()));
}
}
三、组播与广播
3.1 IP 组播地址
| 范围 | 地址 | 说明 |
|---|---|---|
| 保留 | 224.0.0.0 ~ 224.0.0.255 | 本地链路组播(路由协议等) |
| 全球 | 224.0.1.0 ~ 238.255.255.255 | 全局有效组播 |
| 管理 | 239.0.0.0 ~ 239.255.255.255 | 私有组播(类似 192.168.x.x) |
3.2 Java 组播编程
public class MulticastServer {
public static void main(String[] args) throws Exception {
MulticastSocket socket = new MulticastSocket(4446);
InetAddress group = InetAddress.getByName("230.0.0.1");
// 加入组播组
socket.joinGroup(group);
System.out.println("已加入组播组 230.0.0.1:4446");
byte[] buffer = new byte[1024];
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength());
System.out.println("收到组播消息: " + message);
if ("exit".equals(message)) {
break;
}
}
socket.leaveGroup(group);
socket.close();
}
}
public class MulticastClient {
public static void main(String[] args) throws Exception {
MulticastSocket socket = new MulticastSocket();
InetAddress group = InetAddress.getByName("230.0.0.1");
String message = "Hello Multicast Group!";
DatagramPacket packet = new DatagramPacket(
message.getBytes(),
message.length(),
group,
4446
);
socket.send(packet);
socket.close();
}
}
四、UDP 的可靠性增强
4.1 KCP 协议(可靠 UDP)
KCP 是一个快速可靠协议,以牺牲部分带宽为代价换取低延迟:
| 特性 | KCP | TCP |
|---|---|---|
| 平均延迟 | 30-40% 降低 | 基准 |
| 带宽浪费 | 10-20% | 无 |
| 配置灵活 | 可自主调整 | 由系统控制 |
| 实现 | 用户态 | 内核态 |
// Java 版 KCP 使用示例(kcp-netty)
KcpClient kcpClient = new KcpClient();
kcpClient.init(new ChannelConfig(), new KcpListener() {
@Override
public void onConnected(Ukcp kcp) {
ByteBuf data = Unpooled.wrappedBuffer("Hello KCP".getBytes());
kcp.write(data);
}
@Override
public void onReceive(ByteBuf byteBuf, Ukcp kcp) {
// 处理收到的数据
}
});
4.2 自定义可靠机制
public class ReliableUdpPacket {
private long sequenceNumber; // 序列号
private long timestamp; // 发送时间
private byte[] payload; // 数据
private boolean ack; // 是否为 ACK 包
// 发送端:记录已发送,等待 ACK
private Map<Long, PacketEntry> pendingPackets = new ConcurrentHashMap<>();
private AtomicLong nextSeq = new AtomicLong(0);
public void send(byte[] data) {
long seq = nextSeq.incrementAndGet();
ReliableUdpPacket packet = new ReliableUdpPacket(seq, System.currentTimeMillis(), data, false);
pendingPackets.put(seq, new PacketEntry(packet));
udpSend(serialize(packet));
// 启动重传定时器
scheduleRetransmit(seq);
}
private void scheduleRetransmit(long seq) {
scheduler.schedule(() -> {
PacketEntry entry = pendingPackets.get(seq);
if (entry != null && !entry.isAcked()) {
if (entry.getRetryCount() < MAX_RETRIES) {
entry.incrementRetry();
udpSend(serialize(entry.getPacket()));
scheduleRetransmit(seq);
}
}
}, RETRANSMIT_TIMEOUT, TimeUnit.MILLISECONDS);
}
// 接收端:发送 ACK
public void onReceive(ReliableUdpPacket packet) {
if (!packet.isAck()) {
sendAck(packet.getSequenceNumber());
deliverToApplication(packet);
} else {
PacketEntry entry = pendingPackets.remove(packet.getSequenceNumber());
if (entry != null) entry.setAcked(true);
}
}
}
五、UDP 典型应用场景
5.1 DNS 查询
// 使用 UDP 发送 DNS 查询(A 记录)
DatagramSocket socket = new DatagramSocket();
InetAddress dnsServer = InetAddress.getByName("8.8.8.8");
// 构建 DNS 查询包(简化)
byte[] query = buildDnsQuery("example.com", RecordType.A);
DatagramPacket packet = new DatagramPacket(query, query.length, dnsServer, 53);
socket.send(packet);
byte[] response = new byte[512]; // UDP DNS 限制 512 bytes
DatagramPacket reply = new DatagramPacket(response, response.length);
socket.receive(reply);
// 解析响应...
5.2 实时音视频
音视频通话中的 UDP 应用:
发送端编码 接收端解码
┌──────────┐ ┌──────────┐
│ 视频帧 │ → RTP over UDP (媒体端口) → │ 缓冲区 │
│ (H.264) │ │ (Jitter) │
└──────────┘ └────┬─────┘
│
┌──────────┐ ┌────▼─────┐
│ 音频帧 │ → RTP over UDP (媒体端口) → │ 播放 │
│ (Opus) │ └──────────┘
└──────────┘
控制信令:TCP/WebSocket(RTCP 反馈质量)
└──────────────────────────────────────────────┘
六、总结
| 场景 | 选择 UDP 的理由 | 需要补充的机制 |
|---|---|---|
| DNS | 单次请求-响应,无需连接 | 超时重传 |
| 实时游戏 | 低延迟优先,容忍丢包 | 状态同步、预测补偿 |
| 视频直播 | 实时性 > 完整性 | FEC 前向纠错 |
| IoT 上报 | 低功耗、短数据 | 应用层确认 |
| QUIC | 基于 UDP 消除队头阻塞 | 完整可靠传输机制 |
UDP 的"不可靠"不是缺陷,而是一种设计取舍。在需要实时性的场景中,与其等待 TCP 的重传,不如接受偶尔的丢包,这正是视频通话、在线游戏等应用的核心逻辑。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。