UDP数据报套接字编程(User Datagram Protocol)
| 核心特性 | 说明(结合之前的 UDP 字典服务器场景) |
|---|---|
| 无连接 | 客户端与服务器无需提前建立连接,客户端直接发送数据报(DatagramPacket),服务器通过端口监听接收,无需 "三次握手" |
| 面向数据报 | 数据以独立的 "数据包" 为单位传输,每个数据包包含完整的目标地址和端口,独立处理,不合并、不拆分 |
| 不可靠传输 | 不保证数据到达顺序、完整性,丢失不重传、重复不丢弃、乱序不排序,依赖上层业务处理可靠性(如字典查询可容忍偶尔丢失) |
| 低延迟、高效率 | 无连接建立 / 关闭、无确认重传等开销,传输速度快,适合对延迟敏感、对可靠性要求不高的场景 |
| 无流量 / 拥塞控制 | 发送方按自身速率发送数据,不考虑接收方处理能力和网络状态 |
API 介绍
DatagramSocket:
DatagramSocket 是UDP 的Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法: 就是 打开 '文件'

DatagramSocket 方法:receive 就是读取 ,send 就是写


DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket表示UDP完整的数据报
DatagramPacket 构造方法:

UDP数据包的载荷数据,就饿可以通过构造方法来指定
DatagramPacket 方法:

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress
InetSocketAddress ( SocketAddress 的子类)构造方法:


注意:真实的服务器响应与请求是不一样的,这个是回显服务器
代码演示:
UDPSevrve实现:
核心思路:

具体实现:

DatagramPacket是 Java 中用于表示 UDP 数据报的类。new byte[4096]:创建一个长度为 4096 字节的字节数组,用来存储接收到的 UDP 数据报的载荷部分(即实际传输的有效数据)。length: 4096:指定接收数据的最大长度为 4096 字节,确保接收到的数据不会超出字节数组的容量。

将 UDP 数据报中接收到的二进制数据转换为字符串:
requestPacket.getData():获取存储 UDP 数据报载荷的字节数组(即接收到的二进制数据)。offset: 0:指定从字节数组的起始位置(第 0 个字节)开始转换。requestPacket.getLength():获取实际接收到的数据长度,确保只转换有效数据部分。


打印 UDP 通信请求与响应日志


receive() 方法的 DatagramPacket 参数是一个典型的"输出型参数":
输出型参数:调用方提供一个"空容器",方法执行后这个容器被填充了数据。
为什么不用手动close 关闭socket?
UDP 服务器中Socket对象的生命周期和资源释放:
- Socket 对象的生命周期 :在 UDP 服务器中,
Socket对象会伴随服务器的整个运行过程,从服务器启动到停止,始终用于处理客户端的 UDP 数据报通信。 - 资源释放机制 :当服务器进程结束(如关闭服务器程序)时,操作系统会自动释放该进程在 PCB(进程控制块)文件描述符表中占用的所有资源,因此不需要手动调用
close方法来释放Socket资源。
PCB 是 进程控制块(Process Control Block) 的缩写,是操作系统用于管理进程的核心数据结构
服务器启动后,客户端还没请求时,服务器逻辑在做什么?
服务器会在 socket.receive(requestPacket) 处阻塞,直到客户端发送 UDP 请求才会继续执行。
代码演示:
java
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
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) {
// 循环一次, 就相当于处理一次请求.
// 处理请求的过程, 典型的服务器都是分成三个步骤的.
// 1. 读取请求并解析.
// DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求, 计算响应. (服务器最关键的逻辑)
// 但是此处写的是回显服务器. 这个环节相当于省略了.
String response = process(request);
// 3. 把响应返回给客户端
// 根据 response 构造 DatagramPacket, 发送给客户端.
// 此处不能使用 response.length()
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
// 此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
// 需要指定 目的 ip 和 目的端口.
socket.send(responsePacket);
// 4. 打印一个日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
request, response);
}
}
// 后续如果要写别的服务器, 只修改这个地方就好了.
// 不要忘记, private 方法不能被重写. 需要改成 public
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
UDPClient实现:
我们的数据包中,只存储了他自己的IP和端口号,所以客户端这里要记一下服务器的IP和端口号
客户端访问服务器时,IP 和端口是如何分配的?
-目的 IP:服务器的 IP(serverIP)。
-目的端口:服务器的端口(serverport)。
-源 IP:客户端所在主机的 IP。
-源端口:操作系统随机分配的空闲端口。
getByName(): 解析服务器 IP 地址

127.0.0.1(环回 IP)

服务器与客户端之间要联网么?

