【Java EE】UDP 编程核心类与方法

UDP 编程核心类与方法

Java 中编写 UDP 程序需要两个类:DatagramSocket(通信插座)和 DatagramPacket(数据包)。

DatagramSocket(通信插座)

方法签名 说明
DatagramSocket() 创建 UDP 套接字,绑定到本机随机端口(一般用于客户端)
DatagramSocket(int port) 创建 UDP 套接字,绑定到本机指定端口(一般用于服务器)
void receive(DatagramPacket p) 从此套接字接收数据报,若无数据则阻塞等待
void send(DatagramPacket p) 从此套接字发送数据报,不会阻塞,直接发送
void close() 关闭此数据报套接字

DatagramPacket(数据报包)

构造方法

方法签名 说明
DatagramPacket(byte[] buf, int length) 构造一个用于接收 数据报的包,数据存入 buf,接收指定长度
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个用于发送 数据报的包,address 指定目的 IP 和端口

获取对方信息的方法

方法 返回值 说明
getAddress() InetAddress 只拿到 IP 地址
getPort() int 只拿到 端口号
getSocketAddress() SocketAddress 同时拿到 IP 和端口 (封装在 InetSocketAddress 对象中)

继承
<<abstract>>
SocketAddress
+Serializable
InetSocketAddress
-String hostname
-InetAddress addr
-int port
+getAddress() : InetAddress
+getPort() : int
+getHostString() : String
+toString() : String
实际返回的对象类型

封装了 IP + 端口

获取数据的方法

方法 返回值 说明
getData() byte[] 拿到数据报中的完整字节数组(包括空余部分)
getLength() int 拿到有效数据的长度

重要细节

  • "hello".length() 返回的是 字符个数(5)。
  • "hello".getBytes().length 返回的是 字节个数(英文环境下也是 5,但中文环境下一个字符可能占 3 字节)。
  • 构造数据报时,必须用字节个数,因为网络传输的最小单位是字节。

实践:UDP 回显(Echo)服务器⭐

需求:客户端发什么,服务器就原样返回什么。流程如下:
服务器 客户端 服务器 客户端 3. receive() 阻塞等待 7. receive() 阻塞等待响应 loop [循环处理] 1. 从控制台读取用户输入 2. 发送请求数据报到服务器 4. 解析请求字符串 5. process() 计算响应(这里是直接原样返回) 6. 发送响应数据报给客户端 8. 打印响应到控制台

服务器端代码

操作系统 UdpEchoServer main() 操作系统 UdpEchoServer main() 构造方法中创建 socket 绑定 9090 端口 进入主循环 while(true) receive() 阻塞等待 loop [服务器主循环] new UdpEchoServer(9090) socket = new DatagramSocket(9090) 端口绑定成功,文件描述符表新增一项 server.start() 准备接收请求

java 复制代码
public class UdpEchoServer {
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        // ① 服务器必须绑定一个固定端口,让客户端能找到
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            // ② 准备好一个空的数据报,用于承载客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            // ③ receive 会阻塞,直到收到客户端发来的数据报
            socket.receive(requestPacket);
            // ④ 解析有效部分并转成字符串(注意用 getLength() 取有效长度)
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // ⑤ 根据请求计算响应(这里是 Echo 的核心逻辑)
            String response = process(request);

            // ⑥ 构造响应数据报:response.getBytes().length 是字节数,不是字符数
            DatagramPacket responsePacket = new DatagramPacket(
                    response.getBytes(),           // 响应内容的字节数组
                    response.getBytes().length,    // 字节个数
                    requestPacket.getSocketAddress() // 从请求中获取客户端的 IP 和端口
            );
            // ⑦ 发送响应
            socket.send(responsePacket);

            // ⑧ 打印日志,方便观察
            System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    requestPacket.getAddress().toString(),
                    requestPacket.getPort(),
                    request, response);
        }
    }

    public String process(String request) {
        return request; // 回显服务器,原样返回
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

构造接收用的数据报

java 复制代码
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);

这里调用的是两参数构造方法

参数 传入的值 含义
byte[] buf new byte[4096] 一个大小为 4096 字节的空数组,用于存放接收到的数据
int length 4096 最大能接收 4096 字节的数据

为什么是 4096?

  • UDP 协议单个数据报理论上最大约 64KB,实际常用 1500 字节以内(以太网 MTU 限制)。
  • 4096 是常见的选择,足以容纳大多数 UDP 应用层数据,是经验值。
  • 如果对方发的数据超过 4096 字节,多出来的部分会被截断丢弃
    requestPacket 内部结构
    byte 数组

new byte[4096]
length = 4096

最大接收长度
实际数据长度

初始为 0

接收数据(阻塞等待)

java 复制代码
socket.receive(requestPacket);

这是一个阻塞方法,执行流程如下:
调用 receive()
没有数据到达

