文章目录
一、核心思想: 为什么需要NIO?
传统的 Java I/O(java.io,又称 BIO - Blocking I/O)是基于流和阻塞的。
- 流:单向的,要么是输入流(
InputStream),要么是输出流(OutputStream),不能同时读写。 - 阻塞:当一个线程调用
read()或write()时,该线程会被阻塞,直到数据被读取到或写入完成。在此期间,线程什么也做不了。
BIO 模型在处理大量连接时非常低效。通常采用“一个连接,一个线程”的方案。当连接数暴涨时,线程数量也随之暴涨,导致巨大的上下文切换开销,最终耗尽系统资源。
NIO (New I/O / Non-blocking I/O) 的出现就是为了解决这些问题:
- 非阻塞 I/O:线程可以从通道请求读取数据,但如果尚无数据可用,线程可以立即去做别的事情,而不是被阻塞。
- 面向缓冲区:数据被读入或写出一个缓冲区,你可以根据需要前后移动缓冲区,这提供了更大的灵活性。
- 多路复用器:使用单个(或少量)线程来管理多个通道(连接),这是高性能的关键。
🎯 目标:用少量线程处理成千上万个连接。
NIO vs BIO
| 场景 | BIO(阻塞IO) | NIO(非阻塞IO) |
|---|---|---|
| 10个连接 | 10个线程 | 1个线程即可 |
| 1000个连接 | 1000个线程 → 崩溃 | 1~4个线程可支撑 |
| CPU开销 | 高(上下文切换) | 低 |
| 编程复杂度 | 简单 | 较复杂(状态管理) |
二、NIO三大核心组件
Channel、Buffer、Selector
| 组件 | 说明 |
|---|---|
| Buffer(缓冲区) | 数据的容器,所有数据都通过 Buffer 读写 |
| Channel(通道) | 类似流,但可双向读写,支持非阻塞模式 |
| Selector(选择器) | 实现 I/O 多路复用,监听多个 Channel 的事件 |
2.1 Channel【通道】
public interface Channel extends Closeable {
// channel是否已经打开
public boolean isOpen();
// 关闭Channel
public void close() throws IOException;
}
类似于传统的“流”,但是双向的,既可以读,也可以写。它是连接缓冲区和数据源(如文件、套接字)的桥梁。
- 主要实现:
FileChannel:用于文件 IO。SocketChannel&ServerSocketChannel:用于 TCP 网络 IO。DatagramChannel:用于 UDP 网络 IO。

channel vs stream
| 对比项 | Stream | Channel |
|---|---|---|
| 方向 | 单向 | 双向 |
| 阻塞 | 总是阻塞 | 可设为非阻塞 |
| 传输方式 | 逐字节 | 与 Buffer 配合批量传输 |
2.2 Buffer【缓冲区】
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
// ...
}

一个容器对象,所有数据的读写都直接通过缓冲区进行。它本质上是一个内存块数组,并提供了一组方法以便更轻松地使用该内存。
-
核心属性:
capacity:容量,缓冲区最大大小,创建后不可变。position:位置,下一个要读取或写入的元素的索引。limit:界限,缓冲区中第一个不应读取或写入的元素的索引。mark:标记,一个备忘位置,通过mark()标记,可通过reset()恢复到mark的位置。- 关系:0 <= mark <= position <= limit <= capacity
-
主要实现:
ByteBuffer,CharBuffer,IntBuffer等,最常用的是ByteBuffer。 -
核心操作:
allocate(int capacity):分配一块新的缓冲区。put()/get():写入/读取数据。flip():将缓冲区从写模式切换到读模式。limit = position; position = 0;clear():清空缓冲区,准备再次写入。position = 0; limit = capacity;rewind():重读缓冲区。position = 0;
2.3 Selector【选择器】
NIO 的灵魂。它是一个多路复用器,可以监控多个通道的 IO 状态(例如:连接就绪、读就绪、写就绪)。一个单线程的 Selector 可以管理成千上万的通道。
- SelectionKey:表示一个通道在选择器上的注册 token。它包含:
- 兴趣集合 (Interest Set):你关心通道的什么事件
- OP_ACCEPT
- OP_CONNECT
- OP_READ
- OP_WRITE
- 就绪集合 (Ready Set):通道已就绪的操作集合。
- 通道 (Channel) 和 选择器 (Selector) 的引用。
- 附件 (Attachment):可附加一个对象(如一个
Buffer或会话对象),是强大的扩展机制。
- 兴趣集合 (Interest Set):你关心通道的什么事件
工作流程图

