Java的IO模型详解-BIO,NIO,AIO
文章目录
- 一、BIO相关知识
- 读写模型
- BIO 概述
- BIO 特点
- BIO 实现示例
- 服务器端
- 客户端
- 二、NIO相关知识点
- 读写模型
- NIO 核心概念
- NIO 特点
- NIO 实现示例
- 服务器端
- 客户端
- 三、AIO相关知识
- 读写模型
- AIO 概念
- AIO 组件
- AIO 特点
- AIO 实现示例
- 服务器端
- 客户端
- 总结
一、BIO相关知识
Java 的 BIO (Blocking I/O) 模型是基于传统的同步阻塞 I/O 操作。在这种模型中,每个客户端连接都需要一个独立的线程来处理请求。当线程正在执行 I/O 操作时(如读取或写入),它会被阻塞,直到操作完成。
读写模型
BIO 概述
BIO 是 Java 中最简单的 I/O 模型之一,它使用 java.io
包中的类来实现,例如 InputStream
, OutputStream
, Reader
, 和 Writer
。在 BIO 模型中,每当有客户端连接到来时,服务器就会创建一个新的线程来处理这个连接上的所有读写操作。
BIO 特点
- 同步:读写操作是同步进行的,即必须等待一个操作完成后才能继续下一步操作。
- 阻塞:当线程调用 I/O 方法时,如果数据没有准备好,线程就会被阻塞,直到数据准备好为止。
- 每连接一线程:对于每一个客户端连接,服务器都会分配一个线程来处理该连接的所有 I/O 操作。
- 简单易用:BIO 的实现比较简单,易于理解和使用。
- 低效:在高并发的情况下,由于每个连接都需要一个线程,因此线程的创建和管理成本较高,可能导致服务器资源耗尽。
BIO 实现示例
下面是一个简单的 BIO 服务器端实现示例,该服务器接收客户端的连接,并打印出客户端发送的消息。
服务器端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class BioServer {
public static void main(String[] args) {
int port = 8080; // 服务器监听端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server started on port " + port);
while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class ClientHandler implements Runnable {
private final Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received from client: " + inputLine);
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
out.println("Echo: " + inputLine);
}
System.out.println("Client disconnected.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
接下来是一个简单的 BIO 客户端实现示例,该客户端向服务器发送消息并接收服务器的响应。
import java.io.*;
import java.net.Socket;
public class BioClient {
public static void main(String[] args) {
String serverAddress = "localhost";
int port = 8080;
try (Socket socket = new Socket(serverAddress, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("Hello, server!");
out.println("Another message.");
out.println("bye");
String response;
while ((response = in.readLine()) != null) {
System.out.println("Received from server: " + response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
二、NIO相关知识点
Java 的 NIO (New I/O) 模型是在 Java 1.4 中引入的,它提供了一种新的方式来处理输入输出操作,相比于传统的 BIO (Blocking I/O) 模型,NIO 提供了更高的性能和更灵活的编程模型。NIO 主要包括三个核心组件:缓冲区(Buffer)、通道(Channel) 和选择器(Selector)。
读写模型
NIO 核心概念
-
缓冲区(Buffer):缓冲区用于存储不同类型的二进制数据。与流不同的是,缓冲区可以保存数据,可以对数据进行读写操作,也可以在缓冲区之间复制数据。在 NIO 中,所有数据都是通过缓冲区进行操作的。
-
通道(Channel):通道类似于流,但是它可以双向读写数据。通道可以从文件系统、网络等地方获取或发送数据。与流不同的是,通道可以与缓冲区交互,也就是说数据可以被读取到缓冲区中,或者从缓冲区写入到通道中。
-
选择器(Selector):选择器允许单个线程监控多个通道的状态,比如哪些通道是可读的、哪些是可写的。这使得一个线程可以同时处理多个客户端连接,大大提高了并发处理能力。
NIO 特点
- 非阻塞:NIO 允许非阻塞 I/O 操作,即使没有数据可用,线程也不会被阻塞,而是可以继续执行其他任务。
- 高效:由于使用了缓冲区和选择器,NIO 可以在一个线程中处理多个连接,减少了线程的创建和销毁所带来的开销。
- 复杂性:相比于 BIO,NIO 的编程模型更为复杂,需要更深入地理解缓冲区和通道的概念。
NIO 实现示例
下面是一个简单的 NIO 服务器端实现示例,该服务器接收客户端的连接,并打印出客户端发送的消息。
服务器端
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 {
public static void main(String[] args) {
int port = 8080; // 服务器监听端口
try (Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
serverSocketChannel.bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port " + port);
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()) {
acceptConnection(serverSocketChannel, selector);
} else if (key.isReadable()) {
readData(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void acceptConnection(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
SocketChannel clientChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
}
private static void readData(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int numRead;
while ((numRead = clientChannel.read(buffer)) > 0) {
buffer.flip();
byte[] data = new byte[numRead];
buffer.get(data);
String received = new String(data, "UTF-8");
System.out.println("Received from client: " + received);
buffer.clear();
}
if (numRead == -1) { // 如果客户端关闭了连接
clientChannel.close();
key.cancel();
}
}
}
客户端
下面是一个简单的 NIO 客户端实现示例,该客户端向服务器发送消息并接收服务器的响应。
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class NioClient {
public static void main(String[] args) {
String serverAddress = "localhost";
int port = 8080;
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.connect(new InetSocketAddress(serverAddress, port));
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, server!".getBytes(StandardCharsets.UTF_8));
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
buffer.put("Another message.".getBytes(StandardCharsets.UTF_8));
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
buffer.put("bye".getBytes(StandardCharsets.UTF_8));
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
while (socketChannel.isOpen()) {
buffer.clear();
int numRead = socketChannel.read(buffer);
if (numRead == -1) {
break;
}
buffer.flip();
byte[] data = new byte[numRead];
buffer.get(data);
System.out.println("Received from server: " + new String(data, StandardCharsets.UTF_8));
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、AIO相关知识
Java 的 AIO (Asynchronous I/O) 模型是在 Java 7 中引入的,它是 NIO 的扩展,支持真正的异步 I/O 操作。AIO 有时也被称作 NIO 2,因为它是 NIO 的后续版本,增强了 NIO 的功能。
读写模型
AIO 概念
AIO 的关键概念是异步非阻塞 I/O。在 AIO 中,应用程序发起一个 I/O 请求后,可以立即返回并继续执行其他任务,而不需要等待 I/O 操作完成。当 I/O 操作完成后,应用程序会被通知。
AIO 组件
AIO 主要涉及以下组件:
- AsynchronousFileChannel:用于异步地读取、写入和映射文件。
- AsynchronousSocketChannel:用于异步地处理网络连接。
- AsynchronousServerSocketChannel:用于异步地接受新的连接。
- CompletionHandler:用于处理异步 I/O 操作完成时的回调。
AIO 特点
- 异步性:AIO 支持真正的异步 I/O 操作,这意味着线程可以在发起 I/O 操作后立即返回,而不是等待操作完成。
- 非阻塞:线程不会因为等待 I/O 操作而被阻塞,可以继续执行其他任务。
- 高并发:AIO 适合处理高并发的场景,因为它可以有效地利用少量线程处理大量的 I/O 操作。
- 复杂性:相比 BIO 和 NIO,AIO 的编程模型更为复杂,需要熟悉异步编程的概念。
AIO 实现示例
下面是一个简单的 AIO 服务器端实现示例,该服务器接收客户端的连接,并打印出客户端发送的消息。
服务器端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class AioServer {
public static void main(String[] args) {
int port = 8080; // 服务器监听端口
try (AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(port))) {
System.out.println("Server started on port " + port);
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel result, Void attachment) {
serverChannel.accept(null, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] data = new byte[attachment.remaining()];
attachment.get(data);
System.out.println("Received from client: " + new String(data));
result.write(ByteBuffer.wrap("Echo: ".getBytes()), ByteBuffer.wrap(data), new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
result.read(attachment, attachment, this);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000); // 让服务器运行
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
客户端
下面是一个简单的 AIO 客户端实现示例,该客户端向服务器发送消息并接收服务器的响应。
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class AioClient {
public static void main(String[] args) {
String serverAddress = "localhost";
int port = 8080;
try (AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open()) {
socketChannel.connect(new InetSocketAddress(serverAddress, port), null, new CompletionHandler<Void, Void>() {
@Override
public void completed(Void result, Void attachment) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, server!".getBytes());
buffer.flip();
socketChannel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.clear();
buffer.put("Another message.".getBytes());
buffer.flip();
socketChannel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.clear();
buffer.put("bye".getBytes());
buffer.flip();
socketChannel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
buffer.clear();
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] data = new byte[attachment.remaining()];
attachment.get(data);
System.out.println("Received from server: " + new String(data));
socketChannel.close();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000); // 让客户端运行
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
总结
为了便于类比,所以样例代码都是使用了客户端和服务端的交互代码实现。通过比对不难发现一下特点:
- BIO:简单,易于理解,但在高并发情况下效率较低。
- 发起读请求
- 等待数据(阻塞)
- 返回(数据已经准备好了)
- NIO:提高了效率,支持非阻塞 I/O,但编程复杂度增加。
- 发起读请求
- 请求返回,可以继续做自己的事情
- 问一下数据好了没?
- 没好, 直接返回
- 哈了, 等待数据拷贝好, 返回
- AIO:进一步提高了效率,支持真正的异步 I/O,但编程复杂度最高。
- 发起读请求
- 请求返回,做自己的事情
- 数据准好了, 提醒请求方数据可以用了
- 其他说明
- 实际编程中, 使用较多的是bio, 因为其代码简单,易于理解。
- nio则一般用于框架核心级别的代码, 编程难度介于BIO和AIO之间, 且有如netty之类的较为成熟的框架面世
- aio 是三种IO当中效率最高的, 但是因其编程难度极高, 目前的使用案例较少。而一般开发过程更是很少用到aio.