此篇文章我们会简单介绍一下BIO,NIO,AIO三者的基本概念,实现原理以及性能区别,但是大家也只是简单了解原理就好,因为到我们真正的去实现的时候会发现有很多反人类的东西,例如NIO,他的空轮询很容易导致CPU飙升,自定义的方法也可能会有诸多bug。

网络编程的基本模型Client/Server模型,也就是两个进程直接进行相互通信,其中服务端提供配置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接成功,则双方即可进行通信(网络套接字socket)

BIO:

BIO 其实就是IO,文件读写,scoket通信,都是io操作

BIO又被称为阻塞式I/O模型,那为什么会被叫做这个名字呢,原因就是BIO的特性是同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,处理完成后返回应答给客户端,也就是经典的请求-应答通信模型。但是随着客户端并发量上升,服务端的线程数膨胀,系统性能急剧下降,最终会导致系统不可用。这种模型无法满足高并发,高性能的场景。

它的具体通信流程为:

(1)服务器监听

服务器监听:是服务端scoket并不定位具体的客户端scoket,而是处于等待连接的状态,实时监控网络的状态

(2)客户端请求服务器

客户端请求:是指由客户端的scoket提出连接请求,要连接的目标是服务器端的scoket。为此,客户端的scoket必须首先描述它要连接的服务器的scoket,指出服务器scoket的地址和端口号,然后就想服务器端scoket提出连接请求

(3)服务器确认

服务器端连接确认,是指当服务器端scoket监听到或者说接受到客户端scoket的连接请求,他就响应客户端scoket的请求,建立一个新的线程,把服务器端scoket的描述发给客户端。

(4)客户端进行通信

客户端连接确认:一旦客户端确认了此描述,连接就建立好了,双方开始通信。而服务器端scoket继续处于监听状态,继续接受其他客户端scoket的连接请求

总结来说,就是服务端轮询(或者叫无限循环)监听客户端请求,一旦有请求链接,则新建一个线程,来处理这次请求。

所以有个显而易见的问题,那就是,一旦客户端数量过大,线程数肯定就会过大,即使你说使用线程池,也是效率很低的,所以,BIO根本无法实行高并发情景。

NIO:

既然BIO无法适应高并发情景,那么我们自然要想出一种方法来解决这种问题,那么NIO就出现了。

在介绍NIO之前,先澄清一个概念,有的人叫NIO为new IO,有的人把NIO叫做Non-block IO,即同步非阻塞IO

非阻塞。

首先,我们需要先解释三个名词:

1、Buffer-缓冲区

Buffer是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入Buffer对象,体现了新库与原IO的一个重要的区别。

在面向流的IO中,可以将数据直接写入或读取到Stream对象中。在NIO库中,所有的数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),这个字节输出同时存储输入及输出数据,当然也可以使用其他类型的数组,这个数组为缓冲区提供了数据的访问读写等操作属性,如位置,容量,上限等概念。

Buffer类型:我们最常用的就是ByteBuffer,实际上每一种java基本类型都对应了一种缓冲区(除了Boolean类型)

2、通道(Channel)

通道(Channel),也被成为管道,它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是inputStream或者outputStream的子类),而通道可以用于读,写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别,以此执行不同的handler。

事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们通常使用的SocketChannel和ServerSockerChannel都是SelectableChannel的子类

3、多路复用器(seletor)

多路复用器(seletor),他是NIO编程的基础,非常重要,多路复用器提供选择已经就绪的任务的能力。

意思就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。

总结起来,Selector线程就类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好,通知CPU执行IO的读取或写入操作。

Selector模式:当IO事件注册到选择器以后,selector会分配给每个通道一个key值,相当于标签。selector选择器是以轮询的方式进行查找注册的所有通道

当我们的IO事件(通道)准备就绪后,selector就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从通道里读或写数据,写入我们的数据缓冲区中)。

一个多路复用器(Selector)可以负责成千上万个Channel,效率比起BIO大大的提高了,由于jdk使用了epoll代替传统的select实现,所以没有最大连接句柄1024/2048的限制,这也意味着我们只要一个线程负责selector的轮询,就可以接入成千上万个客户端,这是JDK,NIO库的巨大进步。

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

但是,我们需要注意,虽然说NIO是非阻塞的,但是,sellector中的sellector.select(),是阻塞的,所以,你是不是会有什么有趣的想法呢?例如,把sellector这个线程,也是用线程池来分配。

好的,别想了,在想那就是Netty了。Netty的底层就是NIO。

AIO:

AIO编程,在NIO基础之上引入了异步通道的概念。并提供异步文件和异步套接字通道的实现,从而在真正意义上实现了异步非阻塞,之前我们学过的NIO只是非阻塞而非异步。而AIO它不需要通过多路复用器对注册的通道的进行轮训操作即可实现异步读写,从而简化了NIO编程模型。也可以称为NIO2.0,这种模式才是真正的属于异步非阻塞的模型。

 至于上面说的AIO不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写。什么意思呢?

NIO采用轮询的方式,一直在轮询的询问注册在其上的通道(Channel)中数据是否准备就绪,如果准备就绪发起处理。但是AIO就不需要了。

AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO, 即:应用程序向操作系统注册IO监听,然后继续做自己的事情。操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数(这就是一种以订阅者模式进行的改造)。由于应用程序不是“轮询”方式而是订阅-通知方式,所以不再需要selector轮询,由channel通道直接到操作系统注册监听。

由于AIO读完(内核内存拷贝到用户内存)了系统再通知应用,使用回调函数,进行业务处理,所以使得AIO能够胜任那些重量级,读写过程长的任务。

可以说AIO是最简单的模式了。

三者区别

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

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

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