一、概述
Java 网络编程是构建分布式应用和网络服务的基础。Java 提供了两套主要的网络编程 API:
- 传统的阻塞式 I/O(BIO - Blocking I/O) :基于
java.net包,主要类包括Socket和ServerSocket - 新 I/O(NIO - New I/O) :基于
java.nio.channels包,主要类包括SocketChannel和ServerSocketChannel
1.1 传输协议:TCP vs UDP
在深入 Socket 编程之前,需要理解两种底层传输协议:
TCP(传输控制协议)
- 面向连接:通信前需要建立连接(三次握手)
- 可靠传输:保证数据顺序和完整性
- 流式传输:数据以字节流形式传输
- 适用场景:需要可靠传输的应用(HTTP、FTP、邮件等)
UDP(用户数据报协议)
- 无连接:直接发送数据,无需建立连接
- 不可靠传输:不保证数据到达和顺序
- 数据报传输:数据以数据报(Datagram)形式传输
- 适用场景:对实时性要求高、可容忍少量丢失的应用(视频直播、在线游戏、DNS查询等)
1.2 Java Socket 编程分类
根据传输协议和 I/O 模式,Java Socket 编程可以分为:
| 协议 | BIO | NIO |
|---|---|---|
| TCP | Socket / ServerSocket |
SocketChannel / ServerSocketChannel |
| UDP | DatagramSocket / DatagramPacket |
DatagramChannel |
二、Socket 编程基础
Socket 编程概念
Socket(套接字) 是网络通信的端点,是应用程序与网络协议栈之间的接口。在 Java 中,Socket 编程可以从两个维度进行分类:
-
按 I/O 模式分类:
- 阻塞式 I/O(BIO):线程在等待 I/O 操作完成时会被阻塞,直到操作完成
- 非阻塞式 I/O(NIO):线程在等待 I/O 操作时不会被阻塞,可以继续处理其他任务
-
按传输协议分类:
- TCP Socket:面向连接的可靠传输
- UDP Socket:无连接的快速传输
三、传统 BIO:TCP Socket 编程
3.1 Socket 类(TCP 客户端)
3.1.1 概念
Socket 类代表客户端与服务器之间的一个 TCP 连接端点。它封装了 TCP/IP 协议的细节,提供了面向流的网络通信接口。注意:Socket 类只支持 TCP 协议,不支持 UDP。
3.1.2 主要方法
Socket(String host, int port):创建并连接到指定主机和端口connect(SocketAddress endpoint):连接到指定的服务器地址getInputStream():获取输入流,用于接收数据getOutputStream():获取输出流,用于发送数据close():关闭套接字连接
3.1.3 客户端示例
java
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* Socket 客户端示例
* 演示如何使用 Socket 连接到服务器并发送/接收数据
*/
public class SocketClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
public static void main(String[] args) {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
// 获取输出流,用于向服务器发送数据
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true);
// 获取输入流,用于接收服务器响应
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
// 从控制台读取用户输入
Scanner scanner = new Scanner(System.in)) {
System.out.println("已连接到服务器:" + socket.getRemoteSocketAddress());
// 启动一个线程用于接收服务器消息
Thread receiveThread = new Thread(() -> {
try {
String response;
while ((response = reader.readLine()) != null) {
System.out.println("服务器响应:" + response);
}
} catch (IOException e) {
System.out.println("连接已断开");
}
});
receiveThread.start();
// 主线程用于发送消息
String userInput;
while (scanner.hasNextLine()) {
userInput = scanner.nextLine();
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
writer.println(userInput);
}
} catch (IOException e) {
System.err.println("客户端连接失败:" + e.getMessage());
e.printStackTrace();
}
}
}
3.1.4 使用场景
- 简单的客户端-服务器通信
- 连接数量较少的应用
- 对实时性要求不高的场景
- 学习和原型开发
3.2 ServerSocket 类(TCP 服务器)
3.2.1 概念
ServerSocket 类用于 TCP 服务器端,监听指定端口的客户端连接请求。当客户端连接时,accept() 方法会返回一个新的 Socket 对象,用于与该客户端进行通信。注意:ServerSocket 类只支持 TCP 协议。
3.2.2 主要方法
ServerSocket(int port):创建并绑定到指定端口accept():监听并接受客户端的连接请求,阻塞直到有连接setSoTimeout(int timeout):设置 accept 超时时间close():关闭服务器套接字
3.2.3 服务器端示例(单线程版本)
java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* ServerSocket 单线程服务器示例
* 注意:此版本只能处理一个客户端连接
*/
public class SimpleSocketServer {
private static final int PORT = 8888;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器已启动,监听端口:" + PORT);
System.out.println("等待客户端连接...");
// 接受客户端连接(阻塞)
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接:" + clientSocket.getRemoteSocketAddress());
// 处理客户端请求
handleClient(clientSocket);
} catch (IOException e) {
System.err.println("服务器启动失败:" + e.getMessage());
e.printStackTrace();
}
}
private static void handleClient(Socket socket) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true)) {
String message;
while ((message = reader.readLine()) != null) {
System.out.println("收到客户端消息:" + message);
// 回显消息
writer.println("服务器已收到:" + message);
}
} catch (IOException e) {
System.err.println("处理客户端请求时出错:" + e.getMessage());
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.2.4 服务器端示例(多线程版本)
- 主线程仅负责监听端口,收到客户端连接请求(accept)后,立即创建一个子线程专门处理该客户端的完整交互(接收数据、业务处理、发送响应);
- 主线程无需等待子线程结束,直接返回继续监听下一个连接。
java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ServerSocket 多线程服务器示例
* 使用线程池处理多个客户端连接
*/
public class MultiThreadSocketServer {
private static final int PORT = 8888;
private static final int THREAD_POOL_SIZE = 10;
private static final ExecutorService executorService =
Executors.newFixedThreadPool(THREAD_POOL_SIZE);
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("多线程服务器已启动,监听端口:" + PORT);
System.out.println("等待客户端连接...");
while (true) {
// 接受客户端连接(阻塞)
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接:" + clientSocket.getRemoteSocketAddress());
// 为每个客户端创建一个任务,提交到线程池
executorService.submit(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器启动失败:" + e.getMessage());
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
/**
* 客户端处理器
*/
static class ClientHandler implements Runnable {
private final Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true)) {
String message;
while ((message = reader.readLine()) != null) {
System.out.println("[" + Thread.currentThread().getName() +
"] 收到消息:" + message);
// 处理业务逻辑
String response = processMessage(message);
writer.println(response);
}
} catch (IOException e) {
System.err.println("处理客户端请求时出错:" + e.getMessage());
} finally {
try {
socket.close();
System.out.println("客户端连接已关闭:" + socket.getRemoteSocketAddress());
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理消息的业务逻辑
*/
private String processMessage(String message) {
// 示例:简单的命令处理
if (message.startsWith("ECHO:")) {
return "回显:" + message.substring(5);
} else if (message.startsWith("TIME")) {
return "当前时间:" + new java.util.Date();
} else {
return "已收到:" + message;
}
}
}
}
3.2.5 使用场景
- 传统的客户端-服务器应用
- 连接数量可控的场景(通常 < 1000)
- 需要简单易用的网络通信
- 企业内部系统、管理后台等
3.2.6 TCP BIO 的优缺点
优点:
- 编程简单,易于理解
- 代码直观,调试方便
- 适合连接数较少的场景
- 数据可靠传输,保证顺序
缺点:
- 每个连接需要一个线程,资源消耗大
- 高并发场景下性能受限
- 线程阻塞导致资源浪费
- 需要建立连接,有额外开销
四、UDP Socket 编程:DatagramSocket 和 DatagramPacket
4.1 UDP 编程概述
UDP(User Datagram Protocol)是一种无连接的传输协议,与 TCP 不同,UDP 不需要建立连接,直接发送数据报。Java 中使用 DatagramSocket 和 DatagramPacket 进行 UDP 编程。
4.2 DatagramSocket 类
4.2.1 概念
DatagramSocket 是 UDP 通信的套接字,用于发送和接收 UDP 数据报。与 TCP 的 Socket 不同,UDP 的客户端和服务器端都使用 DatagramSocket,没有专门的服务器类。
4.2.2 主要方法
DatagramSocket(int port):创建并绑定到指定端口DatagramSocket():创建未绑定的套接字send(DatagramPacket packet):发送数据报receive(DatagramPacket packet):接收数据报(阻塞)setSoTimeout(int timeout):设置接收超时时间close():关闭套接字
4.3 DatagramPacket 类
4.3.1 概念
DatagramPacket 表示 UDP 数据报,包含要发送或接收的数据以及目标地址信息。
4.3.2 主要方法
DatagramPacket(byte[] buf, int length):创建接收数据报DatagramPacket(byte[] buf, int length, InetAddress address, int port):创建发送数据报getData():获取数据getLength():获取数据长度getAddress():获取发送方/接收方地址getPort():获取发送方/接收方端口
4.3.3 UDP 服务器示例
java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
/**
* UDP 服务器示例
* 注意:UDP 服务器和客户端都使用 DatagramSocket
*/
public class UDPServer {
private static final int PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(PORT)) {
System.out.println("UDP 服务器已启动,监听端口:" + PORT);
while (true) {
// 创建接收缓冲区
byte[] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 接收数据报(阻塞)
socket.receive(packet);
// 获取客户端信息
InetAddress clientAddress = packet.getAddress();
int clientPort = packet.getPort();
// 解析接收到的数据
String received = new String(
packet.getData(),
0,
packet.getLength(),
StandardCharsets.UTF_8
);
System.out.println("收到来自 [" + clientAddress + ":" + clientPort + "] 的消息:" + received);
// 准备响应数据
String response = "服务器已收到:" + received;
byte[] responseData = response.getBytes(StandardCharsets.UTF_8);
// 创建响应数据报
DatagramPacket responsePacket = new DatagramPacket(
responseData,
responseData.length,
clientAddress,
clientPort
);
// 发送响应
socket.send(responsePacket);
}
} catch (Exception e) {
System.err.println("UDP 服务器错误:" + e.getMessage());
e.printStackTrace();
}
}
}
4.3.4 UDP 客户端示例
java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* UDP 客户端示例
*/
public class UDPClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket();
Scanner scanner = new Scanner(System.in)) {
InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
System.out.println("UDP 客户端已启动,连接到服务器:" + SERVER_HOST + ":" + SERVER_PORT);
while (true) {
System.out.print("请输入消息(输入 'exit' 退出):");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 准备发送数据
byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket sendPacket = new DatagramPacket(
sendData,
sendData.length,
serverAddress,
SERVER_PORT
);
// 发送数据报
socket.send(sendPacket);
System.out.println("已发送消息:" + message);
// 接收响应
byte[] receiveBuffer = new byte[BUFFER_SIZE];
DatagramPacket receivePacket = new DatagramPacket(
receiveBuffer,
receiveBuffer.length
);
// 设置超时时间(可选)
socket.setSoTimeout(5000);
try {
socket.receive(receivePacket);
String response = new String(
receivePacket.getData(),
0,
receivePacket.getLength(),
StandardCharsets.UTF_8
);
System.out.println("服务器响应:" + response);
} catch (java.net.SocketTimeoutException e) {
System.out.println("接收超时,服务器可能未响应");
}
}
} catch (Exception e) {
System.err.println("UDP 客户端错误:" + e.getMessage());
e.printStackTrace();
}
}
}
4.3.5 UDP 广播示例
UDP 支持广播和组播,可以实现一对多的通信:
java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* UDP 广播发送示例
*/
public class UDPBroadcastSender {
private static final int BROADCAST_PORT = 9999;
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket()) {
// 启用广播
socket.setBroadcast(true);
String message = "这是一条广播消息";
byte[] data = message.getBytes();
// 使用广播地址(255.255.255.255 或子网广播地址)
InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(
data,
data.length,
broadcastAddress,
BROADCAST_PORT
);
socket.send(packet);
System.out.println("广播消息已发送");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* UDP 广播接收示例
*/
public class UDPBroadcastReceiver {
private static final int BROADCAST_PORT = 9999;
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(BROADCAST_PORT)) {
System.out.println("开始监听广播消息,端口:" + BROADCAST_PORT);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
socket.receive(packet);
String message = new String(
packet.getData(),
0,
packet.getLength()
);
System.out.println("收到广播消息 [" + packet.getAddress() + "]:" + message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3.6 UDP 使用场景
- 实时性要求高的应用:视频直播、语音通话
- 在线游戏:游戏状态同步,可容忍少量丢包
- DNS 查询:快速查询,无需建立连接
- 日志收集:日志服务器接收来自多个客户端的日志
- 服务发现:局域网内服务发现和注册
4.3.7 UDP 的优缺点
优点:
- 无需建立连接,延迟低
- 资源消耗少,适合高并发
- 支持广播和组播
- 编程相对简单
缺点:
- 不保证数据到达
- 不保证数据顺序
- 没有流量控制和拥塞控制
- 数据报大小有限制(通常 64KB)
4.4 TCP vs UDP 对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(需要握手) | 无连接 |
| 可靠性 | 可靠(保证送达和顺序) | 不可靠(可能丢包、乱序) |
| 传输方式 | 字节流 | 数据报 |
| 速度 | 较慢(有连接开销) | 较快(无连接开销) |
| 资源消耗 | 较高 | 较低 |
| 适用场景 | 文件传输、Web、邮件 | 视频直播、游戏、DNS |
| Java 类 | Socket / ServerSocket |
DatagramSocket / DatagramPacket |
五、NIO:SocketChannel 和 ServerSocketChannel(TCP)
5.1 NIO 核心概念
NIO(New I/O)引入了以下核心概念:
- Channel(通道):类似于流,但可以同时进行读写操作
- Buffer(缓冲区):数据容器,所有数据都通过 Buffer 进行传输
- Selector(选择器):用于监听多个通道的事件,实现 I/O 多路复用
5.2 SocketChannel 类(TCP NIO 客户端)
5.2.1 概念
SocketChannel 是 NIO 中用于客户端的通道,支持非阻塞模式的 TCP 连接。它提供了比传统 Socket 更高的性能和更好的并发处理能力。注意:SocketChannel 只支持 TCP 协议。
4.2.2 主要方法
open():打开一个新的 SocketChannelconfigureBlocking(boolean block):设置阻塞/非阻塞模式connect(SocketAddress remote):连接到远程服务器finishConnect():完成非阻塞连接read(ByteBuffer dst):从通道读取数据到缓冲区write(ByteBuffer src):将缓冲区数据写入通道close():关闭通道
5.2.3 客户端示例(阻塞模式)
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* SocketChannel 客户端示例(阻塞模式)
*/
public class SocketChannelClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open()) {
// 连接到服务器
socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("已连接到服务器:" + socketChannel.getRemoteAddress());
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("请输入消息(输入 'exit' 退出):");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 发送数据
buffer.clear();
buffer.put(message.getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 准备读取
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// 接收响应
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
String response = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println("服务器响应:" + response);
}
}
} catch (IOException e) {
System.err.println("客户端连接失败:" + e.getMessage());
e.printStackTrace();
}
}
}
5.2.4 客户端示例(非阻塞模式)
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* SocketChannel 客户端示例(非阻塞模式 + Selector)
*/
public class NonBlockingSocketChannelClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open();
Selector selector = Selector.open();
Scanner scanner = new Scanner(System.in)) {
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
// 注册连接事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 发起连接
socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("正在连接服务器...");
while (true) {
// 等待事件
int readyChannels = selector.select(1000);
if (readyChannels == 0) {
continue;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isConnectable()) {
// 连接就绪
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
System.out.println("已连接到服务器");
// 注册读事件
channel.register(selector, SelectionKey.OP_READ);
// 启动发送线程
new Thread(() -> sendMessages(channel, scanner)).start();
}
} else if (key.isReadable()) {
// 读就绪
SocketChannel channel = (SocketChannel) key.channel();
readFromServer(channel);
}
}
}
} catch (IOException e) {
System.err.println("客户端连接失败:" + e.getMessage());
e.printStackTrace();
}
}
private static void sendMessages(SocketChannel channel, Scanner scanner) {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (scanner.hasNextLine()) {
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
channel.close();
break;
}
buffer.clear();
buffer.put(message.getBytes(StandardCharsets.UTF_8));
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void readFromServer(SocketChannel channel) {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead > 0) {
//buffet创建之后默认为写入模式,通过flip()切换为读取模式
buffer.flip();
String response = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println("服务器响应:" + response);
} else if (bytesRead < 0) {
// 连接已关闭
channel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.2.5 使用场景
- 需要高性能的网络通信
- 大量并发连接的客户端
- 需要非阻塞 I/O 的场景
- 实时通信应用
5.3 ServerSocketChannel 类(TCP NIO 服务器)
5.3.1 概念
ServerSocketChannel 是 NIO 中用于服务器端的通道,类似于传统的 ServerSocket,但支持非阻塞模式。结合 Selector 可以实现高效的 I/O 多路复用,用一个线程处理多个客户端连接。注意:ServerSocketChannel 只支持 TCP 协议。
4.3.2 主要方法
open():打开一个新的 ServerSocketChannelconfigureBlocking(boolean block):设置阻塞/非阻塞模式bind(SocketAddress local):绑定到本地地址accept():接受客户端连接,返回 SocketChannelregister(Selector sel, int ops):注册到选择器
5.3.3 服务器端示例(完整 NIO 实现)
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* ServerSocketChannel 服务器示例(NIO + Selector)
* 使用单线程处理多个客户端连接
*/
public class NIOServer {
private static final int PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open();
Selector selector = Selector.open()) {
// 设置为非阻塞模式
serverChannel.configureBlocking(false);
// 绑定端口
serverChannel.bind(new InetSocketAddress(PORT));
// 注册到选择器,监听连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务器已启动,监听端口:" + PORT);
System.out.println("等待客户端连接...");
while (true) {
// 阻塞等待事件,最多等待 1 秒
int readyChannels = selector.select(1000);
if (readyChannels == 0) {
continue;
}
// 获取就绪的通道
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 移除已处理的键
if (key.isAcceptable()) {
// 处理连接事件
handleAccept(serverChannel, selector);
} else if (key.isReadable()) {
// 处理读事件
handleRead(key);
} else if (key.isWritable()) {
// 处理写事件
handleWrite(key);
}
}
}
} catch (IOException e) {
System.err.println("服务器启动失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 处理客户端连接
*/
private static void handleAccept(ServerSocketChannel serverChannel, Selector selector)
throws IOException {
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 注册读事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
// 发送欢迎消息
ByteBuffer welcomeBuffer = ByteBuffer.wrap(
"欢迎连接到服务器!\n".getBytes(StandardCharsets.UTF_8));
clientChannel.write(welcomeBuffer);
}
/**
* 处理读事件
*/
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
if (buffer == null) {
buffer = ByteBuffer.allocate(BUFFER_SIZE);
key.attach(buffer);
}
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
String message = StandardCharsets.UTF_8.decode(buffer).toString().trim();
System.out.println("收到客户端消息 [" + clientChannel.getRemoteAddress() + "]:" + message);
// 处理消息并准备响应
String response = processMessage(message);
ByteBuffer responseBuffer = ByteBuffer.wrap(
(response + "\n").getBytes(StandardCharsets.UTF_8));
// 注册写事件
key.interestOps(SelectionKey.OP_WRITE);
key.attach(responseBuffer);
} else if (bytesRead < 0) {
// 客户端断开连接
System.out.println("客户端断开连接:" + clientChannel.getRemoteAddress());
clientChannel.close();
}
}
/**
* 处理写事件
*/
private static void handleWrite(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
if (buffer != null && buffer.hasRemaining()) {
clientChannel.write(buffer);
}
// 写完后重新注册读事件
key.interestOps(SelectionKey.OP_READ);
key.attach(null);
}
/**
* 处理业务逻辑
*/
private static String processMessage(String message) {
if (message.startsWith("ECHO:")) {
return "回显:" + message.substring(5);
} else if (message.equals("TIME")) {
return "当前时间:" + new java.util.Date();
} else if (message.equals("HELP")) {
return "可用命令:ECHO:<消息>、TIME、HELP";
} else {
return "已收到:" + message;
}
}
}
特殊说明: 不能一直注册 OP_WRITE,因为服务器输出缓冲区默认是空闲的,一旦注册 OP_WRITE,Selector 会一直触发该事件(相当于无限循环),导致 CPU 100%。所以必须 "有数据要写时才注册,写完就取消"。
5.3.4 使用场景
- 高并发服务器应用
- 需要处理大量连接的场景(> 1000 连接)
- 实时通信系统(聊天室、游戏服务器等)
- 需要高性能的网络服务
5.3.5 TCP NIO 的优缺点
优点:
- 单线程可以处理大量连接
- 非阻塞 I/O,提高资源利用率
- 适合高并发场景
- 性能优于传统 BIO
缺点:
- 编程复杂度较高
- 需要理解 Channel、Buffer、Selector 等概念
- 调试相对困难
- 对开发人员要求较高
六、NIO UDP:DatagramChannel
6.1 DatagramChannel 类
6.1.1 概念
DatagramChannel 是 NIO 中用于 UDP 通信的通道,支持非阻塞模式。它提供了比传统 DatagramSocket 更高的性能和更好的并发处理能力。
6.1.2 主要方法
open():打开一个新的 DatagramChannelconfigureBlocking(boolean block):设置阻塞/非阻塞模式bind(SocketAddress local):绑定到本地地址send(ByteBuffer src, SocketAddress target):发送数据报receive(ByteBuffer dst):接收数据报connect(SocketAddress remote):连接到远程地址(可选,用于只与特定地址通信)close():关闭通道
6.1.3 UDP NIO 服务器示例
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;
/**
* DatagramChannel UDP 服务器示例(NIO)
*/
public class NIOUDPServer {
private static final int PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
try (DatagramChannel channel = DatagramChannel.open()) {
// 绑定端口
channel.bind(new InetSocketAddress(PORT));
// 设置为阻塞模式(也可以设置为非阻塞模式配合 Selector 使用)
channel.configureBlocking(true);
System.out.println("NIO UDP 服务器已启动,监听端口:" + PORT);
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
while (true) {
buffer.clear();
// 接收数据报
InetSocketAddress clientAddress = (InetSocketAddress) channel.receive(buffer);
if (clientAddress != null) {
buffer.flip();
String message = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println("收到来自 [" + clientAddress + "] 的消息:" + message);
// 准备响应
String response = "服务器已收到:" + message;
buffer.clear();
buffer.put(response.getBytes(StandardCharsets.UTF_8));
buffer.flip();
// 发送响应
channel.send(buffer, clientAddress);
}
}
} catch (IOException e) {
System.err.println("NIO UDP 服务器错误:" + e.getMessage());
e.printStackTrace();
}
}
}
6.1.4 UDP NIO 客户端示例
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* DatagramChannel UDP 客户端示例(NIO)
*/
public class NIOUDPClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
try (DatagramChannel channel = DatagramChannel.open();
Scanner scanner = new Scanner(System.in)) {
InetSocketAddress serverAddress = new InetSocketAddress(SERVER_HOST, SERVER_PORT);
System.out.println("NIO UDP 客户端已启动,连接到服务器:" + SERVER_HOST + ":" + SERVER_PORT);
while (true) {
System.out.print("请输入消息(输入 'exit' 退出):");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 发送数据
ByteBuffer sendBuffer = ByteBuffer.wrap(
message.getBytes(StandardCharsets.UTF_8)
);
channel.send(sendBuffer, serverAddress);
System.out.println("已发送消息:" + message);
// 接收响应
ByteBuffer receiveBuffer = ByteBuffer.allocate(BUFFER_SIZE);
InetSocketAddress responseAddress = (InetSocketAddress) channel.receive(receiveBuffer);
if (responseAddress != null) {
receiveBuffer.flip();
String response = StandardCharsets.UTF_8.decode(receiveBuffer).toString();
System.out.println("服务器响应:" + response);
}
}
} catch (IOException e) {
System.err.println("NIO UDP 客户端错误:" + e.getMessage());
e.printStackTrace();
}
}
}
6.1.5 UDP NIO 非阻塞模式示例(配合 Selector)
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* DatagramChannel UDP 服务器示例(非阻塞模式 + Selector)
*/
public class NonBlockingNIOUDPServer {
private static final int PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
try (DatagramChannel channel = DatagramChannel.open();
Selector selector = Selector.open()) {
channel.bind(new InetSocketAddress(PORT));
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
System.out.println("非阻塞 NIO UDP 服务器已启动,监听端口:" + PORT);
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
handleRead(key);
}
}
}
} catch (IOException e) {
System.err.println("服务器错误:" + e.getMessage());
e.printStackTrace();
}
}
private static void handleRead(SelectionKey key) throws IOException {
DatagramChannel channel = (DatagramChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
InetSocketAddress clientAddress = (InetSocketAddress) channel.receive(buffer);
if (clientAddress != null) {
buffer.flip();
String message = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println("收到来自 [" + clientAddress + "] 的消息:" + message);
// 发送响应
String response = "服务器已收到:" + message;
buffer.clear();
buffer.put(response.getBytes(StandardCharsets.UTF_8));
buffer.flip();
channel.send(buffer, clientAddress);
}
}
}
特殊说明: 为什么tcp的多路复用器需要监听写就绪事件,udp的多路复用器不需要监听写就绪事件?
TCP需要监听写就绪事件:
- TCP 有流量控制(滑动窗口),服务器的输出缓冲区可能会满,导致 write() 阻塞:
- TCP 是可靠传输,客户端会向服务器返回「接收窗口大小」(告诉服务器 "我还能接收多少数据");
- 服务器发送数据时,会先写入内核的「输出缓冲区」,再由内核通过网络发给客户端;如果客户端的接收窗口满了(比如客户端处理数据很慢,自己的输入缓冲区满了),服务器的输出缓冲区就会被塞满 ------ 此时调用 clientSocketChannel.write() 会阻塞(非阻塞模式下会返回 "写失败" 或 "写了部分数据")。 因此,TCP 需要 OP_WRITE 事件:当服务器的输出缓冲区有空闲空间(能容纳新数据)时,Selector 触发 OP_WRITE,通知服务器 "可以无阻塞写数据了" UDP 写操作永远不会阻塞(监听了反而出问题):
- UDP 无流量控制:UDP 是 "尽力交付",不管客户端是否能接收(哪怕客户端断开连接),服务器都能直接发送数据包(不会因为客户端 "忙" 而被阻塞);
- UDP 输出缓冲区不会 "满到需要等待":UDP 是数据包传输,每个数据包独立发送,内核的 UDP 输出缓冲区通常足够容纳单个数据包(即使缓冲区临时满了,内核也会直接丢弃数据包,不会让应用层的 send() 阻塞);
- 监听 OP_WRITE 会导致 CPU 飙升:UDP 的输出缓冲区默认是空闲的,一旦注册 OP_WRITE,Selector 会疯狂触发该事件(因为缓冲区一直有空),导致应用层无限循环处理 OP_WRITE,CPU 占用率直接拉满。 简单说:UDP 的 send() 操作在非阻塞模式下「要么立即发送成功,要么立即失败(丢弃数据包)」,永远不会阻塞 ------ 因此不需要 OP_WRITE 事件通知 "可以写了",直接调用 send() 即可。