I/O技术在零碎设计、性能优化、中间件研发中的应用越来越重要,学习和把握I/O相干技术曾经不仅是一个Java攻城狮的加分技能,而是一个必备技能。本文将带你理解BIO/NIO/AIO的倒退历程及实现原理,并介绍以后风行框架Netty的基本原理。

一 Java I/O模型

1 BIO(Blocking IO)

BIO是同步阻塞模型,一个客户端连贯对应一个解决线程。在BIO中,accept和read办法都是阻塞操作,如果没有连贯申请,accept办法阻塞;如果无数据可读取,read办法阻塞。

2 NIO(Non Blocking IO)

NIO是同步非阻塞模型,服务端的一个线程能够解决多个申请,客户端发送的连贯申请注册在多路复用器Selector上,服务端线程通过轮询多路复用器查看是否有IO申请,有则进行解决。

NIO的三大外围组件:

Buffer:用于存储数据,底层基于数组实现,针对8种根本类型提供了对应的缓冲区类。

Channel:用于进行数据传输,面向缓冲区进行操作,反对双向传输,数据能够从Channel读取到Buffer中,也能够从Buffer写到Channel中。

Selector:选择器,当向一个Selector中注册Channel后,Selector 外部的机制就能够主动一直地查问(Select)这些注册的Channel是否有已就绪的 I/O 事件(例如可读,可写,网络连接实现等),这样程序就能够很简略地应用一个线程高效地治理多个Channel,也能够说治理多个网络连接,因而,Selector也被称为多路复用器。当某个Channel下面产生了读或者写事件,这个Channel就处于就绪状态,会被Selector监听到,而后通过SelectionKeys能够获取就绪Channel的汇合,进行后续的I/O操作。

Epoll是Linux下多路复用IO接口select/poll的加强版本,它能显著进步程序在大量并发连贯中只有大量沉闷的状况下的零碎CPU利用率,获取事件的时候,它毋庸遍历整个被侦听的描述符集,只有遍历那些被内核IO事件异步唤醒而退出Ready队列的描述符汇合就行了。

3 AIO(NIO 2.0)

AIO是异步非阻塞模型,个别用于连接数较多且连接时间较长的利用,在读写事件实现后由回调服务去告诉程序启动线程进行解决。与NIO不同,当进行读写操作时,只需间接调用read或write办法即可。这两种办法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read办法的缓冲区,并告诉应用程序;对于写操作而言,当操作系统将write办法传递的流写入结束时,操作系统被动告诉应用程序。能够了解为,read/write办法都是异步的,实现后会被动调用回调函数。

二 I/O模型演变

1 传统I/O模型

对于传统的I/O通信形式来说,客户端连贯到服务端,服务端接管客户端申请并响应的流程为:读取 -> 解码 -> 利用解决 -> 编码 -> 发送后果。服务端为每一个客户端连贯新建一个线程,建设通道,从而解决后续的申请,也就是BIO的形式。

这种形式在客户端数量一直减少的状况下,对于连贯和申请的响应会急剧下降,并且占用太多线程浪费资源,线程数量也不是没有下限的,会遇到各种瓶颈。尽管能够应用线程池进行优化,然而仍然有诸多问题,比方在线程池中所有线程都在解决申请时,无奈响应其余的客户端连贯,每个客户端仍旧须要专门的服务端线程来服务,即便此时客户端无申请,也处于阻塞状态无奈开释。基于此,提出了基于事件驱动的Reactor模型。

2 Reactor模型

Reactor模式是基于事件驱动开发的,服务端程序处理传入多路申请,并将它们同步分派给申请对应的解决线程,Reactor模式也叫Dispatcher模式,即I/O多路复用对立监听事件,收到事件后散发(Dispatch给某过程),这是编写高性能网络服务器的必备技术之一。

Reactor模式以NIO为底层反对,外围组成部分包含Reactor和Handler:

  • Reactor:Reactor在一个独自的线程中运行,负责监听和散发事件,分发给适当的处理程序来对I/O事件做出反馈。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
  • Handlers:处理程序执行I/O事件要实现的理论事件,Reactor通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。相似于客户想要与之交谈的公司中的理论员工。

依据Reactor的数量和Handler线程数量,能够将Reactor分为三种模型:

  • 单线程模型 (单Reactor单线程)
  • 多线程模型 (单Reactor多线程)
  • 主从多线程模型 (多Reactor多线程)

单线程模型

Reactor外部通过Selector监控连贯事件,收到事件后通过dispatch进行散发,如果是连贯建设的事件,则由Acceptor解决,Acceptor通过accept承受连贯,并创立一个Handler来解决连贯后续的各种事件,如果是读写事件,间接调用连贯对应的Handler来解决。

Handler实现read -> (decode -> compute -> encode) ->send的业务流程。

这种模型益处是简略,害处却很显著,当某个Handler阻塞时,会导致其余客户端的handler和accpetor都得不到执行,无奈做到高性能,只实用于业务解决十分疾速的场景,如redis读写操作。

多线程模型

主线程中,Reactor对象通过Selector监控连贯事件,收到事件后通过dispatch进行散发,如果是连贯建设事件,则由Acceptor解决,Acceptor通过accept接管连贯,并创立一个Handler来解决后续事件,而Handler只负责响应事件,不进行业务操作,也就是只进行read读取数据和write写出数据,业务解决交给一个线程池进行解决。

