NIO到底是什么?

NIO到底是什么?,第1张

俗话说,学不会的,越要研究。对于我这样一个Java小白来说,不断的学习与克服,才是我能够选择的前进道路。

我在刚开始自学Java的时候,学到IO这里,就产生了抵触心理,我认为这个东西没用,且复杂,但是我问了下男朋友,这个东西难不难,他却说很简单很好理解,可能就是因为女生对于计算机自己内部沟通交流的方式不够理解的原因,导致了针对IO学习时的觉得它抽象、复杂。特别对于基于IO实现的NIO,我更是云里雾里,所以趁这次机会,好好的理解与总结一下,希望能够以最通俗易懂的形式记录下来。

在我的理解内,NIO就是将传统的IO阻塞单处理模式,优化为非阻塞且可多处理的模式。

其实针对于普通IO,我们想要实现NIO模式也是可以的,我们可以采用多线程方式进行接收和处理,但是这样十分耗费资源,且虚拟机能够支持的最大线程数是有限的,我们不可能无休止的去创建,并且对于CPU来说,来回的调用众多线程,更是一种耗性能的不可采取方法。

多路复用,其实就是将单一进行复用。针对IO模型来说,多路复用的核心点就在于一个线程,去处理多个请求,也是实现了单一进行复用,不过这里的单一并不是限定,我们其实也可以采用少量的线程,去接收多个请求,以此实现了复用。对于普通IO模型来说,实现复用的形式只能是开启多个线程来进行处理,这样的缺点就在于虚拟机对于线程的限制,以及CPU对于性能的消耗;而对于NIO来说,实际的复用就是采用单一线程进行多个请求的接收。

想要突破IO的瓶颈,就需要使用到上文提到的多路复用模式,所以在Java中,为了突破这样的场景,利用多路复用来实现NIO,非阻塞式的IO。

那么什么是阻塞与非阻塞的IO呢,其实就是针对于实际的IO *** 作过程中,是否需要等待的情况。举个最通俗的例子,就像是胡同地点内的堵车情况,胡同很窄很窄,如果前面的车子不向前走,那么你就必须等待,这就是阻塞;而如果像条条大路这样的城市地点,那么就算你前面有车子,你也可以选择拐弯换道通行,无需等待,这就是非阻塞。

而针对Java中的多路复用模式的实现核心,主要通过一个叫做Selector的轮询选择器来完成。

在之前的普通IO情况下,我们的应用在同一时刻只能阻塞的执行同一个 *** 作,也就是说,如果我们的内核系统(底层层面)不准备好相应的数据,那么我们的用户态(应用层面)就必须等待,直到数据就绪、能够读写才能继续执行,在此期间,其他 *** 作无法进行,而且在普通IO模式下,我们只能接收和处理一个请求,除非开启多线程模式,可是这种做法是会有瓶颈和性能消耗的。

在NIO模式下,我们的应用可以通过Selector这样的选择器,在同一时刻接收多个连接请求。当然,只通过选择器,是不可能完成这样的多路复用模式,在多路复用模式下,还需要有相应的通道channel及buffer容器。

Channel:通道,用于接收及存储不同的连接与状态。

Buffer:容器,在应用写入写出时,都通过这个容器进行。

Selctor:选择器,相当于一个调度中心,负责轮询查看不同通道内的请求,做出相应的选择处理。

Channel这样的通道,属于一层桥梁,而Butter属于一辆货车,数据的通信,需要用货车进行承载运行,通过桥梁,送到需求目的地。而这样的桥梁有多个,针对每个不同桥梁能够运输的货物也是不同的,这就是对于JavaChannel中的key不同,不同的key代表不同的状态,每个Channel放置不同的状态。当送货者知道了自己要送货的目的地时,则装货(请求内容)开车,出发送货(连接),它会对于不同的目的地(状态),会选择不同的桥梁,而到达目的地前,会经过收费站,收费站内会有一个调度员(Selector),调度员手里有需求方所要货物的订单(感兴趣状态),通过这个调度员对每个到达车辆的询问,可以知道是否是需求方真正想要的东西,如果是需求方想要的东西,那就让货车停下,通知需求方来卸货,并接收和处理货物。

其实上面的代入表述,其实是一个广义的,只是为了更好的理解。

实际当中,会比表述中严谨的多。