2.4 工作原理流程图
三、代码示例【NIO echo 服务器】
3.1 示例代码
下面是一个完整的 NIO Echo 服务器实现,它接收客户端的消息并原样返回。
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.util.Iterator;
import java.util.Set;
public class NioEchoServer {
public static void main(String[] args) throws IOException {
// 1. 创建选择器 (The Multiplexer)
Selector selector = Selector.open();
// 2. 创建服务器套接字通道并设置为非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 必须设置为非阻塞
serverSocketChannel.bind(new InetSocketAddress(9090)); // 绑定端口
// 3. 将通道注册到选择器,并指定感兴趣的事件为 ACCEPT(接受连接)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Echo Server started on port 9090...");
// 主事件循环
while (true) {
// 4. 阻塞等待就绪的通道。参数可设置超时时间。
selector.select();
// 5. 获取就绪的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 必须先移除,防止下次重复处理
keyIterator.remove();
try {
if (key.isAcceptable()) {
// 6. 处理 ACCEPT 事件:新的客户端连接
handleAccept(key, selector);
} else if (key.isReadable()) {
// 7. 处理 READ 事件:客户端发送了数据
handleRead(key);
}
// 可以处理 isWritable(),但通常只在需要写入大量数据时才注册
} catch (IOException e) {
// 客户端断开连接等异常
System.err.println("Error handling client: " + e.getMessage());
key.cancel(); // 取消这个键的注册
if (key.channel() != null) {
key.channel().close(); // 关闭通道
}
}
}
}
}
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
// 获取注册的 ServerSocketChannel
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受连接,获取客户端的 SocketChannel
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false); // 设置为非阻塞模式
// 欢迎信息
String welcomeMsg = "Welcome to NIO Echo Server. Type 'exit' to quit.\n";
ByteBuffer buffer = ByteBuffer.wrap(welcomeMsg.getBytes());
clientChannel.write(buffer); // 发送欢迎信息
// 将新客户端通道注册到选择器,对 READ 事件感兴趣
// 并为每个通道附加一个专属的 Buffer
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(256));
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
}
private static void handleRead(SelectionKey key) throws IOException {
// 获取客户端通道和附加的 Buffer
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 清空Buffer,准备读取数据
buffer.clear();
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭了连接
System.out.println("Client disconnected: " + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
return;
}
// 切换Buffer为读模式
buffer.flip();
// 将接收到的数据转换为字符串
String received = new String(buffer.array(), 0, buffer.limit()).trim();
System.out.println("Received from " + clientChannel.getRemoteAddress() + ": " + received);
if ("exit".equalsIgnoreCase(received)) {
// 如果客户端发送 "exit",则关闭连接
clientChannel.write(ByteBuffer.wrap("Goodbye!\n".getBytes()));
System.out.println("Client exited: " + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
} else {
// Echo 功能:将收到的数据原样发回
// 注意:write()可能不会一次性写完,需要注册OP_WRITE事件来持续写,
// 但对于Echo这种小数据量场景,通常一次write就能完成,这里做了简化。
buffer.rewind(); // 将position重置为0,重新读一遍Buffer
clientChannel.write(buffer);
}
}
}
3.2 客户端测试
客户端测试
你可以使用 telnet 或 netcat 命令来测试这个服务器:
telnet localhost 9090
# 或
nc localhost 9090
服务器端

客户端

四、NIO优缺点与实践经验
4.1 优点
- 高性能和高伸缩性:单线程即可管理大量连接,避免了线程创建和上下文切换的巨大开销。这是 NIO 最大的优势。
- 更少的资源消耗:与为每个连接创建一个线程相比,线程资源消耗要少得多。
4.2 缺点
- API 复杂:相比于 BIO,NIO 的 API 调用复杂得多,开发和调试难度大。
- 可靠性编程困难:需要处理许多边缘情况,如
write不一定能一次写完(需要注册OP_WRITE并循环写),TCP 粘包/拆包问题(需要在应用层设计协议,如定长消息、分隔符、长度字段等)。 - Bug 与陷阱:如
SelectionKey必须手动从集合中移除,否则会重复处理;对ByteBuffer的状态(position,limit)管理不当会导致错误。
4.3 实践经验总结
- 不要阻塞 Selector 线程:
Selector.select()所在的线程是核心,其中的所有操作(如handleRead)都必须快速非阻塞。任何耗时的操作(如数据库查询、复杂计算)都应该交给专门的业务线程池去处理,以免影响其他连接的响应。 - 管理好 Buffer:
- 考虑使用
Buffer池来避免频繁的创建和销毁。 - 深刻理解
flip(),clear(),rewind()的含义。
- 考虑使用
- 处理写操作:
channel.write(buffer)的返回值表示写入的字节数,它可能无法一次性写完缓冲区中的所有数据。如果需要写入大量数据,应在未写完时注册OP_WRITE事件,在isWritable()时继续写,写完后再取消注册,以避免 Selector 被不必要的写事件占满。 - 使用 Netty 等框架:99% 的场景下,你不应该直接使用原生 NIO API 来编写网络应用。 框架如 Netty 和 Mina 完美地封装了 NIO 的复杂性,解决了上述所有缺点和陷阱(如粘包拆包、API复杂、线程模型),提供了强大、易用且高性能的抽象。学习 NIO 是为了更好地理解这些框架的原理。
2745

被折叠的 条评论
为什么被折叠?



