本文详细讲解 NIO 模型的原理,并简单介绍下 BIO 和 AIO 模型,以及这三种模型的区别
BIO模型
特点是同步阻塞,一个客户端连接对应一个处理线程。
BIO 模型的缺点:
1、如果服务端使用同步方式处理,代码会阻塞,服务端只能同步处理一个客户端的请求,如果一个客户端连接了服务端,但是一直没有向服务端发送消息,服务端读取数据会阻塞,此时别的客户端无法连接服务端!
2、通过在服务端为每个连接开启一个线程,异步处理客户端的请求,可以解决上述问题,但是如果连接数据过多,系统中的线程数就过多,CPU时间片切换频繁,性能会急剧下降,甚至服务端挂掉。
BIO 适用于连接数目比较少且固定的架构。
NIO模型
特点是同步非阻塞,一个线程可以处理多个请求连接。
NIO适用于连接数目比较多且连接时间短的架构,如聊天服务器。
三大核心组件:Channel(通道)、Buffer(缓冲区)、Selector(多路复用器/选择器)
Channel 通道
channel 是某一个实体 (硬件设备/文件/网络套接字/程序) 和操作系统底层 I/O 进行通信的桥梁。
channel 类似于 流,但有个区别:channel 是双向的,既可以读数据,也可以写数据,而流是单向的。
Buffer 缓冲区
NIO 中对数据进行读写,都通过缓冲区。
这里的 Buffer 是经过封装的,不是普通的 byte 数组。它有三个重要的变量:capaticy、position、limit。感兴趣的可以了解下 Buffer 的工作机制。
Selector 多路复用器
多路复用器 selector 是单线程处理多个请求的核心组件。
服务端和客户端都轮询调用 selector 的 select() 方法获取事件,此方法是一个阻塞方法,如果当前没有事件产生,会阻塞。一旦有事件产生,比如新的客户端连接进来了,或者客户端向服务端写入了数据等,select() 方法会结束,此时从 selector 中就能获取一个 SelectionKey 的集合,每个 SelectionKey 代表一个事件,循环处理所有事件。
这种方式充分压缩了单线程的处理能力,利用单线程处理多个客户端的请求,不同的客户端的请求之间无需阻塞,极大地缩短了各个请求的响应时间。
当然,说完全不阻塞,其实也不准确,只是将阻塞放到了 selector 端。服务端或者客户端从 selector 批量拉取事件,假如拉取到很多事件,由于是同步处理的,可能会比较耗时,这个时间段内新产生的事件,也是需要等待的,等这一批处理完才能拉取到新的事件。所以,非阻塞体现在拉取到事件后,比如读取服务端的响应数据 [ channel.read(buffer) ],一定可以读到。read 方法虽然是阻塞方法,但是既然能获取 SelectionKey 事件,说明服务端必然向客户端写数据了。
I/O多路复用底层使用的 linux 的 api (select、poll、epoll) 来实现。
select | poll | epoll (jdk1.5及以上) | |
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | hash表 |
IO效率 | 每次调用都进行线性遍历 时间复杂度为O(n) | 每次调用都进行线性遍历 时间复杂度为O(n) | 事件通知方式,每当有I/O事件就绪 系统注册的回调函数就会被调用 时间复杂度为O(1) |
最大连接 | 无上限 | 无上限 | 无上限 |
通过Java实现NIO服务端和客户端
服务端 Server
public class NIOServer {
public static void main(String[] args) throws IOException {
// 打开ServerSocketChannel通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverSocketChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverSocketChannel.bind(new InetSocketAddress(8030));
// 获得多路复用器Selector
Selector selector = Selector.open();
// 将ServerSocketChannel通道注册到Selector上,
// 并为该通道设置OP_ACCEPT事件,等待接收客户端的请求
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动成功!");
// 服务端不能停止,需要轮询selector来获取客户端的请求
while (true) {
// 当注册的事件到达时,方法会返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
// 遍历SelectionKey中的事件,挨个处理
while (ite.hasNext()) {
SelectionKey key = ite.next();
// 取出SelectionKey后,从迭代器中移出,防止重复执行
ite.remove();
if (key.isAcceptable()) { // 获取到连接事件
ServerSocketChannel server =
(ServerSocketChannel)key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 给客户端发送信息
channel.write(ByteBuffer.wrap(
"已连接成功".getBytes("utf-8")));
// 给SocketChannel设置可读权限,以便从SocketChannel读取消息。
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 获取到可读事件
// 得到可读事件的SocketChannel
SocketChannel channel = (SocketChannel)key.channel();
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// 将数据读取到缓冲区
channel.read(buffer);
// 从缓冲区取出数据
byte[] data = buffer.array();
String msg = new String(data);
System.out.println("服务端接收到信息:" + msg);
String returnMsg = msg + "消息已接收";
// 将需要返回给客户端的消息放入缓冲区
ByteBuffer outBuffer = ByteBuffer.wrap(
returnMsg.getBytes("utf-8"));
// 将消息回送给客户端
channel.write(outBuffer);
} catch (IOException e) {
// 收到异常时,可能是客户端断开了,所以把通道关闭
channel.close();
}
}
}
}
}
}
流程说明
1、创建 ServerSocketChannel 通道并绑定端口
2、创建并打开多路复用器 Selector
3、将通道注册到 Selector 上,并设置 OP_ACCEPT,此时可以开始接收客户端连接了
4、循环调用 selector 的 select() 方法
-- 如果没有事件发生,此方法会阻塞
-- 一旦有事件(客户端连接/客户端写入消息)发生,此方法会返回
5、从 selector 的 selectedKeys 中获取事件列表,依次处理
客户端 Client
public class NIOClient {
public static void main(String[] args) throws IOException {
// 获得一个SocketChannel通道
SocketChannel socketChannel = SocketChannel.open();
// 设置通道为非阻塞
socketChannel.configureBlocking(false);
// 获得多路复用器Selector
Selector selector = Selector.open();
// 客户端连接服务器,其实在后面的finishConnect方法中才正式连接
socketChannel.connect(new InetSocketAddress(
"127.0.0.1", 8030));
// 将SocketChannel通道注册到Selector上,并给通道设置OP_CONNECT事件。
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 轮询访问selector
while (true) {
// 尝试获取事件,如果没有事件,会阻塞;有事件时方法返回
selector.select();
// 获得selector中SelectionKeys迭代器
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
// 遍历SelectionKey中的事件,挨个处理
while (ite.hasNext()) {
SelectionKey key = ite.next();
ite.remove(); // 取出SelectionKey后,从迭代器中移出,防止重复执行
if (key.isConnectable()) { // 连接事件
// 从SelectionKey中取得通道
SocketChannel channel = (SocketChannel)key.channel();
// 如果正在连接,则完成连接
if(channel.isConnectionPending()){
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
// 给服务器发送信息(也可以不发)
channel.write(ByteBuffer.wrap(
"生日快乐!".getBytes("utf-8")));
// 为了可以接收到服务端的信息,需要给通道设置可读权限。
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 可读事件
SocketChannel channel = (SocketChannel)key.channel();
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读到缓冲区
channel.read(buffer);
// 将数据从缓冲区取出
byte[] data = buffer.array();
String msg = new String(data);
System.out.println("客户端收到信息:" + msg);
}
}
}
}
}
流程说明
1、创建 SocketChannel 通道,连接服务端的ip和端口
2、打开多路复用器 Selector
3、将通道注册到 Selector上,并设置 OP_CONNECT
4、循环调用 selector 的 select() 方法
-- 如果没有事件发生,此方法会阻塞
-- 一旦有事件(客户端可以连接/服务端返回消息)发生,此方法会返回
5、从 selector 的 selectedKeys 中获取事件列表,依次处理
AIO模型
特点是异步非阻塞。
异步体现在主线程不需要一直等待 accept 和 read 方法执行完,可以去做其他事情。
非阻塞原理跟 NIO 模型相同,因为 AIO 底层就是对 NIO 的封装。
AIO 的异步的实现,是在调用 accept 和 read 方法的时候,传入一个回调函数。当有事件发生时,回调函数会去处理它。