网络编程模型BIO,NIO
概念
BIO
采用阻塞式 IO 模型:服务器端每接受一个客户端连接,就需要创建一个独立的线程处理该连接的读写;如果没有数据读写,线程会一直阻塞在 read()/write() 方法上。
NIO
一个线程管理多个连接,只有连接有实际数据时才处理,避免 BIO 的线程阻塞和资源浪费。
(1)Channel:双向的 “数据管道”
常用实现类:
ServerSocketChannel:服务器端监听连接(对应 BIO 的 ServerSocket);
SocketChannel:客户端 / 服务器端通信(对应 BIO 的 Socket);
FileChannel:文件读写(本文聚焦网络编程,暂不展开)。
核心特性:可设置为 configureBlocking(false)(非阻塞模式),这是 NIO 高性能的基础。
(2)Buffer:数据的 “中转站”
常用实现类:ByteBuffer(网络编程最常用)、CharBuffer、IntBuffer 等;
核心属性:
capacity:缓冲区总容量(不可变);
position:当前读写位置;
limit:读写的边界(最多能读 / 写多少数据);
核心方法:
flip():切换为 “读模式”(写完数据后,准备读);
clear():清空缓冲区(重置 position/limit,并非删除数据);
compact():压缩缓冲区(保留未读完的数据,方便后续继续读)。
(3)Selector:多连接的 “监听器”
核心作用:一个 Selector 可以监听多个 Channel 的事件,常见事件:
OP_ACCEPT:服务器端有新的客户端连接请求;
OP_READ:Channel 有数据可读;
OP_WRITE:Channel 可以写入数据;
核心流程:
Channel 注册到 Selector,并指定要监听的事件;
Selector 调用 select() 阻塞等待事件(无事件时线程休眠,有事件时唤醒);
处理就绪的事件(如接连接、读数据)。
代码实现
回声服务器案例
BIO
Server端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO回声服务器:接收客户端消息并原样返回
*/
public class BioEchoServer {
// 监听端口
private static final int PORT = 8080;
public static void main(String[] args) {
// 声明ServerSocket,使用try-with-resources自动关闭资源
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("BIO服务器已启动,监听端口:" + PORT);
// 无限循环监听客户端连接
while (true) {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接:" + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
// 为每个客户端创建独立线程处理
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
System.err.println("服务器启动失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 客户端处理器:处理单个客户端的读写操作
*/
static class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
// 使用try-with-resources自动关闭流和socket
try (
// 输入流:读取客户端发送的消息
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 输出流:向客户端发送响应(PrintWriter更易用,自动刷新)
PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()), true)
) {
String msg;
// 阻塞读取客户端消息,直到客户端关闭连接(readLine返回null)
while ((msg = in.readLine()) != null) {
System.out.println("收到客户端[" + clientSocket.getPort() + "]消息:" + msg);
// 回声响应:原样返回消息
out.println("服务器响应:" + msg);
// 特殊指令:客户端发送"exit"则关闭连接
if ("exit".equalsIgnoreCase(msg)) {
System.out.println("客户端[" + clientSocket.getPort() + "]主动断开连接");
break;
}
}
} catch (IOException e) {
System.err.println("客户端[" + clientSocket.getPort() + "]处理异常:" + e.getMessage());
} finally {
// 确保关闭客户端连接
try {
clientSocket.close();
System.out.println("客户端[" + clientSocket.getPort() + "]连接已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
为每个连接新增一个线程处理,这个步骤可使用线程池优化。
Client端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* BIO客户端:向服务器发送消息并接收响应
*/
public class BioEchoClient {
public static void main(String[] args) {
// 服务器地址和端口
String serverIp = "127.0.0.1";
int serverPort = 8080;
// 建立连接并通信
try (
Socket socket = new Socket(serverIp, serverPort);
// 读取用户输入
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
// 向服务器发送消息
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
// 读取服务器响应
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
System.out.println("已连接到BIO服务器:" + serverIp + ":" + serverPort);
System.out.println("请输入消息(输入exit退出):");
String inputMsg;
// 循环读取用户输入并发送
while ((inputMsg = consoleReader.readLine()) != null) {
// 发送消息到服务器
out.println(inputMsg);
// 若输入exit,退出循环
if ("exit".equalsIgnoreCase(inputMsg)) {
System.out.println("客户端主动退出");
break;
}
// 阻塞读取服务器响应
String response = in.readLine();
System.out.println("服务器返回:" + response);
}
} catch (UnknownHostException e) {
System.err.println("无法连接服务器:" + serverIp + ":" + serverPort);
} catch (IOException e) {
System.err.println("客户端通信异常:" + e.getMessage());
}
}
}
代码关键说明
ServerSocket.accept():主线程阻塞等待客户端连接,是 BIO 第一个阻塞点;
BufferedReader.readLine():工作线程阻塞读取客户端数据,是 BIO 第二个阻塞点;
try-with-resources:自动关闭 Socket、流等资源,避免手动关闭遗漏;
ClientHandler:每个客户端连接对应一个独立线程,体现 “一连接一线程” 的核心设计。
ServerSocket和Socket的区别:ServerSocket服务器端专用的Socket,监听指定端口,阻塞等待客户端连接请求。而Socket专门用来处理数据交互,服务端对于每个客户端都会创建一个单独的Socket与客户端的Socket进行数据交互。
NIO
Server端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* NIO回声服务器:单线程管理多连接,非阻塞IO + 多路复用
*/
public class NioEchoServer {
// 监听端口
private static final int PORT = 8080;
// 缓冲区大小(1KB)
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
// 1. 创建Selector(多路复用器)
try (Selector selector = Selector.open()) {
// 2. 创建ServerSocketChannel(服务器端监听通道)
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 绑定端口
serverChannel.bind(new InetSocketAddress(PORT));
// 设置为非阻塞模式(必须!否则Selector无法工作)
serverChannel.configureBlocking(false);
// 3. 将ServerSocketChannel注册到Selector,监听OP_ACCEPT事件
// 第三个参数为附件(可传任意对象,这里暂传null)
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务器已启动,监听端口:" + PORT);
System.out.println("等待客户端连接...");
// 4. 无限循环处理事件
while (true) {
// 阻塞等待事件就绪(返回就绪的事件数,可传超时时间如selector.select(1000))
int readyChannels = selector.select();
// 无就绪事件,继续循环
if (readyChannels == 0) {
continue;
}
// 5. 获取所有就绪的事件(SelectionKey)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 6. 遍历处理每个就绪事件
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 移除已处理的key(必须!否则会重复处理)
keyIterator.remove();
// ========== 处理连接事件(OP_ACCEPT) ==========
if (key.isAcceptable()) {
handleAccept(key);
}
// ========== 处理读事件(OP_READ) ==========
if (key.isReadable()) {
handleRead(key);
}
}
}
} catch (IOException e) {
System.err.println("服务器异常:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 处理客户端连接事件(OP_ACCEPT)
*/
private static void handleAccept(SelectionKey key) throws IOException {
// 1. 获取ServerSocketChannel
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 2. 接受客户端连接(非阻塞!因为ServerSocketChannel已设为非阻塞)
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel == null) {
return; // 无连接时返回null(非阻塞特性)
}
// 3. 设置客户端通道为非阻塞模式
clientChannel.configureBlocking(false);
System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
// 4. 将客户端通道注册到Selector,监听OP_READ事件,并绑定一个ByteBuffer作为附件
clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUFFER_SIZE));
}
/**
* 处理读事件(OP_READ)
*/
private static void handleRead(SelectionKey key) throws IOException {
// 1. 获取客户端通道
SocketChannel clientChannel = (SocketChannel) key.channel();
// 2. 获取绑定的缓冲区(注册时传入的附件)
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 3. 非阻塞读取数据(返回读取的字节数,-1表示客户端关闭连接)
int readBytes = clientChannel.read(buffer);
// ========== 客户端关闭连接 ==========
if (readBytes == -1) {
System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]断开连接");
key.cancel(); // 取消注册
clientChannel.close(); // 关闭通道
return;
}
// ========== 读取并处理数据 ==========
if (readBytes > 0) {
buffer.flip(); // 切换为读模式(position归0,limit设为当前position)
// 将缓冲区数据转为字符串
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String msg = new String(bytes).trim();
System.out.println("收到客户端[" + clientChannel.getRemoteAddress() + "]消息:" + msg);
// 特殊指令:exit关闭连接
if ("exit".equalsIgnoreCase(msg)) {
System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]主动退出");
key.cancel();
clientChannel.close();
return;
}
// ========== 回声响应:将数据写回客户端 ==========
ByteBuffer responseBuffer = ByteBuffer.wrap(("服务器响应:" + msg + "\n").getBytes());
clientChannel.write(responseBuffer); // 非阻塞写(可能写不完,实际开发需处理半包)
// 重置缓冲区,准备下次读取
buffer.clear();
}
}
}
Client端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* NIO客户端:向服务器发送消息并接收响应
*/
public class NioEchoClient {
private static final String SERVER_IP = "127.0.0.1";
private static final int SERVER_PORT = 8080;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
// 1. 创建SocketChannel(客户端通道)
try (SocketChannel clientChannel = SocketChannel.open()) {
// 2. 设置为非阻塞模式
clientChannel.configureBlocking(false);
// 3. 连接服务器(非阻塞,立即返回,需要检查连接状态)
clientChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));
// 等待连接完成(非阻塞连接需要手动检查)
while (!clientChannel.finishConnect()) {
// 连接未完成时,可做其他事(这里简单休眠)
Thread.sleep(10);
}
System.out.println("已连接到NIO服务器:" + SERVER_IP + ":" + SERVER_PORT);
System.out.println("请输入消息(输入exit退出):");
// 读取控制台输入
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
ByteBuffer sendBuffer = ByteBuffer.allocate(BUFFER_SIZE);
ByteBuffer receiveBuffer = ByteBuffer.allocate(BUFFER_SIZE);
String inputMsg;
while ((inputMsg = consoleReader.readLine()) != null) {
// ========== 发送消息到服务器 ==========
sendBuffer.clear();
sendBuffer.put(inputMsg.getBytes());
sendBuffer.flip(); // 切换为读模式,准备发送
clientChannel.write(sendBuffer);
// 输入exit则退出
if ("exit".equalsIgnoreCase(inputMsg)) {
System.out.println("客户端主动退出");
break;
}
// ========== 接收服务器响应 ==========
receiveBuffer.clear();
int readBytes = clientChannel.read(receiveBuffer);
if (readBytes > 0) {
receiveBuffer.flip();
byte[] responseBytes = new byte[receiveBuffer.remaining()];
receiveBuffer.get(responseBytes);
System.out.println("服务器返回:" + new String(responseBytes).trim());
}
}
} catch (IOException | InterruptedException e) {
System.err.println("客户端异常:" + e.getMessage());
e.printStackTrace();
}
}
}
NIO需要处理的问题
「粘包 / 半包」问题
- 核心成因(网络传输的本质特性)
粘包 / 半包不是 NIO 独有的问题(BIO 也存在),但 NIO 是非阻塞 + 缓冲区读写,会让这个问题更突出,根源是:
TCP 是 “流式协议”:TCP 只保证数据有序、可靠传输,但不会保留 “消息边界”—— 发送方发 10 次 100 字节的消息,接收方可能一次收到 1000 字节(粘包),也可能一次只收到 50 字节(半包);
NIO 基于 Buffer 读写:NIO 读取数据时,是从 Channel 读取到 ByteBuffer,而 ByteBuffer 有固定大小(比如 1024 字节),如果一次读取的字节数刚好截断了完整消息,就会产生半包;如果多个消息被一次性读入缓冲区,就会产生粘包。 - 通俗举例
粘包:你发 3 条消息 [“hello”, “world”, “nio”],TCP 可能把它们拼成一个字节流发送,接收方一次读到所有字节,分不清哪里是消息边界;
半包:你发一条 2000 字节的消息,接收方 ByteBuffer 只有 1024 字节,第一次只读到 1024 字节(半包),剩下的 976 字节需要下次读取。 - NIO 中更突出的原因
BIO 是阻塞读取,开发者可以等完整消息读完再处理;但 NIO 是非阻塞读取,channel.read(buffer) 可能返回任意字节数(0 到 buffer 容量),必须手动处理 “没读完” 或 “读多了” 的情况,否则就会解析错误。 - 解决思路
自定义协议:给消息加 “边界标识”,比如固定长度(前 4 字节表示消息长度)、分隔符(如 \n);
缓冲区拼接:半包时把未读完的字节暂存,下次读取后拼接成完整消息;粘包时按协议拆分缓冲区中的字节流。
「Selector 空轮询」问题
- 核心成因(JDK 底层 Bug + NIO 设计缺陷)
这是 NIO 特有的严重问题,根源是 JDK 对 Selector 的实现有漏洞:
Selector.select () 空唤醒:正常情况下,selector.select() 会阻塞到有事件就绪,但 JDK 1.4~1.7 存在 Bug,导致 select() 会无理由 “空唤醒”(返回 0 个就绪事件);
空轮询循环:如果发生空唤醒,线程会进入 “select() → 空唤醒 → 再次 select()” 的死循环,CPU 占用率直接飙升到 100%,服务器性能雪崩。 - 通俗举例
你用 Selector 监听 1000 个连接,正常情况下 select() 会阻塞到有连接发数据;但空轮询时,select() 每隔几毫秒就 “假醒” 一次,线程不停循环,什么正事都没干,却把 CPU 跑满。 - 触发场景
客户端连接后快速断开;
网络波动导致 Channel 状态异常;
多线程操作 Selector 时的竞态条件。 - 解决思路
超时检测:记录 select() 空唤醒的次数,超过阈值就重建 Selector(关闭旧 Selector,重新注册所有 Channel);
升级 JDK:JDK 1.8 对该 Bug 做了优化(但未完全修复);
使用成熟框架:Netty 内置了空轮询的检测和修复机制(比如 NioEventLoop 中的 selectCnt 计数),不用开发者手动处理。
解决案例:
一、针对半包/粘包:自定义处理消息编解码
import java.nio.ByteBuffer;
/**
* 消息编解码工具:解决粘包/半包问题(固定长度头协议)
* 协议格式:[4字节消息长度][消息体字节数组]
*/
public class MessageCodec {
// 消息头长度(4字节,存储int类型的消息长度)
public static final int HEADER_LENGTH = 4;
/**
* 编码:将字符串消息转为符合协议的ByteBuffer
*/
public static ByteBuffer encode(String msg) {
byte[] bodyBytes = msg.getBytes();
// 1. 创建缓冲区(头4字节 + 消息体长度)
ByteBuffer buffer = ByteBuffer.allocate(HEADER_LENGTH + bodyBytes.length);
// 2. 写入消息长度(消息头)
buffer.putInt(bodyBytes.length);
// 3. 写入消息体
buffer.put(bodyBytes);
// 4. 切换为读模式,准备发送
buffer.flip();
return buffer;
}
/**
* 解码:从缓冲区中解析出完整的消息(处理半包/粘包)
* @param buffer 待解析的缓冲区(包含未处理的字节)
* @param tempBuffer 暂存半包数据的缓冲区
* @return 完整的消息(无完整消息时返回null)
*/
public static String decode(ByteBuffer buffer, ByteBuffer tempBuffer) {
// 1. 将新读取的字节写入临时缓冲区
tempBuffer.put(buffer);
// 2. 切换为读模式,准备解析
tempBuffer.flip();
// 3. 检查是否至少有消息头(4字节)
if (tempBuffer.remaining() < HEADER_LENGTH) {
tempBuffer.compact(); // 压缩缓冲区(保留未读完的数据)
return null; // 半包(连消息头都没读完)
}
// 4. 读取消息头(获取消息体长度)
int bodyLength = tempBuffer.getInt();
// 5. 检查是否有完整的消息体
if (tempBuffer.remaining() < bodyLength) {
// 半包(消息头读完,但消息体没读完):重置position,保留数据
tempBuffer.position(tempBuffer.position() - HEADER_LENGTH);
tempBuffer.compact();
return null;
}
// 6. 读取完整消息体
byte[] bodyBytes = new byte[bodyLength];
tempBuffer.get(bodyBytes);
String msg = new String(bodyBytes);
// 7. 处理粘包:将剩余未解析的字节保留在临时缓冲区
ByteBuffer remainingBuffer = tempBuffer.slice();
tempBuffer.clear();
tempBuffer.put(remainingBuffer);
return msg;
}
}
修复后的服务端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 解决粘包/半包的NIO服务器(固定长度头协议)
*/
public class NioFixStickyHalfServer {
private static final int PORT = 8080;
// 临时缓冲区大小(用于暂存半包数据)
private static final int TEMP_BUFFER_SIZE = 4096;
public static void main(String[] args) {
try (Selector selector = Selector.open()) {
// 初始化服务器通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("修复粘包/半包的NIO服务器启动,端口:" + PORT);
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 处理连接事件
if (key.isAcceptable()) {
handleAccept(key);
}
// 处理读事件(核心:用自定义协议解析)
if (key.isReadable()) {
handleRead(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理客户端连接:为每个客户端绑定临时缓冲区(存储半包数据)
*/
private static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel == null) return;
clientChannel.configureBlocking(false);
// 为客户端通道绑定临时缓冲区(用于暂存半包数据)
ByteBuffer tempBuffer = ByteBuffer.allocate(TEMP_BUFFER_SIZE);
clientChannel.register(key.selector(), SelectionKey.OP_READ, tempBuffer);
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
}
/**
* 处理读事件:用MessageCodec解析,解决粘包/半包
*/
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 获取绑定的临时缓冲区
ByteBuffer tempBuffer = (ByteBuffer) key.attachment();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = clientChannel.read(readBuffer);
// 客户端断开连接
if (readBytes == -1) {
System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
return;
}
if (readBytes > 0) {
readBuffer.flip();
// 核心:用自定义协议解码,处理粘包/半包
String msg = MessageCodec.decode(readBuffer, tempBuffer);
if (msg != null) {
System.out.println("收到完整消息:" + msg);
// 回声响应(编码后发送)
ByteBuffer responseBuffer = MessageCodec.encode("服务器响应:" + msg);
clientChannel.write(responseBuffer);
// 特殊指令退出
if ("exit".equalsIgnoreCase(msg)) {
System.out.println("客户端主动退出:" + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
}
}
}
}
}
(3)配套客户端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* 配套客户端(发送符合协议的消息)
*/
public class NioFixStickyHalfClient {
private static final String SERVER_IP = "127.0.0.1";
private static final int SERVER_PORT = 8080;
public static void main(String[] args) {
try (SocketChannel clientChannel = SocketChannel.open()) {
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));
// 等待连接完成
while (!clientChannel.finishConnect()) {
Thread.sleep(10);
}
System.out.println("连接服务器成功,输入消息(exit退出):");
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
String inputMsg;
while ((inputMsg = consoleReader.readLine()) != null) {
// 核心:编码后发送(符合固定长度头协议)
ByteBuffer buffer = MessageCodec.encode(inputMsg);
clientChannel.write(buffer);
if ("exit".equalsIgnoreCase(inputMsg)) {
System.out.println("客户端退出");
break;
}
// 读取服务器响应
ByteBuffer responseBuffer = ByteBuffer.allocate(4096);
int readBytes = clientChannel.read(responseBuffer);
if (readBytes > 0) {
responseBuffer.flip();
// 解码响应消息
String response = MessageCodec.decode(responseBuffer, ByteBuffer.allocate(4096));
if (response != null) {
System.out.println("服务器响应:" + response);
}
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
代码关键说明
MessageCodec.encode():给消息加 4 字节长度头,保证发送的消息符合协议;
MessageCodec.decode():先读长度头,再读完整消息体,半包时用 tempBuffer 暂存数据,粘包时拆分缓冲区;
tempBuffer.compact():压缩缓冲区,保留未读完的半包数据,避免丢失;
每个客户端绑定独立的 tempBuffer,确保多客户端数据不混淆。
二、解决 Selector 空轮询问题(核心:超时检测 + 重建 Selector)
- 解决思路
JDK 空轮询 Bug 表现为 selector.select() 频繁空返回(返回 0),解决方案:
记录 select() 空返回次数;
超过阈值(如 5 次)判定为空轮询,重建 Selector(关闭旧 Selector,重新注册所有 Channel);
核心:保证重建过程中不丢失客户端连接。 - 完整代码(修复空轮询的 NIO 服务器)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
/**
* 解决Selector空轮询的NIO服务器
* 核心:检测空轮询次数,超过阈值重建Selector
*/
public class NioFixEmptyPollServer {
private static final int PORT = 8080;
// 空轮询阈值(连续空返回5次判定为空轮询)
private static final int EMPTY_POLL_THRESHOLD = 5;
// 临时缓冲区大小
private static final int TEMP_BUFFER_SIZE = 4096;
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
// 初始化服务器通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("修复空轮询的NIO服务器启动,端口:" + PORT);
int emptyPollCount = 0; // 空轮询计数器
while (true) {
// 带超时的select(避免永久阻塞)
int readyChannels = selector.select(1000);
// 检测空轮询:select返回0表示无就绪事件
if (readyChannels == 0) {
emptyPollCount++;
// 超过阈值,重建Selector
if (emptyPollCount >= EMPTY_POLL_THRESHOLD) {
System.out.println("检测到Selector空轮询,开始重建...");
selector = rebuildSelector(selector);
emptyPollCount = 0; // 重置计数器
continue;
}
} else {
emptyPollCount = 0; // 有就绪事件,重置计数器
}
// 处理就绪事件(逻辑和之前一致)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
}
}
}
/**
* 重建Selector:解决空轮询Bug
*/
private static Selector rebuildSelector(Selector oldSelector) throws IOException {
// 1. 创建新Selector
Selector newSelector = Selector.open();
// 2. 遍历旧Selector的所有Channel,重新注册到新Selector
Set<SelectionKey> keys = oldSelector.keys();
for (SelectionKey key : keys) {
if (!key.isValid()) {
continue; // 无效key跳过
}
SelectableChannel channel = key.channel();
int interestOps = key.interestOps();
Object attachment = key.attachment(); // 保留原附件(如临时缓冲区)
// 取消旧注册
key.cancel();
// 重新注册到新Selector
channel.register(newSelector, interestOps, attachment);
}
// 3. 关闭旧Selector
oldSelector.close();
System.out.println("Selector重建完成");
return newSelector;
}
// 以下handleAccept/handleRead方法和上一个服务器完全一致,复制即可
private static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel == null) return;
clientChannel.configureBlocking(false);
ByteBuffer tempBuffer = ByteBuffer.allocate(TEMP_BUFFER_SIZE);
clientChannel.register(key.selector(), SelectionKey.OP_READ, tempBuffer);
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
}
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer tempBuffer = (ByteBuffer) key.attachment();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = clientChannel.read(readBuffer);
if (readBytes == -1) {
System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
return;
}
if (readBytes > 0) {
readBuffer.flip();
String msg = MessageCodec.decode(readBuffer, tempBuffer);
if (msg != null) {
System.out.println("收到完整消息:" + msg);
ByteBuffer responseBuffer = MessageCodec.encode("服务器响应:" + msg);
clientChannel.write(responseBuffer);
if ("exit".equalsIgnoreCase(msg)) {
System.out.println("客户端主动退出:" + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
}
}
}
}
}
- 代码关键说明
selector.select(1000):带 1 秒超时的 select,避免永久阻塞,便于检测空轮询;
emptyPollCount:统计连续空返回次数,超过阈值触发重建;
rebuildSelector():核心方法,将旧 Selector 的所有 Channel 重新注册到新 Selector,保留原附件(如半包临时缓冲区),保证业务不中断;
重建后重置计数器,恢复正常监听。
更多推荐

所有评论(0)