在Java中,处理I/O(输入/输出)操作的方式经历了从BIO(Blocking I/O,阻塞式I/O)到NIO(New I/O 或 Non-blocking I/O,新I/O或非阻塞式I/O)的演变。这两种模型在设计和使用上有显著的区别,它们分别适用于不同的应用场景。本文将深入探讨这两种I/O模型的原理、区别以及各自的使用场景。
Java BIO是一种同步阻塞的I/O模型,它是Java最早提供的I/O模型。在进行读写操作的时候,若使用BIO进行通信,则操作不再受到操作系统的控制,而是由应用程序自己控制。在BIO中,数据的读取写入必须阻塞在一个线程内等待其完成。
BIO的工作原理相对简单:当一个连接建立后,服务端会为每个连接创建一个新的线程进行处理,直到连接关闭。这种方式在并发连接数较少时表现良好,但当并发连接数增加时,由于每个连接都需要一个独立的线程,系统的资源消耗会急剧增加,导致性能下降。
BIO的另一个特点是它是面向流的,即一次只能处理一个输入或输出请求,且这些请求是单向的。这种处理方式在某些场景下可能不够灵活。
Java NIO是一种同步非阻塞的I/O模型,它引入了多路复用器和缓冲区的概念,使得一个线程可以处理多个连接,提高了系统的吞吐量和性能。
同步与异步:BIO是同步的,读写操作必须等待数据准备好后才能进行;而NIO是同步非阻塞的,读写操作不再受到数据准备状态的限制,可以进行读写操作,但可能需要等待数据真正写入或读取完成。
阻塞与非阻塞:BIO是阻塞的,在进行读写操作的时候,若使用BIO进行通信,则操作必须阻塞在一个线程内等待其完成;而NIO是非阻塞的,在进行读写操作的时候,若使用NIO进行通信,则操作不再受到阻塞的限制,可以进行其他操作。
面向流与面向缓冲:BIO是面向流的,一次只能处理一个输入或输出请求;而NIO是面向缓冲区的,一次可以处理多个输入或输出请求。
选择器(Selector):NIO有选择器,而BIO没有。选择器能够检测多个注册的通道上是否有事件发生,如果有事件发生便获取事件然后针对每个事件进行相应的响应处理,这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。
1、BIO(Blocking I/O)为什么是同步阻塞的?
2、NIO(New I/O 或 Non-blocking I/O)为什么是同步非阻塞的?
BIO模型因其简单的编程模型和直观的控制流程而易于理解和使用,但在处理大量并发连接时可能会因为每个连接都需要一个线程而变得效率低下。
NIO模型通过引入选择器和通道,使得单个线程可以处理多个连接,从而提高了系统的吞吐量和可伸缩性。虽然NIO的编程模型相对复杂,但它为处理高并发和大数据量的场景提供了更有效的解决方案。
使用BIO(Blocking I/O,阻塞式I/O)模型实现文件复制涉及到使用FileInputStream和FileOutputStream类。以下例展示了如何使用BIO复制文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyBIO {
public static void main(String[] args) {
String sourceFilePath = "path/to/source/file.txt";
String targetFilePath = "path/to/target/file.txt";
try (FileInputStream inputStream = new FileInputStream(sourceFilePath);
FileOutputStream outputStream = new FileOutputStream(targetFilePath)) {
byte[] buffer = new byte[1024]; // 缓冲区,用于临时存储读取的数据
int bytesRead;
// 读取源文件,并写入目标文件
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
System.out.println("File copied successfully!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,我们创建了一个FileInputStream对象来读取源文件,和一个FileOutputStream对象来写入目标文件。我们使用一个字节数组buffer作为缓冲区,来临时存储从源文件读取的数据。while循环会持续读取数据,直到没有更多数据可读(即read方法返回-1)。
每次调用inputStream.read(buffer)时,它会阻塞直到有一些数据可以读取,或者到达文件末尾。同样地,outputStream.write(buffer, 0, bytesRead)也会阻塞直到所有数据都被写入。
这个简单的例子演示了BIO的基本工作方式:它会阻塞等待I/O操作的完成。在高并发或大数据量的场景下,这种阻塞行为可能会成为性能瓶颈,这时可能需要考虑使用NIO(Non-blocking I/O)或其他更高效的I/O模型。
Java NIO实现文件复制,使用FileChannel和ByteBuffer来以流的方式处理文件,适合处理大文件,因为它不会一次性将整个文件加载到内存中。
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FileCopyWithNIO {
public static void main(String[] args) {
// 源文件路径
Path sourcePath = Paths.get("source.txt");
// 目标文件路径
Path destinationPath = Paths.get("destination.txt");
try {
// 打开源文件以进行读取,并获取其FileChannel
FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
// 打开(或创建)目标文件以进行写入,并获取其FileChannel
// 注意:使用TRY_WITH_RESOURCES需要确保sourceChannel和destinationChannel都实现了AutoCloseable接口
// 这里我们手动关闭它们,所以没有使用TRY_WITH_RESOURCES
FileChannel destinationChannel = FileChannel.open(destinationPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
// 分配一个ByteBuffer来存储从源文件中读取的数据
ByteBuffer buffer = ByteBuffer.allocate(1024); // 缓冲区大小可以根据需要调整
// 读取并复制文件内容
while (sourceChannel.read(buffer) != -1) {
// 切换ByteBuffer为读模式,准备从buffer中读取数据
buffer.flip();
// 将数据写入目标文件
destinationChannel.write(buffer);
// 清空buffer,准备下一次读取
buffer.clear();
}
// 关闭文件通道
sourceChannel.close();
destinationChannel.close();
System.out.println("File copied successfully.");
} catch (IOException e) {
System.err.println("Error occurred while copying file: " + e.getMessage());
}
}
}
这个例子展示了如何使用Java NIO的FileChannel和ByteBuffer以高效的方式复制文件,特别适用于处理大文件,因为它不需要一次性加载整个文件到内存中。
使用BIO(Blocking I/O,阻塞式I/O)实现socket通信涉及到ServerSocket和Socket类。下面是一个简单的例子,展示如何使用BIO实现一个基本的服务器-客户端socket通信。
服务器端(Server)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
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 is listening on port " + port);
while (true) {
Socket clientSocket = serverSocket.accept(); // 阻塞等待客户端连接
System.out.println("Client connected from " + clientSocket.getInetAddress());
// 启动一个新线程来处理客户端请求
new Thread(() -> handleClient(clientSocket)).start();
}
} catch (IOException e) {
System.err.println("Could not listen on port " + port);
e.printStackTrace();
}
}
private static void handleClient(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String clientMessage;
while ((clientMessage = in.readLine()) != null) { // 阻塞等待客户端消息
System.out.println("Received message from client: " + clientMessage);
out.println("Echo from server: " + clientMessage); // 发送响应给客户端
}
} catch (IOException e) {
System.err.println("Error handling client: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端(Client)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class BIOClient {
public static void main(String[] args) {
String host = "localhost";
int port = 8080;
try (Socket socket = new Socket(host, port)) {
System.out.println("Connected to server at " + host + ":" + port);
// 发送消息给服务器
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
String serverResponse = in.readLine(); // 阻塞等待服务器响应
System.out.println("Server response: " + serverResponse);
if (userInput.equalsIgnoreCase("bye")) {
break;
}
}
} catch (IOException e) {
System.err.println("Couldn't connect to server at " + host + ":" + port);
e.printStackTrace();
}
}
}
在这个例子中,服务器使用ServerSocket监听8080端口。当客户端连接时,serverSocket.accept()方法会阻塞,直到有客户端连接上。一旦连接建立,服务器会为新连接的客户端启动一个新线程来处理通信。
客户端使用Socket类连接到服务器。客户端和服务器都使用BufferedReader和PrintWriter来读写数据。注意,在读取和写入数据时,这些操作都是阻塞的。
这个例子展示了BIO的基本工作原理:读写操作受到操作系统的控制,并且在操作完成之前,执行这些操作的线程会被阻塞。在高并发的场景下,这种模型可能会导致资源利用率低下,因为每个连接都需要一个线程来处理。
使用Selector来实现非阻塞式I/O操作,我们创建一个简单的非阻塞式服务器,它能够同时处理多个客户端连接。
服务端:
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 = Selector.open();
// 打开一个ServerSocketChannel并设置为非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 绑定ServerSocketChannel到一个地址和端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 将ServerSocketChannel注册到Selector,关心ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞等待需要处理的事件
int readyChannels = selector.select();
if (readyChannels == 0) continue;
// 获取可用通道集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 遍历SelectionKey
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 检查是否有新的连接,如果有,则接受该连接
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted new connection from " + client);
}
// 检查是否有数据可读
else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close();
} else {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
client.register(selector, SelectionKey.OP_WRITE);
}
}
// 检查是否有数据可写
else if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello from server!".getBytes());
buffer.flip();
client.write(buffer);
client.close();
}
// 处理完SelectionKey后,需要从集合中删除
keyIterator.remove();
}
}
}
}
客户端将连接到服务器,发送一条消息,并等待接收服务器的响应
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NonBlockingClient {
public static void main(String[] args) throws IOException {
// 打开一个SocketChannel并连接到服务器
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 完成连接过程
while (!socketChannel.finishConnect()) {
// 非阻塞模式下,可能需要多次调用finishConnect()来完成连接
System.out.println("Connecting to server...");
}
System.out.println("Connected to server");
// 准备要发送的数据
String message = "Hello from client!";
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes());
// 发送数据到服务器
while (writeBuffer.hasRemaining()) {
socketChannel.write(writeBuffer);
}
// 准备读取服务器的响应
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
// 读取服务器的响应
while (true) {
int bytesRead = socketChannel.read(readBuffer);
if (bytesRead == -1) {
break; // 服务器已关闭连接
}
if (bytesRead > 0) {
readBuffer.flip();
while (readBuffer.hasRemaining()) {
System.out.print((char) readBuffer.get());
}
readBuffer.clear();
}
}
// 关闭SocketChannel
socketChannel.close();
}
}
Java BIO和NIO是两种不同的I/O模型,它们在设计、工作原理和使用上有显著的区别。BIO是同步阻塞的I/O模型,它简单直接但性能有限;而NIO是同步非阻塞的I/O模型,它引入了多路复用器和缓冲区的概念,提高了系统的吞吐量和性能。在选择使用哪种模型时,需要根据具体的应用场景和需求进行权衡。如果并发连接数较少且对性能要求不高,可以选择使用BIO;如果并发连接数较多且对性能要求较高,可以选择使用NIO。