Java NIO
Java NIO
NIO 是什么?为什么需要它?
NIO(New I/O 或 Non-blocking I/O) 是 Java 1.4 引入的一套高性能 I/O 框架,用于替代传统的 BIO(Blocking I/O)。它的目标是:用更少的线程,处理更多的连接,特别适合高并发场景(如聊天服务器、游戏后端、日志收集系统等)。
而 NIO 的强大,离不开它的 三大核心组件:
✅ Buffer(缓冲区)
✅ Channel(通道)
✅ Selector(选择器)
核心组件讲解:
buffer(缓冲区)
⁉️ 是什么?
Buffer 是一个固定大小的容器,用于临时存储读写的数据。
所有 NIO 操作都必须通过 Buffer,不能直接操作 Channel。
🔑 核心属性:
属性 说明
capacity 容量,最大能存多少数据(创建后不可变)
position 当前读/写的位置(从 0 开始)
limit 最多能读/写到哪里
🪧 关键方法:
flip():写 → 读 切换(把 position 设为 0,limit 设为当前 position)
clear():清空缓冲区(position=0, limit=capacity)
rewind():重置 position=0,保留数据(用于重复读)
💡 举个生活例子:
你往快递箱(Buffer)里塞包裹(写数据),塞满后盖上盖子(flip()),快递员(Channel)来取走(读数据)。下次再用,先清空箱子(clear())。
✅ 代码示例:文件复制(使用 ByteBuffer)
// 文件复制:source.txt → target.txt
try (FileChannel in = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
FileChannel out = FileChannel.open(Paths.get("target.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配 1KB 缓冲区
while (in.read(buffer) != -1) { // 读入数据
buffer.flip(); // 切换为读模式
out.write(buffer); // 写出数据
buffer.clear(); // 清空,准备下一轮
}
}
channel(通道)
⁉️ 是什么?
- Channel 类似于传统 IO 的 流(Stream),但它是双向的(既能读也能写)。
常见实现:- FileChannel:文件读写
- SocketChannel:TCP 客户端
- ServerSocketChannel:TCP 服务端
- DatagramChannel:UDP
✨ 特点:
必须配合 Buffer 使用
支持 阻塞 / 非阻塞模式(通过 configureBlocking(false) 设置)
✅ 代码示例:非阻塞 TCP 服务端(原生 NIO)
// 创建 ServerSocketChannel
ServerSocketChannel server = ServerSocketChan nel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // ⭐ 设置为非阻塞!
// 接受连接(非阻塞:无连接时立即返回 null)
SocketChannel client = server.accept();
if (client != null) {
client.configureBlocking(false);
// 后续用 Selector 监听读事件...
}
Selector(选择器)
⁉️ 是什么?
Selector 允许单个线程监听多个 Channel 的 I/O 事件(如可读、可写、连接)。
这就是 I/O 多路复用 的核心,也是 NIO 高并发的关键!
🔑 工作流程:
创建 Selector
将 Channel 注册到 Selector,并指定关注事件(OP_READ, OP_WRITE 等)
调用 select() 阻塞等待事件发生
遍历 selectedKeys() 处理就绪的 Channel
✅ 代码示例:单线程管理多个客户端
Selector selector = Selector.open(); // 打开一个选择器
ServerSocketChannel server = ServerSocketChannel.open(); // 打开一个TCP服务端通道
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 设置“非阻塞”模式运行,如不设置则不可注册到selector中
server.register(selector, SelectionKey.OP_ACCEPT); // 设置关注“新连接”事件
while (true) {
selector.select(); // 阻塞直到有意向的事件发生,这里底层是有操作系统的epoll(windows是select)实现
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须remove
if (key.isAcceptable()) {
// 处理新连接
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ); // 这里把client也注册到selector中,关注“可读”事件
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = client.read(buf);
if (len > 0) {
buf.flip();
System.out.println("收到: " + new String(buf.array(), 0, len));
}
}
}
}
✳️Selector可管理的通道
不是所有通道都可以被Selector管理,比如FileChannel,判断是否可以使用Selector管理。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel , 则可以被复用,否则不能。
❓为什么 FileChannel 不使用 Selector?
磁盘I/O与网络I/O的区别:磁盘I/O通常是顺序的,并且访问时间是可预测的,尤其是对于本地文件系统。相反,网络I/O往往是非常不确定的,因为网络延时和丢包等因素都会影响数据的到达时间。
文件系统的同步性与网络的异步性:当我们执行磁盘上的文件操作时,例如从文件中读取数据或向文件写入数据,这些操作大部分时间是同步的。操作系统通常会保证在进行下一个操作之前,当前的操作已经完成(一次性做完不用长时间保持连接,网络对端不止是否完成所有操作,具有不确定性)。这种可预测性意味着,文件系统的操作很少需要非阻塞或多路复用的特性。
不过,jdk.17也提供了异步非阻塞的AsynchronousFileChannel,简单来说实现原理是通过传入一个回调函数实现
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
asynchronousFileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
// 数据读取完成后,这个方法将被调用(读不完还要嵌套)
System.out.println("Read done");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
// 数据读取失败后,这个方法将被调用
System.out.println("Read failed");
exc.printStackTrace();
}
});
总结
网络IO
✡️ NIO 三大件关系图
┌─────────────┐
│ Selector │ ← 单线程监控多个 Channel
└──────┬──────┘
│
┌──────────▼──────────┐
│ ServerSocketChannel│ ← 注册 OP_ACCEPT
│ SocketChannel │ ← 注册 OP_CONNECT/OP_READ/OP_WRITE
│ DatagramChannel │ ← 注册 OP_READ/OP_WRITE
└──────────┬──────────┘
│
┌──────▼──────┐
│ Buffer │ ← 所有数据读写必须经过 Buffer
└─────────────┘
↔️ 以下是传统 BIO 多线程模型(ServerSocket+多线程处理IO)与 NIO 多路复用模型(Selector + ServerSocketChannel)的详细对比:
| 特性 | 传统 BIO (ServerSocket + Socket + 多线程) | NIO 非阻塞多路复用 (Selector + ServerSocketChannel) |
|---|---|---|
| I/O 模型 | 阻塞 I/O | 非阻塞 I/O + 多路复用 |
| 线程模型 | 一连接一线程(或线程池) | 一线程可管理多个连接(Reactor 模式) |
| 连接处理方式 | 每个连接由独立线程处理,线程在读写时阻塞 | 一个线程轮询多个通道的就绪事件,仅就绪后处理 |
| 资源开销 | 线程数随连接数增长,内存和上下文切换开销大 | 少量线程处理大量连接,内存开销低,上下文切换少 |
| 最大并发连接数 | 受限于系统线程数(通常几百到几千) | 可支持数万甚至数十万连接(取决于系统资源) |
| 线程利用率 | 低,大量线程空闲等待 I/O | 高,线程始终处理就绪事件,无空闲等待 |
| 数据读写 | 直接读写,可能阻塞导致线程挂起 | 就绪后才读写,不会阻塞线程 |
| 编程复杂度 | 低,顺序编程,易于理解 | 高,需处理事件注册、缓冲区管理、粘包半包等 |
| 适用场景 | 连接数少、长连接、低并发(如小规模内网服务) | 高并发、短连接、大量空闲连接(如Web服务器、聊天服务) |
| 扩展性 | 差,增加连接需增加线程或线程池大小 | 好,可通过增加 Selector 或线程池进一步扩展 |
| 典型应用 | 早期简单服务器,如单机小应用 | Netty、Tomcat NIO 模式、Redis(单线程多路复用) |
| 数据拷贝优化 | 通常涉及多次内存拷贝 | 可配合直接缓冲区实现零拷贝(如 FileChannel.transferTo) |
| 异常处理 | 线程内异常易处理,单个连接故障不影响其他 | 事件驱动,需小心处理异常,避免影响整个 Selector 循环 |
核心差异总结
- BIO 多线程:简单直观,但线程资源随连接数线性增长,适合连接数可控的低并发场景。
- NIO 多路复用:通过少量线程管理海量连接,适合高并发、长连接或大量空闲连接的场景,但开发复杂度较高。
实际生产中,可结合两者优势:用 NIO 处理 I/O 事件,将业务逻辑交给线程池,实现高性能与可维护性的平衡。
文件IO
⏬ 实现
-
FileChannel(jdk1.4)
-
AsynchronousFileChannel(jdk1.7)
↔️ 以下是文件BIO(传统I/O)与NIO(阻塞 NIO、异步NIO)的详细对比:
| 特性 | BIO 流 (FileInputStream/OutputStream) | FileChannel (阻塞 NIO) | AsynchronousFileChannel (异步 NIO) |
|---|---|---|---|
| 所属包 | java.io |
java.nio.channels |
java.nio.channels |
| I/O 模型 | 流式 I/O | 通道 + 缓冲区 | 异步通道 + 缓冲区 |
| 阻塞特性 | 阻塞(线程等待操作完成) | 阻塞(线程等待操作完成) | 非阻塞(调用立即返回,完成后回调/Future) |
| 线程模型 | 每个操作独占线程 | 每个操作独占线程 | 少量线程可管理大量并发操作 |
| 读写方向 | 单向(读或写需不同流) | 双向(同一个通道可读可写) | 双向(同一个通道可读可写) |
| 缓冲区类型 | byte[](堆内存) |
ByteBuffer(可堆内或直接内存) |
ByteBuffer(可堆内或直接内存) |
| 零拷贝传输 | 不支持 | 支持 transferTo/transferFrom |
不支持(无直接传输方法) |
| 内存映射文件 | 不支持 | 支持 map() 获取 MappedByteBuffer |
不支持 |
| 数据强制落盘 | 需 FileDescriptor.sync()(阻塞) |
force(boolean metaData)(阻塞) |
force(boolean metaData)(阻塞) |
| 文件锁定 | 需 FileChannel 间接实现 |
lock()/tryLock()(阻塞) |
lock()(异步,Future 或回调) |
| 与其他 NIO 集成 | 差(无法直接与 Selector 等配合) | 好(可与 ByteBuffer、Charset 等配合) |
好(可与异步机制、ByteBuffer 等配合) |
| 编程复杂度 | 低(顺序编程) | 中(需管理缓冲区、位置) | 高(需处理回调或 Future 链) |
| 适用场景 | 简单文件操作、小文件、顺序读写 | 大文件、随机访问、需零拷贝或内存映射的场景 | 高并发文件 I/O、需非阻塞的复杂应用 |
说明
- 阻塞特性:
FileChannel虽属 NIO,但默认阻塞;AsynchronousFileChannel实现真正异步非阻塞。 - 零拷贝:
FileChannel的transferTo可直接将文件数据发送到网络通道,减少 CPU 拷贝。 - 内存映射:
FileChannel可将文件映射到内存,像访问数组一样操作文件,适合大文件随机读写。 - 数据强制落盘:三者均可确保数据写入磁盘,但均为阻塞操作(包括异步通道的
force方法)。 - 文件锁定:
AsynchronousFileChannel的锁定方法支持异步,避免阻塞调用线程。
实战Demo(主要对比NIO相较于BIO的优势)
要直观地对比 BIO 和 NIO 在“慢客户端”场景下的差异,最简单的方法不是真的去改网卡配置,而是写一个**“懒惰客户端”**。这个客户端连接上服务器后,故意休眠(Sleep)很长一段时间再发送数据。
这样,服务器端的处理逻辑差异就会被无限放大,从而清晰地看到 BIO 被“阻塞”的过程,以及 NIO 如何“绕过”阻塞去处理其他连接。
以下是一个完整的实战方案,包含代码示例和操作步骤。
🎯 场景设计
-
目标:模拟N个客户端连接服务器。
-
模拟“慢”:
- 服务端监听连接,拿到client套接字后,先取出【请求消息1】“Hello Server”,然后写入【响应消息】“Hello Client”,再等待读取【请求消息2】“Goodbye Server” 。
- 客户端连接后,先发送【请求消息1】“Hello Server” ,然后等待读取服务器【响应消息】“Hello Client”,等待 3 秒再发送【请求消息2】“Goodbye Server”,发送后调用close()方法关闭连接 )。
-
观察点:
Server端处理逻辑保持相同:读【请求消息1】->写【响应消息】->读【请求消息2】
- BIO Server:调用accept()接受Client1,顺序进行读写读,因client端【请求消息2】发送前有3秒阻塞,Server端页阻塞读,所以总耗时≈3秒+;Client1读写完成后,线程才循环再调用accept()接受Client2,处理Client2总耗时≈3*2秒+。
- NIO Server:调用selector.select(),Client1连接触发OP_ACCEPT事件,接受Client1并注册到selector中,继续监听下个事件,Client1发送的数据触发OP_READ事件,则读、写,因client端【请求消息2】发送前有3秒阻塞,没发过来数据不会触发OP_READ,Server端就停留在selelct()方法处可以处理其他事件,可以接受/读/写Client2、Client3…,所以Client的耗时不会对其他Client造成有效的影响。
🧪 准备工作:慢客户端 (SlowClient.java)
这个客户端是关键。它连接服务器后,需要能拖住Server的IO让其套接字的生命周期被Thread.sleep(3000)影响到。
package com.zadaya.io.test;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SlowClient {
// 定义请求并发量
public static final int CONCURRENT_COUNT = 2;
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(CONCURRENT_COUNT);
for (int i = 0; i < CONCURRENT_COUNT; i++) {
executor.submit(() -> {
try {
new SlowClient().start();
} catch (Exception e) {
e.printStackTrace();
}
});
}
executor.shutdown();
while (!executor.isTerminated()) {
Thread.sleep(10);
}
System.out.println("\n\n所有客户端全部处理完毕,总耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
public void start() {
String uuid = UUID.randomUUID().toString().replace("-", "");
try (
Socket socket = new Socket("127.0.0.1", 9001);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
) {
System.out.println("连接建立-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));
// 发送数据1
String request = "<html><body><h1>Hello Server!</h1><p>来自客户端的请求1</p><reqId>" + uuid + "</reqId></body></html>";
out.write(request.getBytes(StandardCharsets.UTF_8));
out.flush();
// 读取数据
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
System.out.println("接到数据-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "-" + new String(buffer, 0, read, StandardCharsets.UTF_8));
}
System.out.println("接收完成-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));
// 等待3000ms
Thread.sleep(3000);
// 发送数据2
String request2 = "<html><body><h1>Goodbye Server!</h1><p>来自客户端的请求2</p><reqId>" + uuid + "</reqId></body></html>";
out.write(request2.getBytes(StandardCharsets.UTF_8));
out.flush();
System.out.println("发送完成-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));
socket.close();
System.out.println("连接完成-" + uuid + "-" + java.time.LocalDateTime.now().toString().substring(0, 26));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
⚔️ 对比实验 A:BIO 服务器 (BIOServer.java)
这个服务器使用单线程处理所有连接(这是 BIO 最典型的瓶颈场景,当然也可适当调高线程比如10)。
package com.zadaya.io.test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
public class BIOServer {
// 定义监听的地址
private static final String ADDRESS = "127.0.0.1";
private static final int PORT = 9001;
public static void main(String[] args) {
new BIOServer().start(ADDRESS, PORT);
}
public void start(String address, int port) {
try {
// 创建固定大小线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
try (ServerSocket serverSocket = new ServerSocket()) {
serverSocket.bind(new InetSocketAddress(address, port)); // 绑定监听的 地址:端口
while (true) {
Socket socket = serverSocket.accept();
executor.submit(new BIOServerHandler(socket));
// new BIOServerHandler(socket).run();
System.out.println();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class BIOServerHandler implements Runnable {
private Socket socket;
public BIOServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
String oriReqId = null;
try (
OutputStream out = this.socket.getOutputStream();
InputStream in = this.socket.getInputStream();
) {
// 1. 先完整读取客户端请求
StringBuilder requestStringBuilder = new StringBuilder();
byte[] buffer = new byte[1024];
int bytesRead;
// 设置超时避免无限等待
socket.setSoTimeout(0);
// 读取1次请求数据
while ((bytesRead = in.read(buffer)) != -1) {
requestStringBuilder.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
// 检查是否读取完整(简单判断是否有</html>)
if (requestStringBuilder.toString().contains("</html>")) {
break;
}
}
String request = requestStringBuilder.toString();
oriReqId = request.substring(request.indexOf("<reqId>") + 7, request.indexOf("</reqId>"));
requestStringBuilder.setLength(0); // requestStringBuilder清空
System.out.println("接收到【请求消息1】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request);
// 组装发送响应消息
// 2. 构造正确的HTTP响应
String response = "<html><body><h1>Hello Client!</h1><p>来自服务器的响应</p><reqId>" + oriReqId + "</reqId></body></html>";
out.write(response.getBytes(StandardCharsets.UTF_8));
out.flush();
socket.shutdownOutput(); // 关闭输出流让客户端不再阻塞等待读取
System.out.println("已发送【响应消息S】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + response);
// 读取2次请求数据
while ((bytesRead = in.read(buffer)) != -1) {
requestStringBuilder.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));
}
String request2 = requestStringBuilder.toString();
System.out.println("接收到【请求消息2】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request2);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
预期输出现象:
- 启动 Server(单线程)。
- 使用线程池同时启动 2个Client:Server 控制台在打印
接收到【请求消息1】,已发送【响应消息S】,会卡住 3秒。 - 3 秒后,Server 打印
接收到【请求消息2】。 - 此时Server 现在才开始处理 Client2 的 accept,控制台在打印
接收到【请求消息1】,已发送【响应消息S】,依旧会卡住 3秒。 - 总耗时极长,Client2 最后打印
接收到【请求消息2】时距离启动Client已过去 6秒。
⚔️ 对比实验 B:NIO 服务器 (NIOServer.java)
这个服务器使用单线程 + Selector,可以同时管理多个连接。
package com.zadaya.io.test;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
// 定义监听的地址
private static final String ADDRESS = "127.0.0.1";
private static final int PORT = 9001;
public static void main(String[] args) {
new NIOServer().start(ADDRESS, PORT);
}
public void start(String address, int port) {
try (
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
) {
serverSocketChannel.bind(new InetSocketAddress(address, port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int selected = selector.select();
if (selected <= 0) {
// 0 个就绪的通道
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isValid()) {
// 继续处理
}
if (key.isAcceptable()) {
handleAccept(key, selector);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
} else {
// 读完写完可以删除
key.cancel();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void handleAccept(SelectionKey key, Selector selector) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
try {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
System.err.println("接受连接异常");
key.cancel();
e.printStackTrace();
}
}
private void handleRead(SelectionKey key) {
try {
SocketChannel socketChannel = (SocketChannel) key.channel();
// 创建缓冲区用于读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 读取数据的StringBuffer
StringBuilder requestStringBuilder = new StringBuilder();
// 开始读取数据
int bytesRead;
while (true) {
bytesRead = socketChannel.read(byteBuffer);
if (bytesRead <= 0) {
break;
}
byteBuffer.flip(); // 缓冲区转换为读模式
requestStringBuilder.append(new String(byteBuffer.array(), 0, bytesRead, StandardCharsets.UTF_8));
byteBuffer.clear(); // 空缓冲区转换为写模式
}
String request = requestStringBuilder.toString();
String oriReqId = (String) key.attachment();
if (oriReqId == null) {
// 记录首次请求的id
oriReqId = request.substring(request.indexOf("<reqId>") + 7, request.indexOf("</reqId>"));
key.attach(oriReqId);
System.out.println("接收到【请求消息1】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request);
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); // 增加写意向
} else {
System.out.println("接收到【请求消息2】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + request);
}
} catch (IOException e) {
key.cancel();
e.printStackTrace();
}
}
private void handleWrite(SelectionKey key) {
try {
SocketChannel socketChannel = (SocketChannel) key.channel();
// 组装发送响应消息
// 构造正确的HTTP响应
String oriReqId = (String) key.attachment();
String response = "<html><body><h1>Hello Client!</h1><p>来自服务器的响应</p><reqId>" + oriReqId + "</reqId></body></html>";
// 创建缓冲区用于写入数据 (直接用response数据创建)
ByteBuffer byteBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));
// 开始写入数据
// 判断buffer中数据是否全部写完
while (byteBuffer.hasRemaining()) {
int written = socketChannel.write(byteBuffer);// 写入数据
if (written <= 0) {
throw new RuntimeException("没有数据写入");
}
}
System.out.println("已发送【响应消息S】-reqId= " + oriReqId + "-" + java.time.LocalDateTime.now().toString().substring(0, 26) + "\n" + response);
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); // 取消写意向
socketChannel.shutdownOutput(); // 关闭输出流让客户端不再阻塞等待读取
} catch (IOException e) {
key.cancel();
e.printStackTrace();
}
}
}
预期输出现象:
- 启动 Server。
- 使用线程池同时启动 2个Client:Server 控制台在打印
接收到【请求消息1】,已发送【响应消息S】,接收到【请求消息1】,已发送【响应消息S】,会卡住 3秒。 - Server线程不会卡在IO操作处,而在select()方法处等待事件通知。
- 3 秒后,Server 被OP_READ事件唤醒,控制台打印
接收到【请求消息2】,接收到【请求消息2】。 - 两个Client线程关闭连接,全部结束,此时距离启动Client大约也就1个 3秒。
谨慎使用!!!
☣️生产实际应用,不建议使用JDK的NIO包手搓WEB服务,可使用成熟的Netty框架进行二次开发,原因如下:
1. 为什么推荐使用 Netty 而非直接使用 JDK NIO?
① 开发效率和复杂性
- JDK NIO 虽然提供了底层能力,但直接使用它构建高性能 Web 服务需要处理大量细节:
- 线程模型设计(如 Reactor 模式实现)
- 缓冲区管理(ByteBuffer 的分配、释放、扩容)
- 粘包/半包处理
- 连接生命周期管理
- 异常处理和断线重连
- 零拷贝、内存池等优化
- Netty 将这些通用问题抽象为组件(EventLoop、ChannelPipeline、ByteBuf、Codec 等),开发者只需关注业务逻辑,大幅提高开发效率。
② 稳定性和性能
- Netty 经过全球无数生产项目验证,修复了大量 NIO 本身的陷阱(如 Selector 空轮询 Bug、Epoll Bug 等)。
- 它提供了高性能的线程模型(主从 Reactor)、内存池(PooledByteBuf)、零拷贝(FileRegion)、高效的协议编解码,性能通常优于手写代码。
- 内置多种传输实现(NIO、Epoll、KQueue、IOCP),能自动选择最佳方式。
③ 功能丰富
- 支持 HTTP、WebSocket、SSL/TLS、HTTP/2 等数十种协议编解码器,可直接复用。
- 提供流量整形、超时控制、心跳机制、IP 过滤等高级功能。
- 良好的扩展性,通过 ChannelHandler 链式处理,易于定制。
④ 维护成本
- 手写服务需要自己持续修复 Bug、适配新 JDK 版本、处理安全漏洞,而 Netty 由社区维护,升级方便。
- 团队成员熟悉 Netty 后,开发维护成本远低于手写底层 NIO。
2. 是否绝对不能用 JDK NIO 手写?
在极少数场景下,直接使用 JDK NIO 也可能合理:
- 极端定制化需求:如果对协议栈或性能有特殊要求,且无法通过 Netty 扩展实现,可能需要自己从底层做起。
- 资源受限环境:如某些嵌入式系统,Netty 依赖包较大,可能不适合。
- 学习研究目的:为了深入理解 NIO 和 Reactor 模型,手写 Demo 很有价值。
- 极简服务:如果只是一个非常简单的 UDP 接收器,可能几十行 NIO 代码就能搞定,引入 Netty 显得“重”。
但即使在这些场景,通常也可以基于 Netty 裁剪(如使用核心模块),或借鉴其设计。
3. 结论
对于绝大多数生产级 Web 服务,强烈建议使用 Netty(或其他成熟框架如 Vert.x、Akka HTTP)进行开发,而不是从零手写 JDK NIO。 这可以避免重复造轮子,减少 Bug,提高性能和可维护性。不过,如果项目规模极小、需求特殊或仅为学习,直接使用 NIO 也未尝不可。
更多推荐

所有评论(0)