源本科技 | 码上会

Java UDP

2026/03/04
33
0

引言

UDP (User Datagram Protocol,用户数据报协议) 是一种无连接不可靠高效的传输层协议。

  • 无连接:发送数据前不需要像 TCP 那样进行“三次握手”建立连接,直接发送即可,因此延迟极低。

  • 不可靠:不保证数据一定到达,不保证顺序,丢失后不会自动重传。

  • 数据包机制:数据被封装成独立的数据报 (Datagram)。每个数据报包含源 IP、目的 IP、端口号以及数据内容。

  • 大小限制:单个 UDP 数据包的有效载荷通常限制在 64KB 以内(受限于 IP 协议的最大包大小)。

适用场景:视频会议、直播流、在线游戏、语音通话 (VoIP)、DNS 查询等对实时性要求高、能容忍少量丢包的场景。


核心工具类

Java 在 java.net 包中提供了两个核心类来实现 UDP 通信:DatagramPacket (数据包) 和 DatagramSocket (套接字)。

DatagramPacket

数据的载体

DatagramPacket 对象代表一个 UDP 数据包,它既用于发送(封装要发出的数据),也用于接收(作为容器装载收到的数据)。

DatagramSocket

通信的门户

DatagramSocket 是发送和接收数据包的端点。无论是客户端还是服务端,都需要创建这个对象。


UDP 消息收发

基础单向通信

本案例演示最简单的“一发一收”模式。

  • 发送端:构造数据,指定目标 IP 和端口,发送。

  • 接收端:绑定固定端口,等待并接收数据。

1. 发送端代码

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPSender {
    public static void main(String[] args) {
        try {
            // 1. 创建 DatagramSocket (客户端通常不需要指定端口,由系统分配)
            DatagramSocket socket = new DatagramSocket();

            // 2. 准备要发送的数据
            String message = "Hello, UDP! 这是一条测试消息。";
            byte[] data = message.getBytes("UTF-8"); // 建议指定编码

            // 3. 指定接收方的 IP 和端口
            // 127.0.0.1 表示本机,实际开发中替换为服务器真实 IP
            InetAddress serverAddress = InetAddress.getByName("127.0.0.1");
            int serverPort = 9999;

            // 4. 创建数据包 (填入数据、长度、目标 IP、目标端口)
            DatagramPacket packet = new DatagramPacket(data, data.length, serverAddress, serverPort);

            // 5. 发送数据包
            socket.send(packet);
            System.out.println("消息已发送: " + message);

            // 6. 关闭资源
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. 接收端代码

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPReceiver {
    public static void main(String[] args) {
        try {
            // 1. 创建 DatagramSocket 并绑定固定端口 (必须与发送端的目标端口一致)
            int localPort = 9999;
            DatagramSocket socket = new DatagramSocket(localPort);
            System.out.println("接收端已启动,监听端口: " + localPort);

            // 2. 准备接收数据的缓冲区 (大小需 >= 发送端数据长度,通常设为 1024 或更大)
            byte[] buffer = new byte[1024];

            // 3. 创建用于接收的数据包 (只需传入缓冲区和长度)
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

            // 4. 接收数据 (阻塞方法,直到收到数据)
            socket.receive(packet);

            // 5. 解析数据
            // 注意:必须使用 getLength() 获取实际接收到的字节数,否则会有多余的空字符
            String receivedMessage = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
            
            // 可选:打印发送方信息
            InetAddress senderIp = packet.getAddress();
            int senderPort = packet.getPort();
            
            System.out.println("接收到来自 [" + senderIp.getHostAddress() + ":" + senderPort + "] 的消息: " + receivedMessage);

            // 6. 关闭资源
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行步骤

  1. 先运行 UDPReceiver (服务端),程序会阻塞在 receive() 处等待。

  2. 再运行 UDPSender (客户端)。

  3. 观察控制台输出,接收端应打印出消息内容。


多线程多发多收

模拟聊天

在实际应用中,通信往往是持续的。为了模拟“一边发一边收”,我们需要使用多线程

  • 主线程:负责启动发送和接收逻辑。

  • 发送线程:循环发送消息。

  • 接收线程:循环接收消息。

注意:同一个 DatagramSocket 实例可以在多个线程中安全地进行 sendreceive 操作。

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class UDPChatSimulator {
    // 定义通信端口
    private static final int PORT = 8888;

    public static void main(String[] args) {
        try {
            // 创建一个 Socket,绑定端口 (既做发送也做接收)
            DatagramSocket socket = new DatagramSocket(PORT);
            System.out.println("=== UDP 聊天模拟启动 (端口: " + PORT + ") ===");

            // 1. 启动接收线程
            Thread receiveThread = new Thread(() -> {
                try {
                    while (!Thread.currentThread().isInterrupted()) {
                        byte[] buffer = new byte[1024];
                        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                        
                        // 阻塞接收
                        socket.receive(packet);
                        
                        String msg = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
                        String sender = packet.getAddress().getHostAddress() + ":" + packet.getPort();
                        System.out.println("\n[收到] 来自 " + sender + ": " + msg);
                    }
                } catch (Exception e) {
                    if (!socket.isClosed()) {
                        System.err.println("接收线程异常: " + e.getMessage());
                    }
                }
            });
            receiveThread.start();

            // 2. 启动发送线程 (模拟持续发送或用户输入)
            Thread sendThread = new Thread(() -> {
                try {
                    Scanner scanner = new Scanner(System.in);
                    InetAddress targetAddress = InetAddress.getByName("127.0.0.1");
                    
                    while (!Thread.currentThread().isInterrupted()) {
                        System.out.print("请输入消息 (输入 'exit' 退出): ");
                        String input = scanner.nextLine();
                        
                        if ("exit".equalsIgnoreCase(input)) {
                            break;
                        }

                        byte[] data = input.getBytes("UTF-8");
                        // 构造发送包:目标是本机同端口 (模拟自发自收或与同一机器另一程序通信)
                        // 实际场景中 targetAddress 和 PORT 应是对方的地址
                        DatagramPacket packet = new DatagramPacket(data, data.length, targetAddress, PORT);
                        
                        socket.send(packet);
                        System.out.println("[发送] 消息已发出");
                    }
                    scanner.close();
                } catch (Exception e) {
                    System.err.println("发送线程异常: " + e.getMessage());
                } finally {
                    // 退出时关闭 Socket
                    socket.close();
                    System.out.println("=== 连接已关闭 ===");
                }
            });
            sendThread.start();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}