一,五种IO模型:
一个IO操作可以分为两个步骤:发起IO请求和实际的IO操作
例如:
1、操作系统的一次写操作分为两步:第一步,将数据从用户空间拷贝到系统空间;第二步,从系统空间往网卡写。
2、一次读操作也分为两步:第一步,将数据从网卡拷贝到系统空间;第二步,将数据从系统空间拷贝到用户空间。
阻塞IO和非阻塞IO的区别在于第一步:发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步IO和异步IO的区别就在于第二个步是否阻塞:如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统做完IO两个阶段的操作再将结果返回,那么就是异步IO。
1.1 同步阻塞
模型特点 :在Linux中,对于一次读取IO的操作,数据并不会直接拷贝到程序的程序缓冲区。通常包括两个不同阶段:
- 等待数据准备好,到达内核缓冲区;
- 从内核向进程复制数据。
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。
阻塞I/O(blocking I/O)模型,进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回。进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。
故事描述:
小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,三天以后才出来,小明直接打了个地铺睡在举办商售票大厅,一直等票出来,然后买票。
1.2 同步非阻塞
模型特点 :
与阻塞式I/O不同的是,非阻塞的recvform系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error(EAGAIN 或 EWOULDBLOCK)。进程在返回error之后,可以处理其他的业务逻辑,过会儿再发起recvform系统调用。采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。 在linux下,可以通过设置socket套接字选项使其变为非阻塞。
当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。
故事描述:
小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,然后小明走了,办理其他事情去了,然后过了2个小时,又去举办商售票大厅买票来了,如果票还没有出来,小明又先去办其他事情了,重复上面的操作,直到有票可以买。
1.3 I/O复用(事件驱动)
模型特点 :
IO 多路复用的好处就在于单个进程就可以同时处理多个网络连接的IO。它的基本原理就是不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。以select为例,当用户进程调用了select,那么整个进程会被阻塞,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
IO多路复用方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。
由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。
故事描述:
小明想买票看演唱会,都直接给黄牛(selector/epoll)打电话了,说帮我留意买个票,票买了通知我,我自己去取(当我接到黄牛的电话时,我需要花费整个路成的时间去读这个数据,买拿这个票),那么票没出来之前,小明完全可以做自己的事情。
1.4 信号I/O
模型特点 :
允许Socket进行信号驱动IO,并注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
故事描述:
小明想买票看演唱会,给举办商售票业务员说,给给你留个电话,有票了请你给我打个电话通知一下(是看人家操作系统提不提供这种功能,Linux提供,windows没有这种机制),我自己再来买票(小明完全可以做自己的事情,但是票还是需要小明自己去拿的)。
1.5 异步非阻塞:
模型特点:
上述四种IO模型都是同步的。相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。
在 Linux 中,通知的方式是 “信号”,分为三种情况:
- 如果这个进程正在用户态处理其他逻辑,那就强行打断,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
- 如果这个进程正在内核态处理,例如以同步阻塞方式读写磁盘,那就把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
- 如果这个进程现在被挂起了,例如陷入睡眠,那就把这个进程唤醒,等待CPU调度,触发信号通知。
故事描述:
小明想买票看演唱会,给举办商售票业务员说(异步非阻塞i/o)打电话了,给你留个地址,有票了请通知快递员,把这张票送到这个地址来,当小明听到敲门声,看见快递员,就知道票好了,而且指导票好了的时候,票已经到他手上了,票不用小明自己去取(应用不用自己再去read数据了)。
2、select(Java)、poll(c++)、epoll(c++)
Linux支持IO多路复用的系统调用有select、poll、epoll,这些调用都是内核级别的。但select、poll、epoll本质上都是同步I/O,先是block住等待就绪的socket,再是block住将数据从内核拷贝到用户内存。
select、poll、epoll之间的区别,如下表:
3、两种I/O多路复用模式:Reactor和Proactor
在这两种模式下的事件多路分离器反馈给程序的信息是不一样的:
1.Reactor模式下说明你可以进行读写(收发)操作了。
2.Proactor模式下说明已经完成读写(收发)操作了,具体内容在给定缓冲区中,可以对这些内容进行其他操作了。
Reactor关注的是I/O操作的就绪事件,而Proactor关注的是I/O操作的完成事件
一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。
Reactor模式采用同步IO,而Proactor采用异步IO。
在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。
而在Proactor模式中,处理器或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。
Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备。
二, 常用网络模型
2.1 BIO模型
BIO 全称Block-IO 是一种同步阻塞的通信模式。我们常说的Stock IO 一般指的是BIO。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
网络编程的基本模型是C/S模型,即两个进程间的通信。服务端提供IP和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
简单的描述一下BIO的服务端通信模型,即BIO 设计原理:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通信模型。
主线程负责监听当有新的连接的时候创建一个新的子线程处理任务。如下图所示:
缺点:
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死掉了。
代码展示:
2.2 NIO模型
NIO 全称New IO,也叫Non-Block IO 是一种同步非阻塞的通信模式。
NIO 设计原理:
NIO 相对于BIO来说一大进步。客户端和服务器之间通过Channel通信。NIO可以在Channel中进行读写操作。这些Channel都会被注册在Selector多路复用器上。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。
1 .Selector(多路复用器)
Selector 称为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。Selector提供选择已经就绪的任务的能力:就是Selector会不断地轮询注册在其上的通道(Channel),如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端只要提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。
NIO所有监听的事件都定义在SelectionKey下。
2. Buffer(缓冲区)
它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区Buffer中进行的。缓冲区实际上是一个数组,但它又与简单数组不同,Buffer 类相比一个简单数组的而言,它是将关于数据的数据内容和信息包含在一个单一的对象中存储的。Buffer最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
常用属性:
position:Buffer可操作的第一个位置。在往Buffer中写数据时会从Buffer数组中的position位置开始写。从Buffer中读数据时会从Buffer的position开始读。
limit:Buffer最多可操作的数据的位置。在往Buffer中写数据时表示最多可写到数据量为limit。从Buffer中读数据时需要开启Buffer的读模式,读从position到limit位置的数据(一般是调用flip方法使limit=position;poslition=0)。
capcity:Buffer数组的容量。
mark:备忘位置,调用mark()使得mark=position,调用reset(),恢复postion使position=mark。
3. Channel(通道)
通道是双向的,和流不同,流是单向的。NIO可以通过Channel进行数据的读,写和同时读写操作,但是不能直接访问数据需要和缓冲区Buffer进行交互。与Selector一起使用时,Channel必须处于非阻塞模式下。
通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
channel与buffer之间的关系:
举例说明: 山西有煤,我们想要,于是建立一条铁路到山西,这条铁路就是这里的"Channel",那么煤通过什么运过来呢?铁路建好了,就差火车了,因此这里的火车就像是缓冲区"Buffer",火车把山西的煤运到这边来,把我们这里的钱运过去。
2.3 AIO模型
AIO 也叫NIO2.0 是一种异步非阻塞的通信模式。在NIO的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
AIO 并没有采用NIO的多路复用器,而是使用异步通道的概念。其read,write方法的返回类型都是Future对象。而Future模型是异步的,其核心思想是:去主函数等待时间。内核先把数据准备好,然后再把数据主动拷贝到程序空间,准备数据和拷贝数据程序都不参与,拷贝好了,系统通知程序可以执行程序了,但AIO现在没有真正的实例,所以一般谈得比较少。
小结:AIO模型中通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道的实现。非阻塞,异步。
常见面试题:
1B IO,NIO,AIO区别
BIO 阻塞同步通信模式,客户端和服务器连接需要三次握手,使用简单,但吞吐量小
NIO 非阻塞同步通信模式,客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。
AIO 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法均是异步方法。
2 Stock通信的伪代码实现流程
服务器绑定端口:server = new ServerSocket(PORT)
服务器阻塞监听:socket = server.accept()
服务器开启线程:new Thread(Handle handle)
服务器读写数据:BufferedReader PrintWriter
客户端绑定IP和PORT:new Socket(IP_ADDRESS, PORT)
客户端传输接收数据:BufferedReader PrintWriter
3 TCP协议与UDP协议有什么区别
TCP : 传输控制协议是基于连接的协议,在正式收发数据前,必须和对方建立可靠的连接。速度慢,合适传输大量数据。
UDP : 用户数据报协议是与TCP相对应的协议。面向非连接的协议,不与对方建立连接,而是直接就把数据包发送过去,速度快,适合传输少量数据。
4 什么是同步阻塞BIO,同步非阻塞NIO,异步非阻塞AIO
同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。
同步非阻塞IO: 用户进程发起一个IO操作以后,可做其它事情,但用户进程需要经常询问IO操作是否完成,这样造成不必要的CPU资源浪费。
异步非阻塞IO: 用户进程发起一个IO操作然后,立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类比Future模式。
总结:
BIO | NIO | AIO 以Java的角度,理解如下:
- BIO,同步阻塞式IO,简单理解:一个线程处理一个连接,发起和处理IO请求都是同步的。通过Socket和ServerSocket完成套接字通道实现。阻塞,同步,连接耗时。
- NIO,同步非阻塞IO,简单理解:一个线程处理多个连接,发起IO请求是非阻塞的但处理IO请求是同步的。通过SocketChannel和ServerSocketChannel完成套接字通道实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。
- AIO,异步非阻塞IO,简单理解:一个有效请求一个线程,发起和处理IO请求都是异步的。通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道实现。非阻塞,异步。
借鉴:https://www.cnblogs.com/diegodu/p/6823855.html
https://www.cnblogs.com/itdragon/p/8337234.html
当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