[IO]-BIO、NIO、AIO

I/O模型

一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO

同步阻塞IO

在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式!

同步非阻塞IO

在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。

异步阻塞IO

此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!

异步非阻塞IO

在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中AIO就是此种IO模型。

BIO

BIO是什么

BIO:同步式阻塞IO,其最为核心的概念就是流(Stream)。一个流要么是输入流,要么是输出流,不可能一个流即使输入流又是输出流。
在进行IO读取的时候,这个线程是会被阻塞的,无法做其他操作,传统简单,通信方式也是如此。这就导致了它并发处理能力非常低,线程之间访问资源通信的时候,耗时也非常久,所以会比较依赖网速与宽带。

BIO通信模型

BIO通信模型:会有一个线程来监听连接,当有一个新的请求,监听线程就会创建一个新的线程来进行处理,这是一种典型的一问一答模式。随着连接的增加,会就频繁的创建和销毁线程,这造成的大量的资源浪费,甚至导致宕机。NIO较好的解决了这个问题。

BIO通信模型-线程池

IO通信模型-线程池:通过引入线程池,可以一定程度上的缓解资源浪费问题,这种方式可以称之为伪异步IO。
图摘自:https://blog.csdn.net/anxpp/article/details/51512200

NIO

NIO是什么

NIO:同步非阻塞通信,他是面向缓冲的IO操作。NIO采用的是一种多路复用的机制,利用单线程轮询事件,高效定位就绪的Channel来决定做什么,只是Select阶段是阻塞式的,能有效避免大量连接数时,频繁线程的切换带来的性能或各种问题。 NIO我们一般认为是New I/O(也是官方的叫法),因为它是相对于老的I/O类库新增的(其实在JDK 1.4中就已经被引入了,但这个名词还会继续用很久,即使它们在现在看来已经是“旧”的了,所以也提示我们在命名时,需要好好考虑),做了很大的改变。但民间跟多人称之为Non-block I/O,即非阻塞I/O,因为这样叫,更能体现它的特点。而下文中的NIO,不是指整个新的I/O库,而是非阻塞I/O。

NIO的三大组件

一个线程在同步的进行轮询检查,Selector不断轮询注册在其上的Channel,某个Channel上面发生读写连接请求,这个Channel就处于就绪状态,被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

Buffer

Buffer是一个对象,包含一些要写入或者读出的数据。在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。除了数组之外,Buffer还提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。每一次读写切换的时候需要调用flip()方法,切换读写。

具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。除了boolean,剩下的Java中七种数据类型都有对应的缓存类。

Channel

Channel指的是可以向其写入数据或是从中读取数据的对象,它类似与Java.io中的Stream,所有的数据读写都是通过Buffer来进行的,永远不会出现直接向Channel写入数据或读取数据的情况。
与Stream不同的是,Channel是双向的,一个流只可能是Input Stream或是Output Stream,Channel打开后则可以进行读取,或是读写。由于Channel是双向的,因此它更能反映出底层操作系统的真实情况。底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

Channel主要分两大类:

  1. SelectableChannel:用户网络读写
  2. FileChannel:用于文件操作

Selector

Selector是Java NIO 编程的基础。

Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

NIO服务端

具体的代码网上有很多,在这里只分享一下编程步骤:

  1. 打开ServerSocketChannel,监听客户端连接
  2. 绑定监听端口,设置连接为非阻塞模式
  3. 创建Reactor线程,创建多路复用器并启动线程
  4. 将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
  5. Selector轮询准备就绪的key
  6. Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路
  7. 设置客户端链路为非阻塞模式
  8. 将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
  9. 异步读取客户端消息到缓冲区
  10. 对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
  11. 将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端

因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能吧需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成。

AIO

AIO是什么

AIO:异步非阻塞IO
AIO不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写。什么意思呢?NIO采用轮询的方式,一直在轮询的询问stream中数据是否准备就绪,如果准备就绪发起处理。但是AIO就不需要了,AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO, 即:应用程序向操作系统注册IO监听,然后继续做自己的事情。操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数(这就是一种以订阅者模式进行的改造)。由于应用程序不是“轮询”方式而是订阅-通知方式,所以不再需要selector轮询,由channel通道直接到操作系统注册监听。

NIO和AIO区别

NIO:会等数据准备好后,再交由应用进行处理,数据的读取/写入过程依然在应用线程中完成,只是将等待的时间剥离到单独的线程中去,节省了数据准备时间,因为多路复用机制,Selector会得到复用,对于那些读写过程时间长的,NIO就不太适合。

AIO: 与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:

1
2
3
4
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel

其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。

应用场景分析

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。在高性能的I/O设计中,有两个比较著名的模式ReactorProactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。(这里不做深究)

总结

BIO是一个连接一个线程。

NIO是一个请求一个线程。

AIO是一个有效请求一个线程。

有一个经典的举例。烧开水。
假设有这么一个场景,有一排水壶(客户)在烧水。
AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。
NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。
BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。
可以看出AIO是最聪明省力,NIO相对省力,叫一个人就能看所有的壶,BIO最愚蠢,劳动力低下。

参考:
Java网络IO编程总结:https://blog.csdn.net/anxpp/article/details/51512200
BIO、NIO、AIO 区别和应用场景:https://blog.csdn.net/lisha006/article/details/82856906
BIO与NIO、AIO的区别:https://blog.csdn.net/skiof007/article/details/52873421

-------------本文结束感谢您的阅读-------------