杰瑞科技汇

Java UDP Socket编程如何实现可靠通信?

UDP 核心概念

在开始编码前,先理解几个关键点:

Java UDP Socket编程如何实现可靠通信?-图1
(图片来源网络,侵删)
  • 无连接:UDP 不像 TCP 那样需要先建立连接(三次握手),发送方可以直接将数据包发送到接收方的 IP 地址和端口号,而不需要接收方的确认。
  • 数据报:数据传输的基本单位是“数据报”(Datagram),每个数据报都包含完整的源地址和目标地址,像一个独立的包裹。
  • 不可靠:UDP 不保证数据包的顺序,也不保证数据包一定能到达目的地,包可能会丢失、重复或乱序。
  • 高效:因为没有连接建立、确认、重传等开销,UDP 的传输效率非常高。

核心类

Java 使用 java.net 包中的两个主要类来处理 UDP:

  • DatagramSocket:代表一个 socket,用于发送和接收 DatagramPacket,它就像是发送和接收邮件的邮局。
  • DatagramPacket:代表一个数据报,包含了要发送的数据、目标地址和端口,或者接收到的数据和源地址、端口,它就像一个装好地址的信封和信件。

编程模型

UDP 编程的流程非常简单:

服务器端:

  1. 创建一个 DatagramSocket 并绑定到一个特定的端口,等待接收数据。
  2. 创建一个空的 DatagramPacket 用于接收数据。
  3. 调用 receive() 方法,阻塞式地等待接收数据,当有数据到达时,数据会被填充到 DatagramPacket 中。
  4. DatagramPacket 中提取数据、客户端地址和端口。
  5. (可选)处理数据。
  6. 创建一个新的 DatagramPacket,将处理后的数据(或原数据)和客户端的地址、端口打包。
  7. 调用 send() 方法将数据包发回给客户端。
  8. 关闭 DatagramSocket

客户端:

Java UDP Socket编程如何实现可靠通信?-图2
(图片来源网络,侵删)
  1. 创建一个 DatagramSocket(通常不需要绑定端口,系统会自动分配一个临时端口)。
  2. 创建一个 DatagramPacket,包含要发送的数据、服务器的 IP 地址和端口。
  3. 调用 send() 方法将数据包发送出去。
  4. 创建一个新的空的 DatagramPacket 用于接收服务器的响应。
  5. 调用 receive() 方法,阻塞式地等待服务器的响应。
  6. DatagramPacket 中提取服务器返回的数据。
  7. 关闭 DatagramSocket

完整代码示例

下面是一个经典的“回显”应用,客户端发送任何消息给服务器,服务器都将原样返回。

服务器端代码 (UDPEchoServer.java)

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPEchoServer {
    // 服务器监听的端口号
    private static final int PORT = 9876;
    public static void main(String[] args) {
        // try-with-resources 语句,确保 DatagramSocket 在使用后被自动关闭
        try (DatagramSocket serverSocket = new DatagramSocket(PORT)) {
            System.out.println("服务器已启动,正在监听端口 " + PORT + "...");
            // 创建一个缓冲区,用于接收数据
            byte[] receiveBuffer = new byte[1024];
            while (true) {
                // 1. 创建一个空的 DatagramPacket 用于接收数据
                DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
                // 2. 阻塞式等待接收数据,当有数据到达时,数据被填充到 receivePacket 中
                serverSocket.receive(receivePacket);
                // 3. 从接收到的数据包中提取数据
                String receivedMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
                System.out.println("收到来自 " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort() + " 的消息: " + receivedMessage);
                // 4. 创建要发送回去的数据包
                // 使用客户端的地址和端口作为目标
                byte[] sendBuffer = receivedMessage.getBytes();
                DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, receivePacket.getAddress(), receivePacket.getPort());
                // 5. 发送数据包
                serverSocket.send(sendPacket);
                System.out.println("已将消息回显给客户端。");
            }
        } catch (SocketException e) {
            System.err.println("无法创建或绑定 DatagramSocket: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("发生 I/O 错误: " + e.getMessage());
        }
    }
}