线程池调配一个线程实现真正的业务解决,而后将响应后果交给主过程的Handler解决,Handler将后果send给client。

单Reactor承当所有事件的监听和响应,而当咱们的服务端遇到大量的客户端同时进行连贯,或者在申请连贯时执行一些耗时操作,比方身份认证,权限查看等,这种刹时的高并发就容易成为性能瓶颈。

主从多线程模型

存在多个Reactor,每个Reactor都有本人的Selector选择器,线程和dispatch。

主线程中的mainReactor通过本人的Selector监控连贯建设事件,收到事件后通过Accpetor接管,将新的连贯调配给某个子线程。

子线程中的subReactor将mainReactor调配的连贯退出连贯队列中通过本人的Selector进行监听,并创立一个Handler用于解决后续事件。

Handler实现read -> 业务解决 -> send的残缺业务流程。

对于Reactor,最权威的材料应该是Doug Lea大神的Scalable IO in Java,有趣味的同学能够看看。

三 Netty线程模型

Netty线程模型就是Reactor模式的一个实现,如下图所示:

1 线程组

Netty形象了两组线程池BossGroup和WorkerGroup,其类型都是NioEventLoopGroup,BossGroup用来承受客户端发来的连贯,WorkerGroup则负责对实现TCP三次握手的连贯进行解决。

NioEventLoopGroup外面蕴含了多个NioEventLoop,治理NioEventLoop的生命周期。每个NioEventLoop中蕴含了一个NIO Selector、一个队列、一个线程;其中线程用来做轮询注册到Selector上的Channel的读写事件和对投递到队列外面的事件进行解决。

Boss NioEventLoop线程的执行步骤:

  • 解决accept事件, 与client建设连贯, 生成NioSocketChannel。
  • 将NioSocketChannel注册到某个worker NIOEventLoop上的selector。
  • 解决工作队列的工作, 即runAllTasks。

Worker NioEventLoop线程的执行步骤:

  • 轮询注册到本人Selector上的所有NioSocketChannel的read和write事件。
  • 解决read和write事件,在对应NioSocketChannel解决业务。
  • runAllTasks解决工作队列TaskQueue的工作,一些耗时的业务解决能够放入TaskQueue中缓缓解决,这样不影响数据在pipeline中的流动解决。

Worker NIOEventLoop解决NioSocketChannel业务时,应用了pipeline (管道),管道中保护了handler处理器链表,用来解决channel中的数据。

2 ChannelPipeline

Netty将Channel的数据管道形象为ChannelPipeline,音讯在ChannelPipline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的双向链表,由ChannelHandler对I/O事件进行拦挡和解决,能够不便的新增和删除ChannelHandler来实现不同的业务逻辑定制,不须要对已有的ChannelHandler进行批改,可能实现对批改关闭和对扩大的反对。

ChannelPipeline是一系列的ChannelHandler实例,流经一个Channel的入站和出站事件能够被ChannelPipeline 拦挡。每当一个新的Channel被创立了,都会建设一个新的ChannelPipeline并绑定到该Channel上,这个关联是永久性的;Channel既不能附上另一个ChannelPipeline也不能拆散以后这个。这些都由Netty负责实现,而无需开发人员的特地解决。

依据起源,一个事件将由ChannelInboundHandler或ChannelOutboundHandler解决,ChannelHandlerContext实现转发或流传到下一个ChannelHandler。一个ChannelHandler处理程序能够告诉ChannelPipeline中的下一个ChannelHandler执行。Read事件(入站事件)和write事件(出站事件)应用雷同的pipeline,入站事件会从链表head 往后传递到最初一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的 handler,两种类型的 handler 互不烦扰。

ChannelInboundHandler回调办法:

ChannelOutboundHandler回调办法:

3 异步非阻塞

写操作:通过NioSocketChannel的write办法向连贯外面写入数据时候是非阻塞的,马上会返回,即便调用写入的线程是咱们的业务线程。Netty通过在ChannelPipeline中判断调用NioSocketChannel的write的调用线程是不是其对应的NioEventLoop中的线程,如果发现不是则会把写入申请封装为WriteTask投递到其对应的NioEventLoop中的队列外面,而后等其对应的NioEventLoop中的线程轮询读写事件时候,将其从队列外面取出来执行。

读操作:当从NioSocketChannel中读取数据时候,并不是须要业务线程阻塞期待,而是等NioEventLoop中的IO轮询线程发现Selector上有数据就绪时,通过事件告诉形式来告诉业务数据已就绪,能够来读取并解决了。

每个NioSocketChannel对应的读写事件都是在其对应的NioEventLoop治理的单线程内执行,对同一个NioSocketChannel不存在并发读写,所以无需加锁解决。

应用Netty框架进行网络通信时,当咱们发动I/O申请后会马上返回,而不会阻塞咱们的业务调用线程;如果想要获取申请的响应后果,也不须要业务调用线程应用阻塞的形式来期待,而是当响应后果进去的时候,应用I/O线程异步告诉业务的形式,所以在整个申请 -> 响应过程中业务线程不会因为阻塞期待而不能干其余事件。