线程挂起,不消耗 CPU
数据报到达
返回,数据写入 requestPacket
等待中
接收完成
阻塞期间,程序停在这里

直到有数据到来

接收完成后,requestPacket 内部发生了两件事:

  1. getData() 返回的字节数组里,前 N 个字节被填入了收到的数据。
  2. getLength() 的返回值变为 N(实际收到的数据长度),而不再是 0。

解析数据为字符串

java 复制代码
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

这里构造字符串的方式很关键

java 复制代码
new String(byte[] bytes, int offset, int length)
参数 传入的值 含义
bytes requestPacket.getData() 整个 4096 字节的数组
offset 0 从数组的第 0 个位置开始取
length requestPacket.getLength() 只取实际收到的有效字节数

为什么不用 new String(requestPacket.getData())

如果直接用这个单参数构造方法,会把整个 4096 字节都转成字符串。但实际有效数据可能只有 10 字节,剩下 4086 字节都是无效的 0 值,会构造出一堆乱码或空白字符。必须用 getLength() 精确限定有效范围。
📦 完整的 4096 字节数组
❌ 无效数据 剩余4091字节全为0
✅ 有效数据 getLength()=5
0
H
e
l
l
o
0
...
0

根据请求计算响应

java 复制代码
String response = process(request);
java 复制代码
public String process(String request) {
    return request; // 回显服务器,原样返回
}

这一行是整个服务器最核心的业务逻辑所在。

  • 对于 Echo 服务器,process() 就是原样返回,请求是什么,响应就是什么。
  • 对于真实的服务器,这里会被替换成复杂的业务逻辑,比如数据库查询、计算处理等。
  • 把业务逻辑抽成单独的 process() 方法,体现关注点分离的原则。未来要写别的服务器,只改这一个方法即可。

构造响应

java 复制代码
DatagramPacket responsePacket = new DatagramPacket(
        response.getBytes(),              // 参数1: 响应内容的字节数组
        response.getBytes().length,       // 参数2: 要发送的字节数
        requestPacket.getSocketAddress()  // 参数3: 目的地址 = 客户端的 IP+端口
);

这里调用的是 DatagramPacket四参数构造方法(发送专用):

参数 传入的值 含义
byte[] buf response.getBytes() 响应字符串转成的字节数组
int offset 省略(默认 0) 从数组的哪个位置开始发送
int length response.getBytes().length 发送的字节数
SocketAddress address requestPacket.getSocketAddress() 数据要发给谁

为什么用 getBytes().length 而不用 length()

方法 返回值 示例 "你好" 示例 "hello"
length() 字符个数 2 5
getBytes().length 字节个数 6(UTF-8下中文每字3字节) 5

结论 :网络传输的是字节,必须用字节长度。如果用了 length(),遇到中文字符就会出错。

requestPacket.getSocketAddress() 拿到的什么?

返回的是 InetSocketAddress 对象,封装了客户端的 IP 和端口。
携带了源 IP 和源端口
requestPacket.getSocketAddress
InetSocketAddress 对象
IP: 127.0.0.1
端口: 54321
当初客户端发来请求时
请求报文
服务器通过 getSocketAddress

提取出这个地址
构造响应时原样填回去

响应就能准确回到客户端

这就是 UDP "无连接" 的体现

  • TCP 建立连接后,双方互相保存对端信息,发送时不用每次都指定目的地址。
  • UDP 每次发送都必须显式指定目的地址。这个地址信息需要自己从请求中提取并保存(就像这里做的一样)。

发送响应

java 复制代码
socket.send(responsePacket);

send()非阻塞方法,它把数据交给操作系统后立即返回,不会等待对端确认(UDP 本身就是不可靠的,发完就不管了)。
立即返回

不等待结果
socket.send()
操作系统内核
网卡驱动
物理网络
继续执行下一条语句

打印日志

java 复制代码
System.out.printf("[%s:%d] req: %s, resp: %s\n",
        requestPacket.getAddress().toString(),
        requestPacket.getPort(),
        request, response);

这行日志会打印如下格式:

复制代码
[/127.0.0.1:54321] req: hello, resp: hello
内容 对应方法 示例
客户端 IP requestPacket.getAddress().toString() /127.0.0.1
客户端端口 requestPacket.getPort() 54321
请求内容 request hello
响应内容 response hello

客户端代码

UdpEchoServer UdpEchoClient 用户键盘输入 UdpEchoServer UdpEchoClient 用户键盘输入 receive() 解除阻塞 receive() 解除阻塞 loop [每次通信] 输入字符串 "hello" 构造 requestPacket (载荷 + 服务器IP+端口) socket.send(requestPacket) 解析 + process() socket.send(responsePacket) 打印 "hello"

