NIO 核心概念讲解
一、NIO 是什么?
NIO = Non-blocking IO(非阻塞式输入输出)
与 BIO 的核心区别:
BIO(阻塞式):
┌──────────────┐ ┌──────────────┐
│ Server │ │ Client 1 │
│ │◀───Socket 1────────│ │
│ 线程1专门 │ └──────────────┘
│ 处理Client1 │
│ │ ┌──────────────┐
│ 线程2专门 │◀───Socket 2────────│ Client 2 │
│ 处理Client2 │ └──────────────┘
│ │
│ 线程3专门 │ ┌──────────────┐
│ 处理Client3 │◀───Socket 3────────│ Client 3 │
└──────────────┘ └──────────────┘
问题:线程多,资源浪费
NIO(非阻塞式):
┌──────────────┐ ┌──────────────┐
│ Server │ │ Client 1 │
│ │◀───Channel 1───────│ │
│ │ └──────────────┘
│ Selector │
│ (监控器) │ ┌──────────────┐
│ │◀───Channel 2───────│ Client 2 │
│ 一个线程 │ └──────────────┘
│ 管理所有 │
│ │ ┌──────────────┐
│ │◀───Channel 3───────│ Client 3 │
└──────────────┘ └──────────────┘
优势:一个线程管理所有客户端
NIO 的核心特点:
- 非阻塞:调用方法立即返回,不会"卡住"
- 事件驱动:有事件才处理,没事件就等待
- 一个线程管理多个连接:用 Selector 监控
二、NIO 的四大核心组件
1. Selector(选择器)
作用: 监控多个 Channel 的事件
比喻: 像监控摄像头,同时监视多个通道
// 创建选择器
Selector selector = Selector.open();
// 等待事件(阻塞,但可以监控多个通道)
int count = selector.select();
// 获取就绪的事件
Set<SelectionKey> keys = selector.selectedKeys();
为什么需要 Selector?
- BIO:一个线程只能处理一个客户端
- NIO:一个线程通过 Selector 可以处理多个客户端
2. Channel(通道)
作用: 数据传输的管道
类型:
ServerSocketChannel:服务端通道,接受连接SocketChannel:客户端通道,读写数据
与 BIO 的 Socket 对比:
| BIO | NIO |
|---|---|
| ServerSocket | ServerSocketChannel |
| Socket | SocketChannel |
| 阻塞 | 非阻塞 |
// 创建服务端通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置非阻塞(重要!)
serverChannel.configureBlocking(false);
// 绑定端口
serverChannel.bind(new InetSocketAddress(7397));
// 注册到选择器
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
3. ByteBuffer(字节缓冲区)
作用: 读写数据的容器
为什么需要 ByteBuffer?
- NIO 不能直接读写数据,必须通过 ByteBuffer
- ByteBuffer 提供了灵活的读写操作
ByteBuffer 的两种模式:
写模式(初始状态):
position: 0 → 5
capacity: 10
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│H│e│l│l│o│ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
↑
position
调用 flip() 后(切换到读模式):
position: 0
limit: 5
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│H│e│l│l│o│ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
↑ ↑
position limit
核心方法:
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 写入数据
buffer.put(bytes);
// 切换到读模式(重要!)
buffer.flip();
// 读取数据
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
// 清空缓冲区
buffer.clear();
// 快捷方式:直接包装数据
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
4. SelectionKey(选择键)
作用: 代表 Channel 在 Selector 上的注册信息
包含信息:
- 哪个 Channel
- 关注什么事件
- 附加数据(可选)
事件类型:
| 事件 | 值 | 说明 | 使用场景 |
|---|---|---|---|
| OP_ACCEPT | 16 | 接受连接 | 服务端 |
| OP_CONNECT | 8 | 连接完成 | 客户端 |
| OP_READ | 1 | 可读 | 客户端和服务端 |
| OP_WRITE | 4 | 可写 | 客户端和服务端 |
// 注册通道到选择器
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
// 判断事件类型
if (key.isAcceptable()) { } // 接受连接
if (key.isConnectable()) { } // 连接完成
if (key.isReadable()) { } // 可读
if (key.isWritable()) { } // 可写
// 获取通道
SocketChannel channel = (SocketChannel) key.channel();
// 取消注册
key.cancel();
三、SimpleNioServer 详解
类的作用:
使用 NIO 实现的服务端,一个线程处理多个客户端。
核心流程图:
SimpleNioServer 启动流程:
1. 创建 Selector
↓
2. 创建 ServerSocketChannel
↓
3. 设置非阻塞模式
↓
4. 绑定端口 7397
↓
5. 注册到 Selector,监听 OP_ACCEPT
↓
6. 进入事件循环 while(true)
↓
7. selector.select() 等待事件
↓
8. 有事件发生,获取 selectedKeys
↓
9. 遍历处理每个事件:
├─ OP_ACCEPT:接受连接
│ ├─ serverChannel.accept() 获取客户端通道
│ ├─ 设置客户端通道为非阻塞
│ ├─ 注册客户端通道到 Selector,监听 OP_READ
│ └─ 发送欢迎消息
│
└─ OP_READ:读取数据
├─ 创建 ByteBuffer
├─ channel.read(buffer) 读取数据
├─ buffer.flip() 切换到读模式
├─ 解析消息
└─ 回复客户端
↓
10. 回到步骤 7,继续监听
核心代码解析:
public static void main(String[] args) throws IOException {
// ========== 步骤1-5:初始化 ==========
// 1. 创建选择器(监控中心)
Selector selector = Selector.open();
// 2. 创建服务端通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 3. 设置为非阻塞模式(NIO 的核心!)
// 非阻塞:调用方法立即返回,不会"卡住"
serverChannel.configureBlocking(false);
// 4. 绑定端口
serverChannel.bind(new InetSocketAddress(7397));
// 5. 把服务端通道注册到选择器
// 监听 OP_ACCEPT 事件(接受连接)
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务端启动,监听端口 7397...");
// ========== 步骤6-10:事件循环 ==========
while (true) {
// 6. 等待事件发生(阻塞)
// 虽然这里会阻塞,但可以同时监控多个通道
// 返回值:有多少个通道就绪
int readyChannels = selector.select();
if (readyChannels == 0) {
continue; // 没有事件,继续等待
}
// 7. 获取所有就绪的事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
// 8. 遍历处理每个事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 重要!处理完要移除,否则会重复处理
iterator.remove();
// 9. 根据事件类型处理
if (key.isAcceptable()) {
// 有客户端连接
handleAccept(key, selector);
} else if (key.isReadable()) {
// 有数据可读
handleRead(key);
}
}
}
}
处理连接事件:
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
// 1. 获取服务端通道
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 2. 接受客户端连接
// 返回客户端通道(SocketChannel)
SocketChannel clientChannel = serverChannel.accept();
System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
// 3. 设置客户端通道为非阻塞
clientChannel.configureBlocking(false);
// 4. 把客户端通道注册到选择器
// 监听 OP_READ 事件(可读)
// 这样就能接收客户端发来的消息了
clientChannel.register(selector, SelectionKey.OP_READ);
// 5. 发送欢迎消息
String welcome = "欢迎连接到 NIO 服务器!\n";
ByteBuffer buffer = ByteBuffer.wrap(welcome.getBytes("UTF-8"));
clientChannel.write(buffer);
}
处理读取事件:
private static void handleRead(SelectionKey key) throws IOException {
// 1. 获取客户端通道
SocketChannel clientChannel = (SocketChannel) key.channel();
// 2. 创建缓冲区(1024 字节)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 从通道读取数据到缓冲区
// 返回值:读取的字节数
// - 大于 0:读到数据
// - 等于 0:没数据
// - 等于 -1:连接断开
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端断开连接
System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
clientChannel.close();
return;
}
if (bytesRead > 0) {
// 4. 切换缓冲区为读模式(重要!)
// 写完数据后,position 在末尾
// flip() 把 position 设为 0,limit 设为之前的 position
// 这样就能从头读取数据了
buffer.flip();
// 5. 读取数据
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes, "UTF-8").trim();
System.out.println("收到消息:" + message);
// 6. 回复客户端
String response = "服务端已收到:" + message + "\n";
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes("UTF-8"));
clientChannel.write(responseBuffer);
// 7. 如果收到 bye,关闭连接
if ("bye".equalsIgnoreCase(message)) {
System.out.println("客户端主动断开:" + clientChannel.getRemoteAddress());
clientChannel.close();
}
}
}
四、SimpleNioClient 详解
类的作用:
使用 NIO 实现的客户端,连接服务端并收发消息。
核心流程图:
SimpleNioClient 启动流程:
1. 创建 Selector
↓
2. 创建 SocketChannel
↓
3. 设置非阻塞模式
↓
4. 连接服务端(非阻塞,可能未完成)
↓
5. 注册到 Selector:
├─ 已连接:监听 OP_READ
└─ 未连接:监听 OP_CONNECT
↓
6. 启动用户输入线程(发送消息)
↓
7. 进入事件循环 while(true)
↓
8. selector.select() 等待事件
↓
9. 遍历处理每个事件:
├─ OP_CONNECT:连接完成
│ ├─ finishConnect() 完成连接
│ └─ 改为监听 OP_READ
│
└─ OP_READ:读取数据
├─ 创建 ByteBuffer
├─ channel.read(buffer) 读取数据
├─ buffer.flip() 切换到读模式
└─ 打印服务端消息
↓
10. 回到步骤 8,继续监听
核心代码解析:
public static void main(String[] args) throws IOException {
// ========== 步骤1-5:初始化 ==========
// 1. 创建选择器
Selector selector = Selector.open();
// 2. 创建客户端通道
SocketChannel clientChannel = SocketChannel.open();
// 3. 设置为非阻塞模式
clientChannel.configureBlocking(false);
// 4. 发起连接(非阻塞)
// 非阻塞模式下,connect() 可能立即返回 false
// 表示连接还在进行中
boolean connected = clientChannel.connect(
new InetSocketAddress("127.0.0.1", 7397)
);
// 5. 注册到选择器
if (connected) {
// 已连接,监听"可读"事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("已连接到服务端");
} else {
// 未连接,监听"连接完成"事件
clientChannel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("正在连接服务端...");
}
// ========== 步骤6:启动用户输入线程 ==========
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入消息(输入 bye 退出):");
while (scanner.hasNextLine()) {
String input = scanner.nextLine();
try {
// 发送消息(加换行符)
ByteBuffer buffer = ByteBuffer.wrap(
(input + "\n").getBytes("UTF-8")
);
clientChannel.write(buffer);
if ("bye".equalsIgnoreCase(input)) {
Thread.sleep(1000); // 等待服务端回复
System.exit(0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// ========== 步骤7-10:事件循环 ==========
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.isConnectable()) {
// 连接完成
handleConnect(key, selector);
} else if (key.isReadable()) {
// 有数据可读
handleRead(key);
}
}
}
}
处理连接完成事件:
private static void handleConnect(SelectionKey key, Selector selector) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 完成连接
// 为什么需要这一步?
// 因为非阻塞模式下,connect() 可能立即返回
// 需要调用 finishConnect() 来确认连接真正完成
if (clientChannel.finishConnect()) {
System.out.println("连接成功!");
// 改为监听"可读"事件
clientChannel.register(selector, SelectionKey.OP_READ);
}
}
处理读取事件:
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 服务端断开
System.out.println("服务端断开连接");
clientChannel.close();
System.exit(0);
}
if (bytesRead > 0) {
// 切换为读模式
buffer.flip();
// 读取数据
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes, "UTF-8").trim();
System.out.println("服务端回复:" + message);
}
}
五、NIO vs BIO 对比
代码对比:
BIO 服务端:
// BIO:为每个客户端创建新线程
Thread clientThread = new Thread(() -> handleClient(clientSocket, clientId));
clientThread.setName("BIO-Client-" + clientId);
clientThread.start(); // 启动新线程
逻辑:来一个连接 → 创建一个线程 → 线程专门服务这个连接
NIO 服务端:
// NIO:根本不创建新线程!
// 只是把客户端通道注册到Selector
clientChannel.register(selector, SelectionKey.OP_READ, clientId);
// 然后继续循环,等待其他事件...
逻辑:来一个连接 → 注册到Selector → 继续等待事件
六、NIO代码示例
/**
* 最简单的 NIO 服务端
*
* NIO 核心:一个线程管理多个客户端
* 关键组件:Selector(选择器)、Channel(通道)、ByteBuffer(缓冲区)
*/
public class SimpleNioServer {
public static void main(String[] args) throws IOException {
// 1. 创建选择器(监控中心)
Selector selector = Selector.open();
// 2. 创建服务端通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 3. 设置为非阻塞模式(NIO 的核心!)
serverChannel.configureBlocking(false);
// 4. 绑定端口
serverChannel.bind(new InetSocketAddress(7397));
// 5. 把服务端通道注册到选择器,监听"接受连接"事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务端启动,监听端口 7397...");
// 6. 事件循环(核心)
while (true) {
// 等待事件发生(阻塞,但可以同时监控多个通道)
// 返回值:有多少个通道就绪
int readyChannels = selector.select();
if (readyChannels == 0) {
continue; // 没有事件,继续等待
}
// 7. 获取所有就绪的事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
// 8. 遍历处理每个事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 处理完要移除,否则会重复处理
// 9. 根据事件类型处理
if (key.isAcceptable()) {
// 有客户端连接
handleAccept(key, selector);
} else if (key.isReadable()) {
// 有数据可读
handleRead(key);
}
}
}
}
/**
* 处理客户端连接事件
*/
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
// 1. 获取服务端通道
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 2. 接受客户端连接,返回客户端通道
SocketChannel clientChannel = serverChannel.accept();
System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
// 3. 设置客户端通道为非阻塞
clientChannel.configureBlocking(false);
// 4. 把客户端通道注册到选择器,监听"可读"事件
clientChannel.register(selector, SelectionKey.OP_READ);
// 5. 发送欢迎消息
String welcome = "欢迎连接到 NIO 服务器!\n";
ByteBuffer buffer = ByteBuffer.wrap(welcome.getBytes("UTF-8"));
clientChannel.write(buffer);
}
/**
* 处理读取数据事件
*/
private static void handleRead(SelectionKey key) throws IOException {
// 1. 获取客户端通道
SocketChannel clientChannel = (SocketChannel) key.channel();
// 2. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 从通道读取数据到缓冲区
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端断开连接
System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
clientChannel.close();
return;
}
if (bytesRead > 0) {
// 4. 切换缓冲区为读模式
buffer.flip();
// 5. 读取数据
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes, "UTF-8").trim();
System.out.println("收到消息:" + message);
// 6. 回复客户端
String response = "服务端已收到:" + message + "\n";
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes("UTF-8"));
clientChannel.write(responseBuffer);
// 7. 如果收到 bye,关闭连接
if ("bye".equalsIgnoreCase(message)) {
System.out.println("客户端主动断开:" + clientChannel.getRemoteAddress());
clientChannel.close();
}
}
}
}
/**
* 最简单的 NIO 客户端
*
* 功能:连接服务端,发送消息,接收回复
*/
public class SimpleNioClient {
public static void main(String[] args) throws IOException {
// 1. 创建选择器
Selector selector = Selector.open();
// 2. 创建客户端通道
SocketChannel clientChannel = SocketChannel.open();
// 3. 设置为非阻塞模式
clientChannel.configureBlocking(false);
// 4. 发起连接(非阻塞,可能立即返回 false)
boolean connected = clientChannel.connect(new InetSocketAddress("127.0.0.1", 7397));
// 5. 注册到选择器
if (connected) {
// 已连接,监听"可读"事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("已连接到服务端");
} else {
// 未连接,监听"连接完成"事件
clientChannel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("正在连接服务端...");
}
// 6. 启动一个线程处理用户输入
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入消息(输入 bye 退出):");
while (scanner.hasNextLine()) {
String input = scanner.nextLine();
try {
// 发送消息
ByteBuffer buffer = ByteBuffer.wrap((input + "\n").getBytes("UTF-8"));
clientChannel.write(buffer);
if ("bye".equalsIgnoreCase(input)) {
Thread.sleep(1000); // 等待服务端回复
System.exit(0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// 7. 事件循环
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.isConnectable()) {
// 连接完成
handleConnect(key, selector);
} else if (key.isReadable()) {
// 有数据可读
handleRead(key);
}
}
}
}
/**
* 处理连接完成事件
*/
private static void handleConnect(SelectionKey key, Selector selector) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 完成连接
if (clientChannel.finishConnect()) {
System.out.println("连接成功!");
// 改为监听"可读"事件
clientChannel.register(selector, SelectionKey.OP_READ);
}
}
/**
* 处理读取数据事件
*/
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 服务端断开
System.out.println("服务端断开连接");
clientChannel.close();
System.exit(0);
}
if (bytesRead > 0) {
// 切换为读模式
buffer.flip();
// 读取数据
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes, "UTF-8").trim();
System.out.println("服务端回复:" + message);
}
}
}
1311



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



