Tomcat支撑高并发的秘密:深入NIO Connector原理




整个tomcat是一个比较完善的框架体系,各个组件之间都是基于接口的实现,所以比较方便扩展和替换。


像这里的

“org.apache.coyote.http11.Http11NioProtocol”


和BIO的

“org.apache.coyote.http11.Http11Protocol”


都是统一的实现

org.apache.coyote.ProtocolHandler接口



所以从整体结构上来说,NIO还是与BIO的实现保持大体一致。首先来看一下NIO connector的内部结构,箭头方向还是消息流。


640?wxfrom=5&wx_lazy=1

还是可以看见connector中三大块


  • Http11NioProtocol

  • Mapper

  • CoyoteAdapter


基本功能与BIO的类似

。重点看看Http11NioProtocol.

和JIoEndpoint一样,NioEndpoint是Http11NioProtocol中负责接收处理socket的主要模块。但是在结构上比JIoEndpoint要复杂一些,毕竟是非阻塞的。但是需要注意的是,tomcat的NIO connector并非完全是非阻塞的,有的部分,例如接收socket,从socket中读写数据等,还是阻塞模式实现的,在后面会逐一介绍。

如图所示,NioEndpoint的主要流程。

0

图中Acceptor及Worker分别是以线程池形式存在,Poller是一个单线程。注意,与BIO的实现一样,缺省状态下,在server.xml中没有配置<Executor>,则以Worker线程池运行,如果配置了<Executor>,则以基于java concurrent 系列的java.util.concurrent.ThreadPoolExecutor线程池运行。

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方法。

java代码

DZ5OKsovm0uulENp5XhZjGSnbONddJblickLhibc0F0SiaWEazJP0LPE6Sk0NDsxUfneUyMI2ANia5QiaOMsFPadmXw

DZ5OKsovm0uulENp5XhZjGSnbONddJblyAs8ib5CMQBqXy7nMTicl315iaSfYicB59OOHnXgZr0lwHMsUV1Fs4tIRw


也就是说当socket.write()返回0时,说明网络状态不稳定,这时将socket注册OP_WRITE事件到辅Selector,由BlockPoller线程不断轮询这个辅Selector,直到发现这个socket的写状态恢复了,通过那个倒数计数器,通知Worker线程继续写socket动作。看一下BlockSelector线程的逻辑。


DZ5OKsovm0uulENp5XhZjGSnbONddJblAHgmBd4G0vLVKTXIf9ia1DjsB0hPpLjKD9vopElCjQgM8lIwaLJ1gxQ

DZ5OKsovm0uulENp5XhZjGSnbONddJblqC3qPKyib9Aic1Ktfd09ibphbficPSJC4ZCvFh2oVJW29T4VLrmBAkoGaw



使用这个辅Selector主要是减少线程间的切换,同时还可减轻主Selector的负担。以上描述了NIO connector工作的主要逻辑,可以看到在设计上还是比较精巧的。NIO connector还有一块就是Comet,有时间再说吧。需要注意的是,上面从Acceptor开始,有很多对象的封装,NioChannel及其KeyAttachment,PollerEvent和SocketProcessor对象,这些不是每次都重新生成一个新的,都是NioEndpoint分别维护了它们的对象池。



DZ5OKsovm0uulENp5XhZjGSnbONddJblmylqwx2BcQ70icB9YTe3U3jkcVDlhPkohN3NxEroQMoZONzu9dzjd3A


当需要这些对象时,分别从它们的对象池获取,当用完后返回给相应的对象池,这样可以减少因为创建及GC对象时的性能消耗。   

猜你喜欢

转载自blog.csdn.net/isharry/article/details/79425451
今日推荐