java 复制代码
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;   // 在代码里自己保存服务器 IP(UDP 不会帮你存)
    private int serverPort;    // 在代码里自己保存服务器端口

    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        // ① 客户端用随机端口即可
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // ② 从控制台读取用户输入
            System.out.println("请输入要发送的内容:");
            if (!scanner.hasNext()) break;
            String request = scanner.next();

            // ③ 构造请求数据报,必须指定服务器的 IP 和端口
            DatagramPacket requestPacket = new DatagramPacket(
                    request.getBytes(),
                    request.getBytes().length,
                    InetAddress.getByName(serverIp), // 把字符串 IP 转了对象
                    serverPort
            );
            // ④ 发送请求(不阻塞)
            socket.send(requestPacket);

            // ⑤ 准备接收响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket); // 阻塞等待服务器返回

            // ⑥ 解析并打印
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

成员变量与构造方法

java 复制代码
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;    // 客户端要通信的服务器 IP
    private int serverPort;     // 客户端要通信的服务器端口

    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();  // 无参构造,随机端口
    }
}
java 复制代码
private String serverIp;    // 客户端要通信的服务器 IP
private int serverPort;     // 客户端要通信的服务器端口
  • UDP 协议本身不保存对端信息,所以需要在自己的代码中用成员变量记录服务器的 IP 和端口。
  • 每次发送数据报时,都要明确告诉 UDP:"请发给这个 IP 的这个端口"。
  • 这和 TCP 完全不用------TCP 建立连接后,直接 write() 就行,协议栈会自动填好目的地址。
java 复制代码
socket = new DatagramSocket();  // 无参构造,随机端口
  • 客户端不需要固定的端口号,让操作系统随机分配一个可用的就行。
  • 只要服务器返回的响应能回到这个随机端口,通信就能正常进行。

构造请求

java 复制代码
DatagramPacket requestPacket = new DatagramPacket(
        request.getBytes(),
        request.getBytes().length,
        InetAddress.getByName(serverIp),  // 把 "127.0.0.1" 转成 InetAddress 对象
        serverPort                        // 服务器的 9090 端口
);
socket.send(requestPacket);

这里使用了 DatagramPacket五参数构造方法

参数 含义
byte[] buf request.getBytes() 要发送的数据
int offset 省略 从数组哪个位置开始
int length request.getBytes().length 发送的字节数
InetAddress address InetAddress.getByName(serverIp) 目的 IP
int port serverPort 目的端口
java 复制代码
InetAddress.getByName(serverIp)
  • 这是一个静态方法 ,把字符串形式的 IP 地址(如 "127.0.0.1")转成 InetAddress 对象。
  • 也可以传域名,比如 "www.baidu.com",它会自动进行 DNS 解析。
  • 如果 IP 格式不对或域名解析失败,会抛出 UnknownHostException

'127.0.0.1'
InetAddress.getByName()
InetAddress 对象

封装了 IP 信息
'www.baidu.com'
先 DNS 查询

得到真实 IP
InetAddress 对象

发送请求

java 复制代码
socket.send(requestPacket)

与服务器端一样,send() 是非阻塞的,数据交给操作系统就返回了。

接收响应

java 复制代码
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);  // 阻塞等待服务器返回
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);

与服务器端的逻辑完全对称

  1. 先准备一个空的 DatagramPacket,大小为 4096 字节。
  2. 调用 receive() 阻塞等待,直到服务器返回数据。
  3. getData() + getLength() 解析出有效数据。

为什么客户端也需要 receive() 阻塞?
Server Client Server Client 调用 receive() 进入阻塞状态 处理中... receive() 解除阻塞 拿到响应数据 send(requestPacket) send(responsePacket) 打印响应

如果客户端不发请求就调用 receive(),或者服务器没返回响应,客户端会一直阻塞 在那里。这其实是一种天然的同步机制,保证了请求-响应的对应关系。

服务器与客户端配合运行全景图

服务器 0.0.0.0:9090
客户端 127.0.0.1:随机端口
网络
网络
new DatagramSocket
从控制台读输入
构造 requestPacket

目的: 127.0.0.1:9090
socket.send 发送请求
socket.receive 阻塞等待响应
解析并打印响应
new DatagramSocket 9090
socket.receive 阻塞等待请求
解析请求
process 计算响应
构造 responsePacket

目的: 客户端IP+端口
socket.send 发送响应

补充:Socket 需要显式关闭吗?

文件需要关闭,但这个问题要看 对象的生命周期。对于上面这个服务器

  • 服务器的 socket 是成员变量,伴随整个进程存在。
  • 进程退出时,操作系统会自动回收 文件描述符表中所有资源,不需要手动 close()
  • 但如果是临时创建的 socket,用完就应该调用 close() 释放。

UDP 词典服务器

