一文了解Java的BIO, NIO和AIO模型
前言
这是我在这个网站整理的笔记,有错误的地方请指出,关注我,接下来还会持续更新。
作者:神的孩子都在歌唱
在 Java 中,BIO、NIO 和 AIO 是三种用于处理网络通信的 I/O 模型,它们的主要区别在于数据处理方式和线程模型的不同。
一. Java的 I/O模型
Java的I/O模型主要包括以下几种:
-
阻塞I/O(Blocking I/O):程序等待I/O操作完成,期间无法做其他事情。
-
非阻塞I/O(Non-blocking I/O):程序发出I/O请求后不等待,可以做其他事情,需要主动检查操作是否完成。
-
I/O多路复用(IO Multiplexing):通过一个线程监视多个I/O通道,当某个通道准备好时才进行操作(如
Selector
)。 -
异步I/O(Asynchronous I/O):程序发出I/O请求,操作完成后系统会通知程序,完全异步处理。
二. BIO(Blocking I/O)
BIO 全称是阻塞式 I/O(Blocking I/O),它是 Java 最传统、最简单的 I/O 模型,采用同步阻塞方式来处理数据。BIO 在处理每个 I/O 请求时会创建一个新的线程,每个线程负责处理一个客户端的连接请求,线程会阻塞等待 I/O 操作完成。
工作原理:
-
服务器端启动一个
ServerSocket
。 -
服务器在调用
accept()
方法时,会阻塞等待客户端连接。 -
客户端连接后,服务器为每个客户端创建一个独立的线程,用于处理 I/O 操作。
-
如果没有数据可读取,线程会阻塞直到有数据可读。
-
线程阻塞会造成系统资源浪费,且无法有效处理大量并发请求。
特点:
-
简单直接。
-
每个连接都需要一个线程来处理。
-
线程数目会随着并发连接数增加,造成较大的资源消耗。
-
适用于连接数量较少且资源充足的场景。
示例代码:
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
InputStream in = socket.getInputStream(); // 阻塞等待数据输入
byte[] buffer = new byte[1024];
int len = in.read(buffer); // 阻塞等待数据读取
三. NIO(Non-blocking I/O)
3.1 介绍
NIO 全称为非阻塞 I/O(Non-blocking I/O),从 Java 1.4 开始引入。它采用了多路复用(Selector)机制,允许通过单个线程处理多个客户端连接,使用事件驱动的方式处理 I/O 操作。
工作原理:
-
NIO 的核心组件包括
Channel(通道)
、Selector(选择器)
和Buffer(缓冲区)
。 -
Selector
监听多个Channel
事件(如连接、读取、写入等),通过单个线程轮询并处理就绪的 I/O 事件。 -
Channel
和传统的Socket
类似,但可以设置为非阻塞模式,当 I/O 操作未完成时不会阻塞线程。 -
数据通过
Buffer
读写,Channel
从Buffer
中读取或写入数据。
组件关系:
-
每个
Channel
都会对应一个Buffer
。 -
Selector
对应一个线程,一个线程对应多个Channel
(连接)。 -
程序切换到哪个
Channel
是由事件(Event
)决定的。 -
Selector
会根据不同的事件,在各个通道上切换。 -
Buffer
就是一个内存块,底层是有一个数组。 -
Buffer
是可以读也可以写,需要flip
方法切换Channel
是双向的
特点:
-
通过少量线程处理大量连接,提升了系统的并发能力。
-
避免了线程阻塞,大幅减少线程数目,降低了资源消耗。
-
更加适用于处理大量并发连接的场景(如聊天服务器、游戏服务器等)。
3.2 缓冲区(Buffer)
Buffer
是Java NIO中用于存储数据的容器。Buffer
不仅用于存储数据,还跟踪当前读写位置等信息,简化了数据的操作流程。
-
工作方式:
Buffer
实际上是一个数组,并且通过以下关键属性来帮助管理缓冲区:-
capacity:缓冲区的总容量(以字节为单位),表示可以存储多少数据。
-
position:当前读/写操作的位置。
-
limit:限制操作的边界,表示可以读/写的最大范围。
-
mark: 标记
-
-
常见类型:Buffer是一个顶级的抽象父类类,以下是其子类
-
ByteBuffer:存储字节数据(常用)。
-
CharBuffer:存储字符数据。
-
IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer:存储相应的基本类型数据。
-
-
使用流程:一般的读写操作流程如下:
-
写入数据到Buffer:先通过
put()
方法将数据写入缓冲区。 -
切换到读模式:调用
flip()
将Buffer从写模式切换到读模式。 -
读取数据:通过
get()
方法读取数据。 -
清除或重用Buffer:调用
clear()
或compact()
方法清空缓冲区或压缩已读取的数据以重用缓冲区。
-
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配一个容量为1024字节的缓冲区
buffer.put(data); // 向缓冲区写入数据
buffer.flip(); // 切换到读模式
channel.write(buffer); // 将缓冲区的数据写入通道
3.3 通道(Channel)
Channel
是Java NIO中的一个接口,它代表了I/O源(如文件或网络套接字)与目标的连接通道。Channel
与传统I/O中的Stream
类似,但它支持异步、非阻塞操作,双向(可以同时进行读写)。
-
常见类型:
-
FileChannel:用于文件的读写操作。
-
SocketChannel:用于TCP网络套接字的读写操作。
-
ServerSocketChannel:用于监听TCP连接的服务器端通道。
-
DatagramChannel:用于UDP数据包的读写操作。
-
-
特点:
Channel
可以异步读写数据,并且可以是非阻塞的。数据的读写通过Buffer
进行。
3.4 Selector(选择器)
Selector
是Java NIO中用于管理多个Channel
的组件,允许一个单一的线程处理多个通道的I/O事件。它的核心功能是监听注册在其上的多个通道的状态,并且可以同时处理这些通道的事件。
-
主要功能:允许线程监控多个通道的读写事件,减少线程数量,提高资源利用率。
-
使用场景:适用于有大量I/O操作的场景,例如高并发的网络服务器。
-
工作机制:通道(Channel)注册到选择器上,并通过选择器监听通道的就绪状态(如读、写、连接等事件)。当有通道就绪时,选择器会返回相应的通道,可以通过处理这些事件来实现非阻塞I/O。
Selector selector = Selector.open(); // 打开一个选择器
channel.register(selector, SelectionKey.OP_READ); // 注册通道到选择器,并监听READ事件
SelectionKey
表示 Selector
和网络通道Channel
的注册关系,共四种:
-
int OP_ACCEPT
:有新的网络连接可以accept
,值为16
-
int OP_CONNECT
:代表连接已经建立,值为8
-
int OP_READ
:代表读操作,值为1
-
int OP_WRITE
:代表写操作,值为4
3.5 案例
需求:
-
编写一个 NIO ,实现服务器端能够接收客户端发送的消息(非阻塞)
-
服务端需要监测客户端上线,离线
3.5.1 服务端
public class GroupChatServer {
// 选择器
private Selector selector;
// 通道
private ServerSocketChannel serverSocketChannel;
// 启动端口
private static final int PORT = 8888;
/**
* 启动
*/
public void start() {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册监听的通道,有新的网络连接就接受
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
/**
* 监听要注册的通道
*/
public void listen() {
try {
while (true) {
// 查询处于就绪状态的通道数
int count = selector.select();
if (count > 0) {
// 获取 selectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 测试此键的通道是否已准备好接受新的 socket 连接
if (selectionKey.isAcceptable()) {
// 接受与此通道的套接字建立的连接
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
// 将通道注册到选择器,读取客户端信息
accept.register(selector, SelectionKey.OP_READ);
System.out.println(accept.getRemoteAddress() + "启动了");
}
// 测试此键的通道是否已准备好读取
if (selectionKey.isReadable()) {
readData(selectionKey);
}
// 删除key,防止重复处理
iterator.remove();
}
} else {
System.out.println("没有就绪的通道数");
}
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
System.out.println("结束");
}
}
/**
* 读取客户端信息
*/
public void readData(SelectionKey key) {
SocketChannel channel = null;
try {
channel = (SocketChannel) key.channel();
// 创建buffer,分配新的字节缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
// 将此 channel 中的字节序列读取到给定的 buffer 中,返回读取的字节数
int read = channel.read(allocate);
if (read > 0) {
String msg = new String(allocate.array());
System.out.println(channel.getRemoteAddress() + "客户端消息: " + msg.trim());
}
} catch (Exception e) {
try {
// 客户端关闭后要进行处理
if (channel != null) {
System.out.println(channel.getRemoteAddress() + "离线了");
}
// 取消注册和关闭通道
key.cancel();
if (channel != null) {
channel.close();
}
} catch (IOException exception) {
System.out.println(exception.getMessage());
}
}
}
public static void main(String[] args) {
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.start();
groupChatServer.listen();
}
}
listen方法代码解析:
-
当客户端连接时,会通过
ServerSocketChannel
得到SocketChannel
。 -
Selector
进行监听select
方法,返回有事件发生的通道的个数。 -
将
socketChannel
注册到Selector
上,register(Selector sel, int ops)
,一个Selector
上可以注册多个SocketChannel
。 -
注册后返回一个
SelectionKey
,会和该Selector
关联(集合)。 -
进一步得到各个
SelectionKey
(有事件发生)。 -
在通过
SelectionKey
反向获取SocketChannel
,方法channel()
。 -
可以通过得到的
channel
,完成业务处理,读取客户端发送的消息。
3.5.2 客户端代码
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1";
private final int PORT = 8888;
private final Selector selector;
private final SocketChannel socketChannel;
//构造器,完成初始化工作
public GroupChatClient() throws IOException {
// 开启新的选择器
selector = Selector.open();
//打开套接字通道并将其连接到远程地址
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将 channel 注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
}
/**
* 向服务器发送消息
*/
public void sendInfo(String info) {
try {
// write: 将字节序列从给定的 buffer 写入此 channel wrap:将字节数组包装到缓冲区中
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public static void main(String[] args) throws IOException {
GroupChatClient groupChatClient = new GroupChatClient();
// 发送数据给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String str = scanner.nextLine();
groupChatClient.sendInfo(str);
}
}
}
3.5.3 测试
我们分别启动服务端和客户端,然后客户端发送消息给服务端
服务端能够正常接收消息和监听客户端状态
3.6 总结
-
Selector:管理多个通道,监听它们的就绪状态,可以有效地处理大规模并发连接。
-
Channel:代表了I/O的连接,支持非阻塞I/O操作。
-
Buffer:用于存储从通道中读取的数据或准备写入通道的数据,通过灵活的读写机制优化数据处理。
四. AIO(Asynchronous I/O)
AIO 全称为异步 I/O(Asynchronous I/O),从 Java 7 开始引入。AIO 提供了真正的异步非阻塞 I/O 操作,适合处理大量连接数且对 I/O 操作要求高的场景。
工作原理:
-
AIO 基于异步事件通知机制,允许在 I/O 操作完成时通知线程进行后续处理。
-
开发者提交 I/O 请求后,不必等待操作完成,操作系统会在 I/O 操作完成后通知应用程序,应用程序可以通过回调函数处理完成后的逻辑。
-
这使得系统可以在处理其他任务时继续执行,不需要等待 I/O 任务的完成。
特点:
-
更加高效地利用系统资源,完全依赖于操作系统的底层支持。
-
更适合高并发和高负载场景,尤其是处理大量连接但每个连接的 I/O 读写较少的场景。
-
复杂性更高,支持度取决于操作系统。
前 AIO
还没有广泛应用,Netty
也是基于 NIO
,而不是 AIO
五. BIO、NIO 和 AIO 的比较
特性 | BIO | NIO | AIO |
---|---|---|---|
I/O 模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
线程模型 | 每个连接一个线程 | 单线程处理多连接 | 异步回调,线程池处理 |
复杂度 | 简单 | 中等 | 高 |
适用场景 | 少量连接,数据处理简单的场景 | 大量并发连接,且 I/O 操作频繁 | 大量并发连接,但 I/O 操作较少 |
性能 | 低 | 中 | 高 |
总结
-
BIO 适合小规模、简单的应用,因为其每个连接都需要一个线程来处理,线程资源消耗较大。
-
NIO 适合大规模、高并发的网络应用,能够通过少量线程处理大量连接,但编程复杂度有所增加。
-
AIO 则适合高负载的网络应用,异步非阻塞使得其在高并发下表现更优越,但依赖于操作系统的支持且编程复杂度较高。
参考文档:https://github.com/dongzl/netty-handbook/tree/master/docs
作者:神的孩子都在歌唱
本人博客:https://blog.csdn.net/weixin_46654114
- 点赞
- 收藏
- 关注作者
评论(0)