Java NIO(New Input/Output)是 Java 1.4 版本引入的新的 I/O API,它提供了与标准 I/O 不同的处理方式,主要区别在于 NIO 采用了非阻塞的 I/O 操作,并且基于通道(Channel)和缓冲区(Buffer)进行数据处理。NIO的核心思想是通过通道(Channel)、缓冲区(Buffer)和多路复用(Selector)实现高效的I/O操作,尤其适合网络服务器需要处理大量连接的场景。
NIO与传统IO(BIO)的区别
特性 | 传统IO(BIO) | NIO |
---|---|---|
模型 | 阻塞式(Blocking) | 非阻塞式(Non-blocking) |
数据流 | 面向流(Stream) | 面向缓冲区(Buffer) |
多线程处理 | 每个连接需一个线程,资源消耗大 | 单线程处理多个连接(Selector) |
适用场景 | 低并发、简单连接 | 高并发、大量短连接 |
核心组件
Java NIO 的核心组件包括:
-
通道(Channel)
- 通道是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
- 通道与流的不同之处在于,通道是双向的,而流是单向的(只能读或写)。
- 主要实现类有:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel 等。
-
缓冲区(Buffer)
- 缓冲区是一个用于存储特定基本类型数据的容器,本质上是一个数组。
- 所有缓冲区都是 Buffer 抽象类的子类,主要实现类有:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
-
选择器(Selector)
- 选择器允许一个单独的线程来监视多个通道的输入,从而实现非阻塞 I/O。
- 可以注册多个通道到一个选择器,然后使用一个线程来 "选择" 已准备好进行 I/O 操作的通道。
缓冲区(Buffer)
缓冲区是 NIO 中数据的容器,使用时需要了解几个核心概念:
- 容量(Capacity):缓冲区能够容纳的数据元素的最大数量,在创建时设定,不能改变。
ByteBuffer
(最常用)、CharBuffer
、IntBuffer
等。
- 位置(Position):下一个要读取或写入的数据元素的索引。
- 界限(Limit):缓冲区中可以操作的数据的界限,位于 limit 之后的数据不能读写。
- 标记(Mark):一个备忘位置,调用 mark () 方法将 mark 设为当前的 position 值,之后可以通过 reset () 方法恢复到这个位置。
这四个属性之间的关系满足:0 <= mark <= position <= limit <= capacity
缓冲区的基本使用流程:
- 写入数据到 Buffer
- 调用 flip () 方法,将 Buffer 从写模式切换到读模式
- 从 Buffer 中读取数据
- 调用 clear () 方法或者 compact () 方法,将 Buffer 清空,以便再次写入
通道(Channel)
通道是数据传输的载体,主要有以下几种类型:
- FileChannel:用于文件的数据读写。
- SocketChannel:用于 TCP 网络数据读写。
- ServerSocketChannel:允许我们监听 TCP 连接请求,为每个连接创建一个新的 SocketChannel。
- DatagramChannel:用于 UDP 数据读写。
FileChannel 示例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelExample {
public static void main(String[] args) throws Exception {
// 创建文件输入流
FileInputStream fin = new FileInputStream("input.txt");
// 获取输入流的通道
FileChannel inChannel = fin.getChannel();
// 创建文件输出流
FileOutputStream fout = new FileOutputStream("output.txt");
// 获取输出流的通道
FileChannel outChannel = fout.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从输入通道读取数据到缓冲区
while (inChannel.read(buffer) != -1) {
// 切换到读模式
buffer.flip();
// 从缓冲区写入数据到输出通道
outChannel.write(buffer);
// 清空缓冲区,准备下一次读取
buffer.clear();
}
// 关闭通道和流
inChannel.close();
outChannel.close();
fin.close();
fout.close();
}
}
选择器(Selector)
选择器是 Java NIO 中实现非阻塞 I/O 的关键组件,它允许一个线程处理多个通道。使用选择器的步骤如下:
- 创建 Selector 对象
- 将 Channel 注册到 Selector,并指定感兴趣的事件
- 调用 Selector 的 select () 方法,检查是否有通道准备好进行 I/O 操作
- 获取就绪通道的集合,进行相应的处理
Selector 支持的事件类型:
- SelectionKey.OP_READ:通道有数据可读
- SelectionKey.OP_WRITE:通道可以写入数据
- SelectionKey.OP_CONNECT:通道建立了连接
- SelectionKey.OP_ACCEPT:通道准备好接受新的连接
非阻塞服务器示例:
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 NonBlockingServer {
public static void main(String[] args) throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 创建ServerSocketChannel并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 将ServerSocketChannel注册到Selector,并监听ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动,监听端口8080...");
while (true) {
// 等待就绪的通道,select()方法会阻塞直到有通道就绪
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 获取所有就绪的SelectionKey
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 处理接受连接事件
if (key.isAcceptable()) {
// 获取服务器套接字通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 接受客户端连接
SocketChannel socketChannel = ssc.accept();
System.out.println("接受新连接: " + socketChannel);
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
// 注册到Selector,并监听READ事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 处理读取数据事件
else if (key.isReadable()) {
// 获取客户端套接字通道
SocketChannel socketChannel = (SocketChannel) key.channel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据
int bytesRead = socketChannel.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());
socketChannel.write(response);
} else if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端关闭连接: " + socketChannel);
socketChannel.close();
}
}
// 移除处理过的key
keyIterator.remove();
}
}
}
}
客户端可以连接到服务器,发送消息并接收服务器的响应:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class NonBlockingClient {
public static void main(String[] args) {
try (
// 创建SocketChannel并连接到服务器
SocketChannel socketChannel = SocketChannel.open();
// 创建Scanner用于读取用户输入
Scanner scanner = new Scanner(System.in)
) {
// 连接到服务器
socketChannel.connect(new InetSocketAddress("localhost", 8080));
System.out.println("已连接到服务器");
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取用户输入并发送消息
while (true) {
System.out.print("请输入消息(输入exit退出):");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 发送消息到服务器
buffer.clear();
buffer.put(message.getBytes("UTF-8"));
buffer.flip();
socketChannel.write(buffer);
// 接收服务器响应
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String response = new String(data, "UTF-8");
System.out.println("服务器响应:" + response);
}
}
System.out.println("客户端关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码说明:
-
建立连接:
- 创建
SocketChannel
并连接到服务器的地址和端口(localhost:8080)
- 创建
-
消息发送:
- 使用标准输入读取用户输入的消息
- 将消息编码为 UTF-8 格式并放入 ByteBuffer
- 通过 SocketChannel 将消息发送到服务器
-
响应接收:
- 从 SocketChannel 读取服务器响应
- 将接收到的数据解码为字符串并打印
-
资源管理:
- 使用 try-with-resources 自动关闭 SocketChannel
- 输入 "exit" 时退出客户端程序
使用方法:
- 先启动服务器程序(NonBlockingServer)
- 再启动客户端程序(NonBlockingClient)
- 在客户端输入消息并按回车发送
- 查看服务器响应
- 输入 "exit" 结束会话
运行效果图:
阻塞与非阻塞 I/O
Java NIO 的非阻塞模式是其核心优势之一:
- 阻塞 I/O:传统的 Java I/O 是阻塞的,当一个线程调用 read () 或 write () 方法时,该线程会被阻塞,直到有数据可读或数据完全写入,在此期间线程不能做其他事情。
- 非阻塞 I/O:Java NIO 的非阻塞模式允许线程在没有数据可读时立即返回,继续执行其他任务,而不是被阻塞。通过 Selector,可以监视多个通道的 I/O 状态,一个线程可以处理多个通道的请求。
非阻塞 I/O 的优势在于:
- 减少线程数量,降低系统开销
- 提高系统吞吐量和响应速度
- 更高效地利用系统资源
应用场景
Java NIO 适合以下场景:
- 高并发连接:需要处理大量并发连接的应用,如服务器、中间件等。
- 实时通信:需要实时响应的应用,如即时通讯、游戏服务器等。
- I/O 密集型应用:需要处理大量 I/O 操作的应用,使用 NIO 可以减少线程数量,提高系统效率。
Java NIO 通过通道、缓冲区和选择器提供了非阻塞的 I/O 操作方式,相比传统的 I/O 更加高效,尤其适合处理大量并发连接的场景。理解通道、缓冲区和选择器的工作原理,以及它们之间的协作方式,是掌握 Java NIO 编程的关键。