Java提供的IO方式
Java 提供了多种 IO 方式,主要分为以下三种:
1. BIO (Blocking IO): 阻塞式 IO
- 这是最传统的一种 IO 方式,特点是同步阻塞;高并发场景下效率较低,容易导致线程资源耗尽。
- 当线程进行 IO 操作时,会被阻塞,直到操作完成才能继续执行。
- 适用于连接数较少且连接比较长的场景,例如文件读写。
- 例子: InputStream, OutputStream, FileReader, FileWriter 等。
// 使用 BIO 读取文件内容
try (FileInputStream fis = new FileInputStream("file.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理读取的数据
}
} catch (IOException e) {
e.printStackTrace();
}
2. NIO (Non-Blocking IO): 非阻塞式 IO
- NIO 引入了通道 (Channel)、缓冲区 (Buffer) 和选择器 (Selector) 等概念,实现了非阻塞 IO。
- 线程可以发起 IO 操作后立即返回,无需等待操作完成。
- 通过 Selector 可以实现多路复用,即一个线程可以同时处理多个 Channel。
- 适用于连接数较多且连接比较短的场景。Selector 的空轮询问题需要处理。空轮询是 NIO 编程中需要注意的问题,它会浪费 CPU 资源、增加响应延迟和能耗。 通过设置超时时间、使用非阻塞方法或事件驱动模型,可以有效地避免空轮询问题,提高应用程序的性能和效率。
- 例子: SocketChannel, ServerSocketChannel, Selector 等。
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 EchoServer {
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) throws IOException {
// 创建 Selector 和 ServerSocketChannel
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress("localhost", 8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环处理事件
while (true) {
// 阻塞等待事件就绪
if (selector.select() == 0) {
// 处理空轮询
System.out.println("Selector 空轮询...");
continue;
}
// 获取就绪 Channel 的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 处理连接事件
if (key.isAcceptable()) {
handleAccept(key);
}
// 处理读事件
if (key.isReadable()) {
handleRead(key);
}
// 处理写事件
if (key.isWritable()) {
handleWrite(key);
}
// 移除处理过的 SelectionKey
keyIterator.remove();
}
}
}
// 处理连接事件
private static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(), SelectionKey.OP_READ);
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
}
// 处理读事件
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
// 将读取的数据写入到另一个缓冲区,用于后续写回客户端
ByteBuffer writeBuffer = ByteBuffer.allocate(bytesRead);
writeBuffer.put(buffer);
writeBuffer.flip();
clientChannel.register(key.selector(), SelectionKey.OP_WRITE, writeBuffer);
} else if (bytesRead == -1) {
// 客户端关闭连接
clientChannel.close();
System.out.println("客户端断开连接:" + clientChannel.getRemoteAddress());
}
}
// 处理写事件
private static void handleWrite(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
clientChannel.write(buffer);
if (!buffer.hasRemaining()) {
// 数据写完,重新注册读事件
clientChannel.register(key.selector(), SelectionKey.OP_READ);
}
}
}
处理空轮询: 在 selector.select() 返回 0 时,表示没有事件就绪,此时进行空轮询处理。
处理连接事件: 当 key.isAcceptable() 为 true 时,表示有新的客户端连接请求,接受连接并注册读事件。
处理读事件: 当 key.isReadable() 为 true 时,表示客户端发送了数据,读取数据并将其写入到另一个缓冲区,用于后续写回客户端。同时注册写事件。
处理写事件: 当 key.isWritable() 为 true 时,表示可以向客户端发送数据,将之前读取的数据写回客户端。如果数据写完,重新注册读事件。
NIO 中处理空轮询的代码示例:
在 Netty 中,事件驱动模型由 EventLoopGroup 和 EventLoop 处理。EventLoopGroup 负责管理 EventLoop,而 EventLoop 负责处理 Channel 的 I/O 事件。当有 I/O 事件发生时,Netty 会自动调用相应的 ChannelHandler 进行处理,避免了空轮询问题。
选择合适的方法:
设置超时时间: 适用于需要定期检查其他资源或执行其他任务的场景。
使用 selectNow() 方法: 适用于需要立即响应 I/O 事件的场景。
事件驱动模型: 适用于高并发、高性能的网络应用程序。
========方法一:设置超时时间
Selector selector = Selector.open();
// ... 注册通道 ...
while (true) {
// 设置超时时间为 1 秒
if (selector.select(1000) == 0) {
// 处理空轮询
System.out.println("Selector 空轮询...");
// 可以执行一些其他的任务,例如检查其他资源
continue;
}
// ... 处理就绪通道 ...
}
=======方法二:使用 selectNow() 方法
Selector selector = Selector.open();
// ... 注册通道 ...
while (true) {
// 使用 selectNow() 立即返回,即使没有通道就绪
if (selector.selectNow() == 0) {
// 处理空轮询
System.out.println("Selector 空轮询...");
// 可以执行一些其他的任务,例如检查其他资源
} else {
// ... 处理就绪通道 ...
}
}
=======方法三:事件驱动模型(Netty 示例)
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// ... 添加 ChannelHandler ...
}
});
// 绑定端口并启动服务器
ChannelFuture future = bootstrap.bind(8080).sync();
future.channel().closeFuture().sync();
3. AIO (Asynchronous IO): 异步 IO
- AIO 是 NIO 的升级版,实现了真正的异步 IO。
- 线程发起 IO 操作后可以立即返回,并注册一个回调函数,当操作完成后会自动调用回调函数。
- 适用于连接数非常多且连接比较长的场景。并非所有操作系统都支持 AIO。
- 例子: AsynchronousSocketChannel, AsynchronousServerSocketChannel 等。
// 使用 AIO 读取文件内容
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(Paths.get("file.txt"));
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> result = fileChannel.read(buffer, 0);
// 获取读取结果
int bytesRead = result.get();
// 处理读取的数据