BIO和NIO | Reactor

java的IO模型&操作系统IO模型

  • java的IO模型 有四种:同步阻塞IO、同步非阻塞IO、IO多路复用、异步IO。其中IO多路复用,也属于同步IO
  • 操作系统的IO模型 有五种:除了上面四种IO模型之外还有信号驱动IO,它属于同步IO

同步阻塞IO模型(BIO)
  阻塞I/O模型就是最常用的I/O模型,缺省情况下所有的文件操作都是阻塞的,以Socket来讲解此模型:在用户空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误时才返回,在此期间会一直等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O。

同步非阻塞IO模型
  recvfrom从用户空间到内核空间的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核空间是不是有数据到来,有数据到来则从内核空间复制数据到用户空间。

IO多路复用(NIO)
  Linux提供select/poll,进程通过将一个或者多个fd(file descriptor,是进程独有的文件描述符表的索引)传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮助我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式替代顺序扫描,因此性能更高。当有fd就绪时,立即会调用callback函数。

信号驱动I/O模型
  首先开启Socket信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为进程生成一个SIGIO信号,通过信号会掉通知应用程序调用recvfrom来读取数据,并通知主循环函数来处理数据。

异步I/O(AIO)
  告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知开发者。这种模型与信号驱动I/O模型的主要区别是:信号驱动I/O模型由内核通知开发者何时可以开始一个I/O操作,异步I/O模型由内核通知开发者I/O操作何时已经完成。

基本概念:IO的类型
同步IO
  应用程序调用内核函数到最终应用程序从用户空间中获取数据的整个流程是需要用户线程一次性完成的那么就是同步IO
异步IO
  应用程序调用内核函数请求获取数据和最终从用户空间中拿到数据不是一次性完成的,而是将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,完成后OS通知应用程序处理(回调)
阻塞IO
  应用程序调用内核函数请求数据,如果此时还没有数据,那么应用程序就一直等待着,直到成功拿到数据为止,此时应用程序线程是一直处于等待状态的,那么就是阻塞IO
非阻塞IO
  应用程序调用内核函数请求数据,如果此时还没有数据,那么应用程序就不等待先去处理其他事情,过一会再重新尝试请求,直到成功拿到数据为止,此时应用程序不会一直处于等待状态,那么就是非阻塞IO

BIO(同步阻塞)

  服务器的实现模式是一个连接一个线程,这样的模式很明显的一个缺陷是:由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出。

NIO(同步非阻塞)

  NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器Selector上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。NIO采用的是一种多路复用的机制,利用单线程轮询事件,高效定位就绪的Channel来决定做什么,只是Select阶段是阻塞式的,能有效避免大量连接数时,频繁线程的切换带来的性能或各种问题。一个线程在同步的进行轮询检查,Selector不断轮询注册在其上的Channel,某个Channel上面发生读写连接请求,这个Channel就处于就绪状态,被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
  首先,Requester方通过Selector.open()创建了一个Selector准备好了调度角色。创建了SocketChannel(ServerSocketChannel) 并注册到Selector中,通过设置key(SelectionKey)告诉调度者所应该关注的连接请求。阻塞,Selector阻塞在select操作中,如果发现有Channel发生连接请求,就会唤醒处理请求。

AIO(异步非阻塞)

  在JDK1.7中,这部分内容被称作NIO.2,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是由OS先完成再通知服务器应用去启动线程处理(回调)。
  AIO不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写。NIO采用轮询的方式,一直在轮询地询问stream中数据是否准备就绪,如果准备就绪发起处理。但是AIO就不需要了,AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO, 即:应用程序向操作系统注册IO监听,然后继续做自己的事情。操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数(这就是一种以订阅者模式进行的改造)。由于应用程序不是“轮询”方式而是订阅-通知方式,所以不再需要selector轮询,由channel通道直接到操作系统注册监听。

总结

Java对BIO、NIO、AIO的支持:

  • Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善
  • Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理
  • Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,

BIO、NIO、AIO适用场景分析:

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

  在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。

Reactor

  Reactor模式是一种事件驱动的编程模型,通过Service Handler同步的将输入事件采用多路复用分发给相应的Request Handler处理。Reactor事件处理机制为:主程序将事件以及对应事件处理的方法在Reactor上进行注册, 如果相应的事件发生,Reactor将会主动调用事件注册的接口,即回调函数。libevent即为封装了epoll并注册相应的事件以及回调函数,实现的事件驱动的框架。传统的网络模型处理IO事件是被动型的,有请求过来就给该请求分配cpu资源用来处理,请求在没有IO发生时也会占用该线程资源,reactor模型中一个线程用来对接nio中的select,在有IO事件时将IO下发给下游注册的处理器来处理IO,避免资源浪费,提高了系统吞吐量。

为何要用Reactor

  常见的网络服务中,如果每一个客户端都维持一个与登陆服务器的连接。那么服务器将维护多个和客户端的连接以处理和客户端的contnect 、read、write ,特别是对于长链接的服务,有多少个c端,就需要在s端维护同等的IO连接。这对服务器来说是一个很大的开销。

1、BIO
比如我们采用BIO的方式来维护和客户端的连接。很明显,为了避免资源耗尽,我们采用线程池的方式来处理读写服务。但是这么做依然有很明显的弊端:

1、同步阻塞IO,读写阻塞,线程等待时间过长
2、在制定线程策略的时候,只能根据CPU的数目来限定可用线程资源,不能根据连接并发数目来制定,
   也就是连接有限制。否则很难保证对客户端请求的高效和公平。
3、多线程之间的上下文切换,造成线程使用效率并不高,并且不易扩展
4、状态数据以及其他需要保持一致的数据,需要采用并发同步控制

2、NIO
那么可以有其他方式来更好的处理么,我们可以采用NIO来处理,NIO中支持的基本机制:

1、非阻塞的IO读写
2、基于IO事件进行分发任务,同时支持对多个fd的监听

事实上NIO已经解决了上述BIO暴露的问题了,服务器的并发客户端有了量的提升,不再受限于一个客户端一个线程来处理,而是一个线程可以维护多个客户端(selector 支持对多个socketChannel 监听)。

三、Reactor
首先我们基于Reactor Pattern 处理模式中,定义以下三种角色:

1、Reactor 将I/O事件分派给对应的Handler
2、Acceptor 处理客户端新连接,并分派请求到处理器链中
3、Handlers 执行非阻塞读/写 任务
  • 单Reactor多线程模型
      Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,由Reactor分发)。在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,这样可以减小主reactor的性能开销,从而更专注的做事件分发工作了,从而提升整个应用的吞吐。
  • 多Reactor多线程模型
      将Reactor分成两部分,mainReactor负责监听server socket,用来处理新连接的建立,将建立的socketChannel指定注册给subReactor。subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离IO读写事件,读写网 络数据,对业务处理的功能,另其扔给worker线程池来完成。
      mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor主要做和建立起来的socket做数据交互和事件业务处理操作,它的个数上一般是和CPU个数等同,每个subReactor一个县城来处理。此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。此种模型已经应用netty上了。

  Reactor事件处理机制的编程模型,在Redis中也得到了很好的运用,Redis中基于I/O多路复用开发Reactor事件处理机制,监听多个套接字的读/写事件。读事件绑定读操作和具体执行命令的操作函数,写事件绑定命令回复的操作函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值