【Java网络编程】 五种 IO 模型

五种 IO 模型

我们在研究几种常见模型之前先要搞清楚如下几个概念:

阻塞:

当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足。

非阻塞:

当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。

同步:

如果有多个任务或者事件要发生(主要指 IO 事件),这些任务或者事件必须逐个地进行并且必须应用程序参与,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行。

异步:

如果有多个任务或者事件发生(主要指 IO 事件),都交给操作系统执行我们就可以去做别的事情并不需要真正的完成 IO 操作,当操作完成之后给我们的应用程序一个通知就可以。

网络种数据传输与内核之间的关系:
消息由发送方产生,从发送方的用户空间传入内核空间借助网络传输介质完成传输,消息会发送到接收方的内核空间,接收方如果要想读取时需要将消息从内核空间拷贝到用户空间。

1.1 同步阻塞 IO

模型特点 :

在 Linux 中,对于一次读取 IO 的操作,数据并不会直接拷贝到程序的程序缓冲区。通常包括两个不同阶段:

  • a、等待数据准备好,到达内核缓冲区。
  • b、内核向进程复制数据。 对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。

故事描述:

小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,三天以后才出来,小明直接打了个地铺睡在举办商售票大厅,一直等票出来,然后买票。

【Java网络编程】 五种 IO 模型

我们熟悉的 BIO 这种网络模型就是采用这种 IO 模型,进行开发。

1.2 同步非阻塞 IO

模型特点 :

与阻塞式 I/O 不同的是,非阻塞的 recvform 系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个 error(EAGAIN 或 EWOULDBLOCK)。进程在返回之后,可以处理其他的业务逻辑,过会儿再发起 recvform 系统调用。采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。 在 linux 下,可以通过设置 socket套接字选项使其变为非阻塞。

故事描述:

小明从家里面先到演唱会现场问售票业务员买票,但是票还没出来,然后小明走了,办理其他事情去了,然后过了 2 个小时,又去举办商售票大厅买票来了,如果票还没有出来,小明又先去办其他事情了,重复上面的操作,直到有票可以买。

【Java网络编程】 五种 IO 模型

1.3 I/O 复用(事件驱动)

模型特点 :

IO 多路复用的好处就在于单个进程就可以同时处理多个网络连接的 IO。它的基本原理就是不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。以 select 为例,当用户进程调用了 select,那么整个进程会被阻塞,而同时,kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从内核拷贝到用户进程。

故事描述:

小明想买票看演唱会,都直接给黄牛(selector/epoll)打电话了,说帮我留意买个票,票买了通知我,我自己去取(当我接到黄牛的电话时,我需要花费整个路成的时间去读这个数据,买拿这个票),那么票没出来之前,小明完全可以做自己的事情。

【Java网络编程】 五种 IO 模型

我们通常所说的 Reactor 模式(反应堆设计模式),就是利用 IO 复用模型实现的,并且 Java 当中的 NIO 模型就是这种模式。

反应器设计模式(Reactor pattern)是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至相关的工作线程进行处理。

单线程 Reactor 模型

单线程的 Reactor 模式对于客户端的所有请求使用一个专门的线程去处理,这个线程无限循环地监听是否有客户端的请求抵达,一旦收到客户端的请求,就将其分发给响应处理程序进行处理。

【Java网络编程】 五种 IO 模型

事件驱动设计:

采用基于事件驱动的设计,当有事件触发时才会调用处理器进行数据处理。使用 Reactor 模式可以对线程的数量进行控制,可以使用一个线程去处理大量的事件。

  • Reactor 负责响应 IO 事件,当检测到一个新的事件会将其发送给相应的处理程序去处理。
  • Handler 负责处理非阻塞的行为,标识系统管理的资源,同时将处理程序与事件绑定。

【Java网络编程】 五种 IO 模型

单线程的 Reactor 的特点是只有一个 Reactor 线程,也就是说只有一个 Selector 事件通知器,因此字节的读取 I/O 和后续的业务处理 process()均由 Reactor 线程来做,很显然业务的处理影响后续事件的分发,所以引出多线程版本进行优化。

多线程 Reactor 模型

考虑到工作线程的复用,可以将工作线程设计线程池。将处理器的执行放入线程池,并使用多线程处理业务逻辑,Reactor 仍然是单个线程。

【Java网络编程】 五种 IO 模型

相对于第一种单线程的模式来说,在处理业务逻辑,也就是获取到 IO 的读写事件之后,交由线程池来处理,这样可以减小主 reactor 的性能开销,从而更专注的做事件分发工作了,从而提升整个应用的吞吐。

主从 reactor 模型

【Java网络编程】 五种 IO 模型

第三种模型比起第二种模型,是将 Reactor 分成两部分:

  • 1、mainReactor 负责监听 server socket,用来处理新连接的建立,将建立的 socketChannel 指定注册给 subReactor。
  • 2、subReactor 维护自己的 selector, 基于 mainReactor 注册的 socketChannel 多路分离 IO 读写事件,读写网 络数据,对业务处理的功能,另其扔给 worker 线程池来完成。

第三种模型中,我们可以看到,mainReactor 主要是用来处理网络 IO 连接建立操作,通常一个线程就可以处理,而 subReactor 主要做和建立起来的 socket 做数据交互和事件业务处理操作,此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。关于此种模型的应用,目前有很多优秀的矿建已经在应用了,比如 mina 和 netty 等。

1.4 信号 I/O

模型特点 :

允许 Socket 进行信号驱动 IO,并注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。

故事描述:

小明想买票看演唱会,给举办商售票业务员说,给你留个电话,有票了请你给我打个电话通知一下(是看人家操作系统提不提供这种功能,Linux 提供,windows 没有这种机制),我自己再来买票(小明完全可以做自己的事情,但是票还是需要小明自己去拿的)。

【Java网络编程】 五种 IO 模型

1.5 异步非阻塞 IO

模型特点:

上述四种 IO 模型都是同步的。相对于同步 IO,异步 IO 不是顺序执行。用户进程进行 aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。

在 Linux 中,通知的方式是 “信号”,分为三种情况:

  • 1、如果这个进程正在用户态处理其他逻辑,那就强行打断,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记”一下放进队列,然后返回该进程原来在做的事。
  • 2、如果这个进程正在内核态处理,例如以同步阻塞方式读写磁盘,那就把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
  • 3、如果这个进程现在被挂起了,例如陷入睡眠,那就把这个进程唤醒,等待 CPU 调度,触发信号通知。

故事描述:

小明想买票看演唱会,给举办商售票业务员说(异步非阻塞 i/o)打电话了,给你留个地址,有票了请通知快递员,把这张票送到这个地址来,当小明听到敲门声,看见快递员,就知道票好了,而且指导票好了的时候,票已经到他手上了,票不用小明自己去取(应用不用自己再去 read 数据了)。

【Java网络编程】 五种 IO 模型

我们在网络开发种使用的 Proactor 就是使用此种 IO 模型实现,并且 Java 当中的 AIO 模型就是使用这种模式。与 Reactor 模式不同,Proactor 模式将所有 I/O 操作都交给内核来处理,工作线程仅仅负责业务逻辑。其主要流程如下:

  • 1、向事件分发器注册事件回调。
  • 2、事件发生。
  • 3、操作系统读取数据,并放入应用缓冲区,然后通知事件分发器。
  • 4、事件分发器调用之前注册的函数。
  • 5、在回调函数中对数据进行后续处理。