客户端代码 (UDPEchoClient.java)

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UDPEchoClient {
    // 服务器的 IP 地址(本地回环地址)
    private static final String SERVER_HOST = "127.0.0.1";
    // 服务器的端口号,必须与服务器的端口号一致
    private static final int SERVER_PORT = 9876;
    public static void main(String[] args) {
        // try-with-resources 语句
        try (DatagramSocket clientSocket = new DatagramSocket();
             Scanner scanner = new Scanner(System.in)) {
            // 获取服务器的 IP 地址
            InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
            System.out.println("客户端已启动,连接到服务器 " + SERVER_HOST + ":" + SERVER_PORT);
            System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
            while (true) {
                System.out.print("> ");
                String message = scanner.nextLine();
                if ("exit".equalsIgnoreCase(message)) {
                    break;
                }
                // 1. 创建要发送的数据包
                byte[] sendBuffer = message.getBytes();
                DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, serverAddress, SERVER_PORT);
                // 2. 发送数据包
                clientSocket.send(sendPacket);
                System.out.println("消息已发送: " + message);
                // 3. 创建一个空的 DatagramPacket 用于接收服务器的响应
                byte[] receiveBuffer = new byte[1024];
                DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
                // 4. 阻塞式等待接收服务器的响应
                clientSocket.receive(receivePacket);
                // 5. 从接收到的数据包中提取数据
                String echoMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
                System.out.println("服务器回显: " + echoMessage);
            }
        } catch (SocketException e) {
            System.err.println("无法创建 DatagramSocket: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("发生 I/O 错误: " + e.getMessage());
        }
        System.out.println("客户端已关闭。");
    }
}

如何运行

  1. 编译代码:打开终端或命令提示符,进入两个 .java 文件所在的目录,然后运行:

    javac UDPEchoServer.java UDPEchoClient.java
  2. 启动服务器:在一个终端窗口中运行服务器,服务器会启动并等待连接。

    java UDPEchoServer

    你会看到输出:服务器已启动,正在监听端口 9876...

    Java UDP Socket编程如何实现可靠通信?-图3
    (图片来源网络,侵删)
  3. 启动客户端:在另一个终端窗口中运行客户端。

    java UDPEchoClient

    你会看到输出:客户端已启动,连接到服务器 127.0.0.1:9876,并提示你输入消息。

  4. 交互

    • 在客户端输入 Hello, Server! 然后按回车。
    • 客户端会发送消息,并显示 服务器回显: Hello, Server!
    • 服务器端终端会显示收到的消息和回显操作。
    • 可以继续输入消息进行交互,直到在客户端输入 exit 退出。

关键点解析与最佳实践

  • 缓冲区大小DatagramPacket 的缓冲区大小(byte[])需要根据你的应用场景来设定,如果发送的数据超过了缓冲区大小,数据会被截断。1024 字节是一个常见的起始大小。
  • 阻塞式操作receive() 方法是阻塞的,如果没有数据到达,程序会一直等待,如果你希望实现非阻塞,可以使用 setSoTimeout(int timeout) 方法来设置一个超时时间,超时后,receive() 会抛出 SocketTimeoutException
  • DatagramPacket 的重用:在服务器端,我们循环使用同一个 receivePacket 对象来接收数据,这是高效的,因为每次 receive() 调用都会用新数据覆盖这个对象,发送时则需要为每个客户端创建新的 DatagramPacket
  • 异常处理:网络编程充满了不确定性,IOException 及其子类(如 SocketException)必须被妥善处理。
  • try-with-resources:强烈建议使用 try-with-resources 语句来自动管理 DatagramSocket 的生命周期,确保它在程序结束时或发生异常时被正确关闭,避免资源泄漏。
  • 多线程:上面的例子是单线程的,如果服务器需要同时为多个客户端服务,你需要将 receive()send() 的逻辑(或至少是 receive() 之后的部分)放到一个循环中,并使用多线程来处理每个客户端的请求,否则,一个客户端的交互会阻塞其他客户端。

UDP vs. TCP (快速对比)

特性 UDP (User Datagram Protocol) TCP (Transmission Control Protocol)
连接 无连接 面向连接(需要三次握手)
可靠性 不可靠(不保证顺序、不保证不丢失) 可靠(通过确认、重传、校验和保证数据完整)
速度 快(开销小) 慢(开销大)
数据单位 数据报 字节流
头部大小 8 字节 20 字节(不含选项)
应用场景 实时视频/音频、DNS、在线游戏、广播 网页浏览、文件传输、电子邮件

选择使用 UDP 还是 TCP,完全取决于你的应用需求,如果你需要快速传输,并能容忍少量数据丢失,UDP 是绝佳选择,如果你需要确保数据准确无误地、按顺序到达,TCP 是不二之选。

分享:
扫描分享到社交APP
上一篇
下一篇