本文会对比BIO、NIO、AIO的区别,会从计算机运行的角度去分析这些IO,通过此方式来加强自己对IO的理解,也希望和大家一起交流学习。
宏观了解IO
- 计算机由硬件和软件组成,硬件是软件运行的基础。接下来我会简单分析一下一个程序在计算机中是怎样运行的。这里以window系统为例,当我们双击一个应用,比如微信,之后电脑作了下面的操作。
1.首先双击微信时候,操作系统会首先读取程序的标识信息【包括文件的位置,大小,创建时间等】。
2.操作系统根据程序所在的位置,通过文件管理系统将程序从磁盘加载到内存中【磁盘寻址】。
3.初始化程序运行数据,创建进程。
4.程序运行通过CPU分配的时间片去运行。 - 分析完一个程序的运行过程后,我们可以知道,程序要经过读取、加载、cpu分配时间片运行这些步骤,而这些步骤多多少少都涉及到了IO流的操作,那么你有没有思考过这些IO流有什么区别,为什么会有那么多不同种类的IO流呢?
其实可以简单猜想,肯定是需求场景不同,所以需要的IO流不同,接下来我会简单介绍下这些不同IO流。
IO模型
IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO
BIO(Blocking IO)
- BIO是什么,BIO是同步阻塞式IO。什么是同步,同步就是上一个步骤没有完成,就不能进行下一个步骤。什么是阻塞,就是必须等待一个步骤处理完,并得到结果。
- 从上面定义可以看出BIO的缺点:会阻塞程序运行。传统的Socket编程就是使用BIO这种方式,现在的网络通信都是基于BIO发展起来的,IO模型的不断优化,才有了现在的即时通讯,例如微信和QQ流畅的聊天,以及生活中只要涉及到互联网的地方都会有IO的产生。下面来看一下传统的Socket编程:
# 服务端代码如下:
public class ServiceSocket {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待连接...");
//等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端连接了,开始处理请求");
//每次来一个连接就创建一个线程去处理
new Thread(() -> {
byte[] bytes = new byte[1024];
try {
int read = socket.getInputStream().read(bytes);
if (-1 != read) {
System.out.println("读取客户端发来的数据:" + new String(bytes, 0, read));
}
//回写数据给客户端
socket.getOutputStream().write("Hello Client".getBytes());
socket.getOutputStream().flush();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
# 客户端代码如下:
public class ClientSocket {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1",9000);
//发数据给服务端
System.out.println("客户端准备发消息给服务端...");
socket.getOutputStream().write("Hello Server".getBytes());
socket.getOutputStream().flush();
byte[] bytes = new byte[1024];
int read = socket.getInputStream().read(bytes);
System.out.println("客户端收到服务端消息:"+new String(bytes,0,read));
socket.close();
}
}
- 上面的Socket在实际使用中会存在许多问题,比如我们说的BIO阻塞式IO,那么具体阻塞在哪?如下:Socket socket = serverSocket.accept(); 这段代码会阻塞住,只有当有连接进来时才会继续执行;
int read = socket.getInputStream().read(bytes); 这段代码也会阻塞,只有读取到数据后才会处理。
上面的Socket使用中还会有这样的问题,一个客户端对应一个线程请求,那么在高并发时,上万的客户端同时连接,那么线程也要创建上万个,可能出现资源不够服务压力过大的情况,当然可以使用线程池的方式,但是试想一下,线程池中的线程都有任务在处理,其它的任务只能排队处理,很有可能线程池也承受不足并发的压力。 - 综上所述,使用BIO的场景可以是对于那些连接数较少,系统架构比较固定的场合。BIO有那么多不足,所以之后出现了NIO这个东西。
- BIO模型图如下:
NIO(None Blocking Io)
-
NIO是同步非阻塞IO,一个线程就可以处理多个连接,客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理。I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现。
这里简单说明一下这三种方式的不同:
1.select:底层数组,连接数有限制,通过循环遍历查看是否有对应事件,时间复杂度 O(n)。
2.poll:底层链表,连接没有限制,通过循环遍历查看是否有对应事件,时间复杂度 O(n)。
3.epoll:底层哈希表,没有连接限制,当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度 O(1)。 -
NIO相比于BIO读取速度会快很多,但设计开发复杂很多,当然需要特定的场景,比如:适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统,当一个连接处理时间很长时后面的请求也会等待,阻塞。下面首先看一下NIO模型图:
-
上图是NIO的大体模型图,我们了解到一个关键的组件,selector,了解NIO还需要了解其它的几个核心组件
1.Channel(通道):可以理解为水管,数据流动的管道,每一个管道都要注册到selector上
2.Buffer(缓冲区):可以理解为蓄水池,每一个Channel对应一个Buffer,Buffer底层就是数组,这里还要注意的一点是,Buffer与Channel管道流向可以是双向的,既可以是读操作也可以是写操作,而BIO中读操作需要通过socket.getInputStream().read();BIO写操作需要通过socket.getOutputStream().write(); -
NIO模型图的补充版
-
接下来我们来看一下NIO的代码
# NIO服务端
public class NioServer {
public static void main(String[] args) throws Exception{
//获取一个服务类似于ServerSocket serverSocket = new ServerSocket(9000);
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//设置为非阻塞,如果为true会注册报错
socketChannel.configureBlocking(false);
//绑定端口
socketChannel.socket().bind(new InetSocketAddress(9000));
//创建selector复用器
Selector selector = Selector.open();
//将管道注册,并绑定IO事件
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
System.out.println("等待连接事件...");
selector.select();
System.out.println("有连接进来了");
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()){
SelectionKey key = it.next();
//防止重复处理
it.remove();
//开始处理对应的key
if (key.isAcceptable()) {
System.out.println("有连接事件发生");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
//处理完连接请求不会继续等待客户端的数据发送
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//通过Selector监听Channel时对读事件感兴趣
sc.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("有读事件发生");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//NIO非阻塞体现:当调用到read方法时肯定是客户端发送数据的事件
int len = sc.read(buffer);
if (len != -1) {
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
//将数据写到缓存
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
System.out.println("write事件");
}
}
}
}
}
# NIO客户端
public class NioClient {
public static void main(String[] args) throws Exception{
//创建客户端
SocketChannel channel = SocketChannel.open();
//设置为非阻塞
channel.configureBlocking(false);
//创建选择器
Selector selector = Selector.open();
//连接服务端 目标地址+端口确定一个应用程序
channel.connect(new InetSocketAddress("127.0.0.1",9000));
channel.register(selector, SelectionKey.OP_CONNECT);
// 轮询访问selector
while (true) {
selector.select();
// 获得selector中选中的项的迭代器
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
// 删除已选的key,以防重复处理
it.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (socketChannel.isConnectionPending()) {
socketChannel.finishConnect();
}
// 设置成非阻塞
socketChannel.configureBlocking(false);
//在这里可以给服务端发送信息哦
ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
socketChannel.write(buffer);
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
socketChannel.register(selector, SelectionKey.OP_READ); // 获得了可读的事件
} else if (key.isReadable()) {
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel socketChannel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(buffer);
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
}
}
}
}
}
}
- 为了方便,将逻辑都写在了Main方法中,并且异常均抛出。总结上面代码可以看出大概分为以下几个步骤:
1.创建服务端或者客户端,即创建一个连接管道channel
2.设置为非阻塞的模式
3.创建多路复用器select
4.将管道分别注册到select上,并监听感兴趣的事件
5.当有事件发生,针对不同的事件做出不同的响应
总结:由上代码可以看出,NIO的实现比BIO复杂许多,我们常说BIO是阻塞的,主要体现在BIO的后台线程需要阻塞客户端读写数据,如果客户端不写数据就会阻塞,而NIO将等待客户端操作的事情交给了select,select负责轮询注册到上面的客户端,发现有事件时会交给后台线程处理对应的事件,所以后台线程不会阻塞,只有对应事件发生,才会去处理请求。
AIO(Asynchronous IO)
- 首先我们来看看什么是AIO:AIO是异步非阻塞IO,简单的说就是会有一个线程单独去处理任务,任务处理完毕后会通过调用回调函数来通知主线程任务处理完毕,一般适用于连接数较多且连接时间较长的应用。
- 来看以下AIO的代码实现
# AIO服务端
public class AioServer {
public static void main(String[] args) throws Exception{
//创建AIO服务端
AsynchronousServerSocketChannel socketChannel = AsynchronousServerSocketChannel.open();
//绑定端口
socketChannel.bind(new InetSocketAddress(9000));
//处理连接事件,会在后台单独开启线程去处理
socketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
//回调函数,处理连接完成后的操作
@Override
public void completed(AsynchronousSocketChannel resultChannel, Object attachment) {
//再次接收客户端连接请求
socketChannel.accept(attachment,this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
//这里设置读操作,并且读完后异步通知
resultChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer byteBuffer) {
byteBuffer.flip();
System.out.println(new String(buffer.array(), 0, result));
resultChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
}
# AIO客户端
public class AioClient {
public static void main(String[] args) throws Exception{
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
//这里模拟客户端向服务端发送消息,只会发送一次
socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(1024);
Integer readLen = socketChannel.read(buffer).get();
if (readLen != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, readLen));
}
}
}
- AIO其实是对NIO的封装,但是Netty的底层却是用的NIO,因为在UNIX机器上NIO速度比AIO快,实现没有那么复杂。
总结:
1.BIO使用场景:连接数较少,系统架构比较固定的场合
2.NIO使用场景:适用于连接数目多且连接比较短(轻操作) 的架构
3.AIO使用场景:一般适用于连接数较多且连接时间较长的应用
转载:https://blog.csdn.net/qq_37803406/article/details/105178872