如图所示,我们可以看到Java中对于NIO的实现。首先程序会向Selector中注册Channel,及对应Channel中所要关注的事件,并开始轮询与检测这样的多个Channel,如果其中有某个事件状态符合我们所注册的通道事件,那么Selector就会将它作为key集返回给程序,与此同时,类似于读和写这样的事件,就已经将内容存储到了butter中,程序通过key感知到对应事件后,可以直接通过butter去做相应的 *** 作,这里也就是我们提到的非阻塞式,无需等待,但严格意义来将,NIO在无任何事件处理的时候还是处于阻塞等待的状态的,但是有多个事件时,便会针对这多个事件而进行非阻塞的处理了。

1.NIO采用了多路复用模式,利用单个或少数量的线程去接收多个IO请求以此提高程序性能。

2.实现多路复用的核心在于Selector,通过在Selector中注册不同的事件,使Selector利用轮询机制,可以关注、处理不同的请求事件。

3.数据必须通过Channel存储入一个Buffer提供给应用处理。

4.只有程序所关注的事件已经准备好后,Selector才使用户感知,让用户直接进行处理。

ps:这次是我第一次写文章,而且是自己所不太熟悉的部分,所以写的不是很好,主要还是为了能够自己理解,如果其中有错误的地方,希望大家指出。

Java NIO框架MINA用netty性能和链接数、并发等压力测试参数好于mina。

特点:

1。NIO弥补了原来的I/O的不足,它再标准java代码中提供了高速和面向块的I/O

原力的I/O库与NIO最重要的区别是数据打包和传输方式的不同,原来的I/O以流的方式处理数据,而NIO以块的方式处理数据;

2.NIO以通道channel和缓冲区Buffer为基础来实现面向块的IO数据处理,MINA是开源的。

JavaNIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的 *** 作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情,那么改进做法就是开设线程,让线程去等待,但是这样做也是相当耗费资源的。

Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。

Java NIO出现不只是一个技术性能的提高,会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等 *** 作上已经可以和C或Perl等语言并驾齐驱。

如果至今还是在怀疑Java的性能,说明思想和观念已经完全落伍了,Java一两年就应该用新的名词来定义。从JDK1.5开始又要提供关于线程、并发等新性能的支持,Java应用在游戏等适时领域方面的机会已经成熟,Java在稳定自己中间件地位后,开始蚕食传统C的领域。

原理:

NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生。比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。在使用上,也在分两个方向,一个是线程处理,一个是用非线程,后者比较简单。

Tomcat有Connector和Container两大核心组件,Connector组件负责网络请求接入,Connector目前支持BIO、NIO、APR三种模式,后续文章会再重点对比下NIO和APR,Tomcat5以后的版本开始支持NIO了;Container组件实现了对servlet的容器管理功能;service服务将Connector和Container又包了一层,包装成外部可以获取的服务;多有service都运行在Tomcat这个大Server服务上,Server有所有service的实例,并实现了LifeCycle接口可以控制所有service的生命周期。

Tomcat的NIO实现主要是在Connector组件内,Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。

整个Connector组件包含三部分:Http11NioProtocol、Mapper、CoyoteAdapter。Http11NioProtocol包含NioEndpoint和Http11ConnectionHandler,NioEndpoint是Http11NioProtocol中负责接收处理socket的主要模块;Http11ConnectionHandler是连接处理器。NioEndpoint主要是实现了socket请求监听线程Acceptor、socket NIO poller线程、以及请求处理线程池。

NioEndpoint的内部处理流程为:

Acceptor 接收socket线程,这里虽然是基于NIO的connector,但是在接收socket方面还是传统的serverSocket.accept()方式,获得SocketChannel对象,然后封装在一个tomcat的实现类org.apache.tomcat.util.net.NioChannel对象中。然后将NioChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入events queue里。这里是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过queue通信,Acceptor是events queue的生产者,Poller是events queue的消费者。

Poller Poller线程中维护了一个Selector对象,NIO就是基于Selector来完成逻辑的。在connector中并不止一个Selector,在socket的读写数据时,为了控制timeout也有一个Selector,在后面的BlockSelector中介绍。可以先把Poller线程中维护的这个Selector标为主Selector。 Poller是NIO实现的主要线程。首先作为events queue的消费者,从queue中取出PollerEvent对象,然后将此对象中的channel以OP_READ事件注册到主Selector中,然后主Selector执行select *** 作,遍历出可以读数据的socket,并从Worker线程池中拿到可用的Worker线程,然后将socket传递给Worker。整个过程是典型的NIO实现。