代码演示:
java
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
// UDP 本身不保存对端的信息, 就自己的代码中保存一下
private String serverIp;
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) {
// 1. 从控制台读取用户输入的内容.
System.out.println("请输入要发送的内容:");
if (!scanner.hasNext()) {
break;
}
String request = scanner.next();
// 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.
// 构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
// 3. 发送数据报
socket.send(requestPacket);
// 4. 接收服务器的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 5. 从服务器读取的数据进行解析, 打印出来.
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();
}
}
UDP Dict Server 实现:
字典服务器的实现是基于UDPServer实现:
其他的都一样,接收的数据需要处理一下,需要在我们构造方法设置的字典里,寻找有没有英文对应的
只需要重写一下处理请求返回相应的数据
java
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<String, String>();
public UdpDictServer(int port) throws SocketException {
// 调用父类构造方法这句代码, 必须放到子类构造方法的第一行
super(port);
dict.put("hello", "你好");
dict.put("world", "世界");
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
// 可以添加更多更多的数据.
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "没有找到该单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9091);
server.start();
}
}
TCP数据报套接字编程(Transmission Control Protocol)
概念
1. ServerSocket:TCP 服务端的 "大门"
- 它是服务端的 "监听接口",负责绑定端口、等待客户端连接 。
- 用
ServerSocket(int port)绑定端口后,就像在这个端口上 "开了一扇门",持续监听是否有客户端来敲门。 - 调用
accept()方法时,服务端会 "阻塞等待"------ 直到有客户端发起连接请求,才会返回一个Socket对象(相当于给这个客户端 "开了一扇专属的小门")。
- 用
2. Socket:客户端与服务端的 "专属通道"
- 客户端创建
Socket时,需指定服务端的 IP 和端口(相当于 "主动敲门"); - 服务端通过
accept()得到的Socket,是与该客户端的双向通信通道 :- 服务端和客户端都可以通过这个
Socket的输入流(getInputStream())读数据,输出流(getOutputStream())写数据。
- 服务端和客户端都可以通过这个
核心特性(与 UDP 对比,呼应之前的网络通信场景):
- 面向连接:通信前需通过 "三次握手" 建立连接,通信结束后通过 "四次挥手" 关闭连接;
- 可靠传输:保证数据的完整性、有序性,丢失会重传、重复会丢弃、乱序会排序;
- 面向字节流:数据以字节流形式传输,而非独立数据包;
- 有流量控制和拥塞控制:避免因发送方速率过快导致接收方处理不及,或网络拥堵;
- 延迟略高:因连接建立、确认等机制,传输效率低于 UDP,但可靠性更强。
API 介绍
ServerSocket
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
ServerSocket 方法:

Socket
Socket 是客户端Socket,或服务端中接收到客⼾端建立连接(accept方法)的请求后,返回的服 务端Socket。 不管是客⼾端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及⽤来与对方收发数据的。
Socket 构造方法:

代码演示
TCPEchoClient:
java
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 客户端在 new Socket 的时候, 就会和服务器建立 TCP 连接.
// 此时少了服务器 IP 和 端口.
socket = new Socket(serverIp, serverPort);
// socket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort));
}
public void start() {
System.out.println("client start!");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取用户输入.
System.out.print("-> ");
String request = scanner.next();
// 2. 把请求发送给服务器.
writer.println(request);
writer.flush();
// 3. 从服务器读取响应
if (!scannerNetwork.hasNext()) {
break;
}
String response = scannerNetwork.next();
// 4. 把响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
TCPEchoServer:
java
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("server start!");
// 不能使用 FixedThreadPool, 线程数目固定.
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// 首先要先接受客户端的连接, 然后才能进行通信.
// 如果有客户端和服务器建立好了连接, accept 能够返回.
// 否咋 accept 会阻塞.
// [有连接]
Socket socket = serverSocket.accept();
// 通过这个方法处理这个客户端整个的连接过程.
// 直接调用 processConnection, 此时就会 "顾此失彼" 一旦进入到 processConnection 方法
// 就不能再次调用 accept
// processConnection(socket);
// 此处创建新线程. 在新线程里, 调用 processConnection
// Thread t = new Thread(() -> {
// processConnection(socket);
// });
// t.start();
// 如果当前线程数目进一步增多, 创建销毁进一步频繁, 此时线程创建销毁开销不可忽视了.
// 使用线程池, 是进一步的改进手段.
executorService.submit(() -> {
processConnection(socket);
});
}
}
private void processConnection(Socket socket) {
// 在一次连接中, 客户端和服务器之间可能会进行多组数据传输.
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
// [面向字节流] & [全双工]
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 处理多次请求/响应的读写操作.
// 一次循环就是读写一个请求/响应
// 1. 读取请求并解析 (可以直接使用 Scanner 完成)
if (!scanner.hasNext()) {
// 客户端关闭了连接
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
// 假设这里有一系列的逻辑呢??
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回客户端
writer.println(response);
writer.flush();
// 4. 打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 假如在这之前也有一系列逻辑呢?
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
TCPEchoServer:
java
package network;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer {
private Map<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("hello", "你好");
dict.put("world", "世界");
dict.put("cat", "小猫");
dict.put("dog", "小狗");
}
public String process(String request) {
return dict.getOrDefault(request, "没有找到该单词");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}