一、理论说明
1. NIO 的定义
Java NIO 是从 Java 1.4 版本开始引入的一套新的 I/O API,它提供了与标准 I/O 不同的工作方式。NIO 以块(Buffer)为基本处理单位,采用非阻塞(Non-blocking)模式,并引入了选择器(Selector)机制,使得单线程可以同时处理多个 I/O 通道,大幅提高了 I/O 效率,尤其适用于高并发场景。
2. NIO 与传统 I/O 的区别
特性 | 传统 I/O(java.io) | NIO(java.nio) |
---|---|---|
处理方式 | 基于流(Stream),单向传输 | 基于缓冲区(Buffer),双向传输 |
阻塞模式 | 阻塞 I/O(BIO) | 非阻塞 I/O(NIO) |
通道 | 无 | 有(Channel) |
选择器 | 无 | 有(Selector),支持多路复用 |
面向对象 | 面向字节流或字符流 | 面向缓冲区和通道 |
适用场景 | 连接数目少且固定的架构 | 连接数目多且短的架构(如聊天服务器) |
二、核心组件
1. 缓冲区(Buffer)
缓冲区是一个用于存储特定基本类型数据的容器,所有数据都通过缓冲区处理。主要缓冲区类型有:ByteBuffer
、CharBuffer
、IntBuffer
等,最常用的是ByteBuffer
。
import java.nio.ByteBuffer;
public class BufferExample {
public static void main(String[] args) {
// 创建容量为10的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
// 写入数据
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
// 切换为读模式
buffer.flip();
// 读取数据
while(buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 输出: Hello
}
}
缓冲区的三个核心属性:
- capacity:缓冲区的最大容量
- position:当前读写位置
- limit:读写的上限(写模式下等于 capacity,读模式下等于实际写入的数据量)
2. 通道(Channel)
通道是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。通道与流的不同之处在于通道是双向的,而流是单向的(只能读或写)。主要通道类型有:
FileChannel
:用于文件读写SocketChannel
:用于 TCP 客户端ServerSocketChannel
:用于 TCP 服务器DatagramChannel
:用于 UDP 通信
3. 选择器(Selector)
选择器是 NIO 的核心,它允许单线程处理多个通道。通过注册通道到选择器并监听各种事件(如连接、接受、读、写),实现了单线程管理多个客户端连接的能力。
import java.nio.channels.*;
import java.io.IOException;
public class SelectorExample {
public static void main(String[] args) throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 创建通道并注册到选择器
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new java.net.InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询选择器
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) continue;
// 处理就绪的通道
java.util.Set<SelectionKey> selectedKeys = selector.selectedKeys();
java.util.Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读事件
}
keyIterator.remove();
}
}
}
}
三、文件操作示例
以下是使用 NIO 进行文件复制的示例:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class FileCopyExample {
public static void main(String[] args) {
try (FileInputStream fin = new FileInputStream("source.txt");
FileOutputStream fout = new FileOutputStream("target.txt");
FileChannel inChannel = fin.getChannel();
FileChannel outChannel = fout.getChannel()) {
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到缓冲区,然后写入目标通道
while (inChannel.read(buffer) != -1) {
buffer.flip(); // 切换为读模式
outChannel.write(buffer);
buffer.clear(); // 清空缓冲区,准备下一次读取
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、网络编程示例(非阻塞服务器)
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 NioServer {
private static final int PORT = 8080;
private Selector selector;
private ServerSocketChannel serverChannel;
public NioServer() {
try {
// 初始化选择器和服务器通道
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(PORT));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动,监听端口: " + PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
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.isAcceptable()) {
// 处理新连接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = ssc.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
} else if (key.isReadable()) {
// 处理读事件
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data, "UTF-8");
System.out.println("收到消息: " + message);
// 回显消息给客户端
ByteBuffer response = ByteBuffer.wrap(("服务器已收到: " + message).getBytes());
clientChannel.write(response);
} else if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
clientChannel.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new NioServer().start();
}
}
五、面试题
题目:
答案:
六、自我总结
Java NIO 通过引入缓冲区、通道和选择器,提供了一种高效的 I/O 处理方式,尤其适合高并发场景。相比传统 I/O,NIO 的主要优势在于:
- 非阻塞 I/O 允许单线程处理多个连接,减少线程开销
- 选择器机制实现了高效的事件驱动模型
- 缓冲区操作简化了数据处理流程
- 通道支持双向数据传输
然而,NIO 的编程模型相对复杂,需要理解缓冲区状态转换、事件轮询等概念。在实际应用中,应根据具体场景选择合适的 I/O 方式:
- 对于连接数少且稳定的场景,传统 I/O 可能更简单
- 对于高并发、短连接的场景,NIO 是更好的选择
- 对于超高性能需求,可以考虑使用 NIO 2.0(AIO,异步 I/O)
NIO 是 Java 网络编程的核心技术之一,广泛应用于各种高性能服务器、中间件和分布式系统中。