Worker Worker线程拿到Poller传过来的socket后,将socket封装在SocketProcessor对象中。然后从Http11ConnectionHandler中取出Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑,跟BIO实现一样。在Worker线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。在从socket中读数据和往socket中写数据的过程,并没有像典型的非阻塞的NIO的那样,注册OP_READ或OP_WRITE事件到主Selector,而是直接通过socket完成读写,这时是阻塞完成的,但是在timeout控制上,使用了NIO的Selector机制,但是这个Selector并不是Poller线程维护的主Selector,而是BlockPoller线程中维护的Selector,称之为辅Selector。

NioSelectorPool NioEndpoint对象中维护了一个NioSelecPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑。以执行servlet后,得到response,往socket中写数据为例,最终写的过程调用NioBlockingSelector的write方法。

对于Acceptor监听到的Socket请求,经过NioEndpoint内部的NIO 线程模型处理后,会转变为SocketProcessor在Executor中运行,其在Run过程中会交给Http11ConnectionHandler处理,Http11ConnectionHandler会从ConcurrentHashMap<NioChannel,Http11NioProcessor>缓存中获取相应的Http11NioProcessor来继续处理,Http11NioProcessor主要是负责解析socket请求Header,解析完成后,会将Request、Response(这里的请求、响应在tomcat中看成是coyote的请求、响应,意思是还需要CoyoteAdaper处理)交给CoyoteAdaper继续处理,CoyoteAdaper这里的工作主要将socket解析的Request、Response转化为HttpServletRequest、HttpServletResponse,而这里的请求响应就是最后交给Container去处理。

同时我们可以看到Acceptor线程会将接受到的SocketChannel(一个socket请求)封装为PollerEvent放到Poller线程中的ConcurrentLinkedQueue<PollerEvent>缓存中,注意到这里的缓存是ConcurrentLinkedQueue是支持并发的,那么在Poller线程的内部,它只需要从这个缓存中不停地获取PollerEvent然后处理就可以了。最后Poller线程处理完成后会封装成SocketProcessor交给NioEndpoint内的线程池Executor去处理。线程池中的Work thread线程在处理SocketProcessor过程中,会调用Http11ConnectionHandler处理,而Http11ConnectionHandler则从ConcurrentHashMap<NioChannel,Http11NioProcessor>缓存中获取相应的Http11NioProcessor来继续处理,这里要注意的ConcurrentHashMap也是支持并发的。

一个或多个Acceptor线程,每个线程都有自己的Selector,Acceptor只负责accept新的连接,一旦连接建立之后就将连接注册到其他Worker线程中

多个Worker线程,有时候也叫IO线程,就是专门负责IO读写的。一种实现方式就是像Netty一样,每个Worker线程都有自己的Selector,可以负责多个连接的IO读写事件,每个连接归属于某个线程。另一种方式实现方式就是有专门的线程负责IO事件监听,这些线程有自己的Selector,一旦监听到有IO读写事件,并不是像第一种实现方式那样(自己去执行IO *** 作),而是将IO *** 作封装成一个Runnable交给Worker线程池来执行,这种情况每个连接可能会被多个线程同时 *** 作,相比第一种并发性提高了,但是也可能引来多线程问题,在处理上要更加谨慎些。tomcat的NIO模型就是第二种。

所以一般参数就是Acceptor线程个数,Worker线程个数。

参考官方文档 https://tomcat.apache.org/tomcat-8.5-doc/config/http.html?spm=5176.100239.blogcont39093.5.Vomyf0

参数主要有以下几个:

1)acceptCount

连接在被ServerSocketChannel accept之前就暂存在这个队列中,acceptCount就是这个队列的最大长度。ServerSocketChannel accept就是从这个队列中不断取出已经建立连接的的请求。所以当ServerSocketChannel accept取出不及时就有可能造成该队列积压,一旦满了连接就被拒绝了;

2)acceptorThreadCount

Acceptor线程只负责从上述队列中取出已经建立连接的请求。在启动的时候使用一个ServerSocketChannel监听一个连接端口如8080,可以有多个Acceptor线程并发不断调用上述ServerSocketChannel的accept方法来获取新的连接。参数acceptorThreadCount其实使用的Acceptor线程的个数

这篇文章从tomcat的整体架构入手,分别介绍了tomcat中的NIO相关类,也介绍了一个网络请求在tomcat中的处理流程,最后介绍了一下tomcat中关键的几个参数对NIO线程模式的作用和影响,相信会对希望了解tomcat nio线程模型的同学会有所帮助。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/11542840.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-16
下一篇 2023-05-16

发表评论

登录后才能评论

评论列表(0条)

保存