Socket(套接字)是网络编程的核心工具,也是操作系统提供给应用程序的网络通信接口。简单来说,Socket 文件是操作系统对网卡的抽象封装,读写 Socket 文件,本质上就是借助网卡完成数据的收发操作。
一、UDP 用户数据报协议
UDP 是无连接、不可靠的传输协议,Java 中通过 DatagramSocket(收发)和 DatagramPacket(封装)实现 UDP 编程。
DatagramSocket
DatagramSocket 是 UDP 对应的 Socket 实现,专门用于发送和接收 UDP 数据报,它就像一个「数据收发中转站」,负责建立应用程序与网络层的连接。
(1)构造方法
| 方法签名 | 方法说明 |
|---|---|
DatagramSocket() | 创建一个 UDP 数据报套接字,绑定到本机任意随机端口(适用于客户端,无需固定端口) |
DatagramSocket(int port) | 创建一个 UDP 数据报套接字,绑定到本机指定端口(适用于服务端,需要固定端口供客户端访问) |
(2)常用方法
| 方法签名 | 方法说明 |
|---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(无数据时会阻塞等待,直到收到数据) |
void send(DatagramPacket p) | 从此套接字发送数据报(直接发送,不阻塞等待对方确认) |
void close() | 关闭此数据报套接字,释放相关资源 |
DatagramPacket
DatagramPacket 是 UDP 数据报的「载体」,负责封装 UDP 通信中的数据内容、目标地址、端口号等信息。所有通过 DatagramSocket 收发的数据,都必须封装在这个对象中。
(1)构造方法
UDP 的收发数据报构造方法有所区分,需注意区分使用场景:
| 方法签名 | 方法说明 |
|---|---|
DatagramPacket(byte[] buf, int length) | 用于接收数据报:接收到的数据会存入字节数组 buf,仅保留前 length 长度的内容 |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 用于发送数据报:从字节数组 buf 的 offset 下标开始,取 length 长度的数据作为发送内容;address 指定接收端的 IP 和端口 |
(2)常用方法
| 方法签名 | 方法说明 |
|---|---|
InetAddress getAddress() | 接收场景:获取发送端的 IP 地址;发送场景:获取接收端的 IP 地址 |
int getPort() | 接收场景:获取发送端的端口号;发送场景:获取接收端的端口号 |
byte[] getData() | 获取数据报中封装的原始字节数据 |
创建 SocketAddress
构造发送用的 DatagramPacket 时,需要传入 SocketAddress 对象,通常我们使用 InetSocketAddress 来快速创建(指定 IP 和端口):
// 示例:创建指向 本地回环地址 127.0.0.1:8888 的 Socket 地址
SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
实现回声服务器
回声服务器的核心功能是:接收客户端发送的消息,不做任何处理,直接原封不动地返回给客户端。下面我们分别实现服务端和客户端代码,一步一步完成 UDP 通信。
UDP 服务端代码
服务端需要固定端口,循环接收客户端请求,并返回响应结果,核心步骤是「接收请求 → 构造响应 → 发送响应」。
import java.io.*;
import java.net.*;
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 requestPacket = new DatagramPacket(new byte[1024],
1024);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(), 0,
requestPacket.getLength());
// 2. 根据请求构造响应
String response = process(request);
// 3. 封装响应返回客户端
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(),
response.getBytes().length,
requestPacket.getSocketAddress()
);
socket.send(responsePacket);
// 4. 打印日志
System.out.printf("[客户端 %s:%d] 请求:%s | 响应:%s\n",
requestPacket.getAddress().getHostAddress(),
requestPacket.getPort(),
request,
response);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
UDP 客户端代码
客户端需要指定服务端的 IP 和端口,读取用户输入并发送给服务端,然后接收服务端的响应并打印。
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
// 客户端无需绑定固定端口,由操作系统自动分配随机端口(避免端口冲突)
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动...");
Scanner scanner = new Scanner(System.in);
// 持续发送用户输入的消息
while (true) {
// 1. 从控制台读取用户输入
System.out.println("-> ");
String request = scanner.next();
// 2. 构造请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(
request.getBytes(),
request.getBytes().length,
InetAddress.getByName(serverIP),
serverPort
);
socket.send(requestPacket);
// 3. 接受服务器响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],
1024);
socket.receive(responsePacket);
// 4. 打印结果
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();
}
}
二、TCP 传输控制协议
TCP 是面向连接、可靠的传输协议,Java 中通过ServerSocket(服务端)和Socket(客户端)建立连接,基于 InputStream/OutputStream(数据读写)实现 TCP 编程。
ServerSocket
ServerSocket 是创建TCP服务端Socket的API。
(1)构造方法
| 方法签名 | 方法说明 |
|---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
(2)核心方法
| 方法签名 | 方法说明 |
|---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
(1)构造方法
| 方法签名 | 方法说明 |
|---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
(2)核心方法
| 方法签名 | 方法说明 |
|---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
实现回声服务器
TCP 服务端代码
import java.io.*;
import java.net.*;
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("服务器启动。。。");
// 持续监听并接收新的客户端连接
while (true) {
Socket socket = serverSocket.accept();
// 应对多个客户端发送请求
// 使用线程池避免线程频繁创建销毁
ExecutorService service = Executors.newCachedThreadPool();
service.submit(() -> {
processConnection(socket);
});
}
}
private void processConnection(Socket socket){
System.out.printf("客户端上线! [%s:%d]\n",
socket.getInetAddress().toString(), socket.getPort());
try (OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
// 持续和同一个客户端进行多次数据收发
while (true) {
if (!scanner.hasNext()) {
break;
}
// 1. 接受请求并解析
String request = scanner.next();
// 2. 根据请求构造响应
String response = process(request);
// 3. 封装响应返回客户端
printWriter.println(response);
printWriter.flush(); // 将缓冲区数据写入 IO 设备
// 4. 打印日志
System.out.printf("[客户端 %s:%d] 请求: %s | 响应: %s\n",
socket.getInetAddress().toString(), socket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.printf("客户端下线! [%s:%d]\n",
socket.getInetAddress().toString(), socket.getPort());
try {
socket.close(); // 注意关闭资源!
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
TCP 客户端代码
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int port) throws IOException {
socket = new Socket(serverIp, port);
}
public void start() {
System.out.println("客户端启动。。。");
try (OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream()) {
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNetWork = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取用户输入
System.out.println("-> ");
String request = scannerConsole.next();
// 2. 构造请求,发送给服务器
printWriter.println(request);
printWriter.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();
}
}