UdpDictServer(词典服务器)
UdpEchoServer(回显服务器)
继承
super调用
完全复用
被覆盖
socket 成员变量
构造方法:绑定端口
start():主循环
process():原样返回
dict 成员变量(新增)
构造方法:先调 super(port),再初始化词典
start():完全继承
process():重写!查词典翻译

核心思想

  • UdpEchoServer 中已经把网络通信的框架写好了:收请求 → 调 process() → 发响应。
  • 子类只需要重写 process() 方法,就能改变服务器的业务逻辑。
java 复制代码
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

/**
 * UDP 词典服务器
 * 继承自 UdpEchoServer,复用了网络通信框架
 * 只需要重写 process() 方法,把"原样返回"改成"查词典翻译"
 */
public class UdpDictServer extends UdpEchoServer {
    
    // 词典:用 HashMap 存储,键是中文词条,值是英文翻译
    // HashMap 查找速度快(O(1)),适合做词典
    private HashMap<String, String> dict = new HashMap<>();

    /**
     * 构造方法
     * @param port 服务器要绑定的端口号
     * @throws SocketException 如果端口被占用或没有权限,抛出此异常
     */
    public UdpDictServer(int port) throws SocketException {
        // 第一步:调用父类构造方法
        // 父类会执行 socket = new DatagramSocket(port),完成端口绑定
        super(port);

        // 第二步:初始化词典,添加示例词条
        dict.put("小猫", "cat");      // 小猫 → cat
        dict.put("小狗", "dog");      // 小狗 → dog
        dict.put("小兔子", "rabbit"); // 小兔子 → rabbit
        dict.put("小鸭子", "duck");   // 小鸭子 → duck
        
        // 实际项目中,词条可以从文件、数据库或远程 API 加载
    }

    /**
     * 重写父类的 process() 方法
     * 父类中是 "原样返回",这里改成 "查词典翻译"
     * 
     * @param request 客户端发来的请求,例如 "小猫"
     * @return 查到的翻译结果,如果没找到则返回 "未找到该词条"
     */
    @Override
    public String process(String request) {
        // getOrDefault(key, defaultValue):
        //   如果 key 在 HashMap 中存在,返回对应的 value
        //   如果 key 不存在,返回 defaultValue("未找到该词条")
        // 避免了 get() 返回 null 需要额外判断的问题
        return dict.getOrDefault(request, "未找到该词条");
        
        // 等价于下面的代码:
        // String result = dict.get(request);
        // if (result != null) {
        //     return result;       // 找到了,返回翻译
        // } else {
        //     return "未找到该词条"; // 没找到,返回提示信息
        // }
    }

    /**
     * 程序入口
     * 创建词典服务器并启动
     */
    public static void main(String[] args) throws IOException {
        // 创建服务器对象,绑定 9090 端口
        // 构造方法中会自动调用 super(9090) 绑定端口,并初始化词典
        UdpDictServer server = new UdpDictServer(9090);
        
        // 调用父类 UdpEchoServer 的 start() 方法,进入主循环
        // start() 中的逻辑:收请求 → 调用 process() → 发响应
        // 这里的 process() 已经是重写后的"查词典版本"
        server.start();
    }
}

运行效果对比
如果是 UdpEchoServer:9090 UdpDictServer:9090 客户端 如果是 UdpEchoServer:9090 UdpDictServer:9090 客户端 process() → 查词典 → "cat" 如果是回显服务器 process() → 原样返回 词典里没有 → 返回默认值 发送 "小猫" 返回 "cat" 返回 "小猫" 发送 "大象" 返回 "未找到该词条"

相关推荐
iCxhust1 小时前
点亮8086最小系统的LED
stm32·单片机·嵌入式硬件·51单片机·微机原理·8086最小系统·8088单板机
时空自由民.2 小时前
开环无感FOC与SPWM&SVPWM
单片机·嵌入式硬件
集芯微电科技有限公司3 小时前
替代TMUX1380A/TMUX1309A双向8:1单通道 4:1双通道控制多路复用器
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
我要成为嵌入式大佬3 小时前
项目制作日记简介
单片机·嵌入式硬件
FreakStudio3 小时前
工控开发板从开箱到点亮 LED-恩智浦MCXE31B 实测:3 路 CAN + 以太网+自带调试器
python·单片机·嵌入式·大学生·面向对象·技术栈·并行计算·电子diy·电子计算机
猿来&如此3 小时前
【51单片机】开发板介绍
单片机·嵌入式硬件·51单片机
进击的小头4 小时前
第21篇:TI DSP 寄存器级开发与库函数开发对比
驱动开发·单片机·嵌入式硬件
LCG元6 小时前
STM32实战:基于STM32F103的智能手环(计步+心率+OLED)
stm32·单片机·嵌入式硬件
计算机安禾7 小时前
【计算机网络】第18篇:UDP的轻量级设计——无连接传输的本质及QUIC的改造路径
网络协议·计算机网络·udp