BIO+NIO+AIO(输入/输出)的三种模型
·
BIO (Blocking I/O) - 同步阻塞I/O
模型: 一个连接对应一个线程。比如:去餐厅点餐,点完后就站在柜台前一直等着,直到厨师做好饭递给你。期间你不能做任何事
工作方式:
- 应用程序线程发起 read 系统调用。
- 内核开始准备数据(比如从网卡读取)。
- 应用程序线程被阻塞,什么都做不了,直到内核将数据准备好并拷贝到用户空间。
- 数据准备好后,线程被唤醒,继续执行。
特点: 编程简单直观。但在高并发场景下,需要创建大量线程来处理大量连接,线程上下文切换开销巨大,消耗系统资源,性能瓶颈明显。
/**
* BIO模型下的Echo服务器
* 每个客户端连接都会创建一个独立的线程进行处理
*/
public class BioEchoServer {
public static void main(String[] args) throws IOException {
int port = 8888;
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("BIO Echo服务器启动,监听端口: " + port);
//无限循环,接受客户端连接
while(true){
//1.阻塞等待客户端连接(Acceptor线程)
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接来自: " + clientSocket.getRemoteSocketAddress());
// 2. 为每个新的连接创建一个新线程进行处理 (Handler线程)
new Thread(new EchoClientHandler(clientSocket)).start();
}
}
/**
处理客户端连接的线程任务
*/
static class EchoClientHandler implements Runnable{
private final Socket clientSocket;
public EchoClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run(){
try(
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true)
){
String inputLine;
//3.阻塞读取客户端发送的数据
while((inputLine = in.readLine())!=null){
System.out.println("收到消息: " + inputLine);
// 4. 将数据原样写回给客户端 (Echo)
out.println("Echo: " + inputLine);
}
}catch(IOException e){
System.out.println("处理客户端连接时发生异常: " + e.getMessage());
}finally{
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客户端连接关闭: " + clientSocket.getRemoteSocketAddress());
}
}
}
}
NIO (Non-blocking I/O / New I/O) - 同步非阻塞I/O
模型: 一个线程处理多个连接。比如:点完餐后,拿到一个号码牌。你可以离开柜台去玩手机。你时不时地抬头看屏幕(轮询),看看你的号码是否被叫到。叫到后,你去取餐。
工作方式 (核心:Selector + Channel + Buffer):
- 应用程序线程发起 read 系统调用。
- 如果内核数据还没准备好,立即返回一个错误(EWOULDBLOCK),而不是阻塞线程。
- 应用程序线程可以继续处理其他Channel的I/O请求。
- 应用程序通过一个叫 Selector 的组件不断轮询注册在其上的所有 Channel。
- 当某个Channel的数据准备好后,Selector 会通知应用程序线程。
- 线程再对这个Channel进行实际的读写操作。
特点: 大大减少了线程数量,适用于高并发、短连接(如聊天服务器、网关)。编程模型比BIO复杂。
/**
* NIO模型下的Echo服务器
* 使用单个Selector线程处理所有连接(Accept、Read、Write)
*/
public class NioEchoServer {
public static void main(String[] args) throws IOException {
// 1. 创建Selector(调度中心)
Selector selector = Selector.open();
// 2. 创建ServerSocketChannel并设置为非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 必须设置为非阻塞
serverSocketChannel.bind(new InetSocketAddress(8888));
// 3. 将ServerSocketChannel注册到Selector,关注ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Echo服务器启动,监听端口: 8888");
// 事件循环
while (true) {
// 4. 阻塞等待就绪的Channel,超时时间为1秒
if (selector.select(1000) == 0) {
// System.out.println("等待1秒,无事件发生...");
continue;
}
// 5. 获取所有就绪的事件的SelectionKey集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 6. 处理事件,并移除已处理的key
keyIterator.remove();
try {
if (key.isAcceptable()) {
handleAccept(key, selector);
}
if (key.isReadable()) {
handleRead(key);
}
// 可写事件通常不需要专门监听,只在需要时才注册
} catch (IOException e) {
// 客户端异常断开连接
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
System.out.println("客户端连接异常关闭");
}
}
}
}
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受连接,生成SocketChannel
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
System.out.println("客户端连接来自: " + clientChannel.getRemoteAddress());
// 将新连接注册到Selector,关注READ事件,并附加一个Buffer
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 读取数据
int bytesRead = channel.read(buffer);
if (bytesRead == -1) {
// 客户端正常关闭连接
System.out.println("客户端断开连接: " + channel.getRemoteAddress());
channel.close();
return;
}
if (bytesRead > 0) {
// 切换Buffer为读模式
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String message = new String(bytes);
System.out.println("收到消息: " + message);
// 准备Echo回复
String echoMessage = "Echo: " + message;
ByteBuffer echoBuffer = ByteBuffer.wrap(echoMessage.getBytes());
// 将数据写回客户端
channel.write(echoBuffer);
// 清空Buffer,为下一次读做准备
buffer.clear();
}
}
}
// 可以使用 `nc localhost 8888` 或写一个NIO客户端来测试
AIO (Asynchronous I/O) - 异步非阻塞I/O
模型: 基于事件和回调。比如:你点完餐后,就可以完全离开去做自己的事。厨师做好饭后,会主动打电话通知你(回调)饭做好了,让你来取。
工作方式:
- 应用程序线程发起一个 read 异步操作,并提供一个回调函数(或返回一个 Future)。
- 这个调用立即返回,应用程序线程可以去干别的事,完全不会被这个I/O操作阻塞。
- 内核自己完成所有的数据准备和从内核空间到用户空间的拷贝工作。
- 内核完成所有工作后,主动通知应用程序(通过调用之前提供的回调函数,或设置 Future 的状态)。
特点: 理论上是最高效的模型,真正的异步。适用于连接数多且连接时间长的应用(如文件服务器、大型资源下载)。在Linux上的实现底层仍使用epoll,并非真正的原生AIO,因此在实际项目中不如NIO普及。
/**
* AIO模型下的文件服务器(简化版,发送指定文件)
*/
public class AioFileServer {
// 定义要发送的文件路径
private static final String FILE_PATH = "./large_file.dat";
public static void main(String[] args) throws Exception {
// 1. 创建一个异步通道组,使用线程池
AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
Executors.defaultThreadFactory()
);
// 2. 创建异步服务器通道并绑定端口
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(group);
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("AIO文件服务器启动,监听端口: 8888,提供文件: " + FILE_PATH);
// 3. 开始异步接受连接
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
// 接受下一个连接(重要!形成链式调用)
serverChannel.accept(null, this);
// 处理当前连接
System.out.println("客户端连接: " + clientChannel);
try {
handleClient(clientChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("接受连接失败: " + exc.getMessage());
}
});
// 主线程阻塞,防止服务器退出
group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
private static void handleClient(AsynchronousSocketChannel clientChannel) throws IOException {
Path path = Paths.get(FILE_PATH);
// 4. 异步打开文件(返回一个Future)
java.nio.channels.AsynchronousFileChannel fileChannel = java.nio.channels.AsynchronousFileChannel.open(
path, StandardOpenOption.READ);
// 创建一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024 * 64); // 64KB Buffer
// 5. 定义一个内部类,用于链式读取和发送文件
FileReadSendHandler handler = new FileReadSendHandler(fileChannel, clientChannel, buffer, 0L);
// 6. 开始第一轮读取->发送
handler.readAndSend();
}
// 处理读取和发送的回调类
static class FileReadSendHandler implements CompletionHandler<Integer, ByteBuffer> {
private final java.nio.channels.AsynchronousFileChannel fileChannel;
private final AsynchronousSocketChannel clientChannel;
private final ByteBuffer buffer;
private long filePosition;
public FileReadSendHandler(java.nio.channels.AsynchronousFileChannel fileChannel,
AsynchronousSocketChannel clientChannel,
ByteBuffer buffer, long filePosition) {
this.fileChannel = fileChannel;
this.clientChannel = clientChannel;
this.buffer = buffer;
this.filePosition = filePosition;
}
public void readAndSend() {
// 7. 发起异步文件读取
fileChannel.read(buffer, filePosition, buffer, this);
}
@Override
public void completed(Integer bytesRead, ByteBuffer attachment) {
if (bytesRead == -1) {
// 文件读取完毕
System.out.println("文件发送完成给客户端: " + clientChannel);
try {
fileChannel.close();
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
if (bytesRead > 0) {
// 准备Buffer用于写入网络通道
attachment.flip();
filePosition += bytesRead;
// 8. 发起异步网络写入,将刚读到的数据发送给客户端
clientChannel.write(attachment, attachment, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesWritten, ByteBuffer innerBuffer) {
if (innerBuffer.hasRemaining()) {
// 如果没写完,继续写
clientChannel.write(innerBuffer, innerBuffer, this);
} else {
// 当前Buffer的数据已全部发送完毕,清空Buffer,准备下一次读取
innerBuffer.clear();
readAndSend(); // 链式调用,继续读下一块文件内容
}
}
@Override
public void failed(Throwable exc, ByteBuffer innerBuffer) {
System.err.println("发送数据失败: " + exc.getMessage());
closeChannels();
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("读取文件失败: " + exc.getMessage());
closeChannels();
}
private void closeChannels() {
try {
fileChannel.close();
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 这个示例展示了AIO最复杂的回调地狱(Callback Hell)问题,逻辑比NIO复杂很多。
更多推荐

所有评论(0)