Tomcat源码解析如何支持NIO

1 篇文章 0 订阅

Tomcat源码解析


我们前面介绍过了JDK里的NIO,那在实际生产环节又是如何使用的呢?

目前公司实际业务使用的是Tomcat8,我们以Tomcat8为例简单介绍下。

在这里插入图片描述
在Tomcat官网Advanced IO章节有介绍,based on NIO/NIO2,内容相当少。

基于官方文档,我们尝试在tomcat源码中寻找NIO/NIO2相关的类信息:
在这里插入图片描述
经过一番搜寻可以很明显的看到NIO相关的信息。channel,selector,pool经验告诉我们这些都不可能是业务处理的核心逻辑,剩下的就只有这个endPoint了。

先来了解下NioEndPoint的层次结构:
在这里插入图片描述

其父类是AbstractEndpoint,我们来看下这个类提供了哪些方法:
在这里插入图片描述
大家前面如果有看过我们在介绍NIO时的demo,对这些方法一定会感到熟悉,这些方法都有用到,或者都有类似的用到。

我们回顾下前面我们自己手写NIO server端时是如何做的呢?先是定义一个channel,然后绑定 ip 和 端口号,设置为NIO模式,最后启动。
使用多路复用器的时候,在上述基础上定义出一个selector,然后将channel注册到selector。

按照我们的常规理解,NIO的server肯定是先init进行初始化,初始化完成之后再进行init。下面我们以代码debug的模式来看下流程是如何流转的。

我们随机找一个基于Tomcat部署的应用,将应用跑起来,在init和start处都加上断点:
在这里插入图片描述
debug显示,确实是先进行的init,但是由于if判断不通过,什么都没有做,此时的bindState还依旧是默认值:
在这里插入图片描述
所以当流程流转到start方法之后,肯定是可以执行bind方法:
在这里插入图片描述
我们来看下这个bind方法:
在这里插入图片描述
这是一个抽象类,具体实现由子类完成,我们将流程继续往下执行:
在这里插入图片描述
最终这个子类找到的是NioEndpoint,它的bind方法里面,我们看到了熟悉的ServerSocketChannel.open() !!!

相信这段代码大家一定都很熟悉,它和我们自己写的demo过程和逻辑几乎一模一样,定义了channel,绑定了端口号,咦,为啥设置的Blocking是true而不是我们的预期中的false呢,设置为true不是会进行IO堵塞吗?这个先放下,往下面走

方法最后我们看到有一个pool.open方法,这是个啥,我们进去看一眼:
在这里插入图片描述
open方法中,先是将一个变量设为了true,后面紧跟着是调用了getSharedSelector方法。看到这个Selector,大家第一反应是不是会想到:这个是否是我们前面介绍的多路复用器呢?不多说,我们去看下getSharedSelector里面做了什么事情:
在这里插入图片描述
getSharedSelector的方法很简单,如果变量SHARED_SELECTOR为空,则创建一个selector。到这边我们可以肯定,这里面就是用到了我们预期的多路复用器,SHARED_SELECTOR有且只会有一个,方法返回值也是这个新创建的多路复用器,我们称之为“复用器A”吧

下面流程重新回到open方法:
在这里插入图片描述
先是创建出一个NioBlockingSelector,再调用open方法,方法入参还是前面的getSharedSelector()方法,这个我们已经分析过,返回值是那个唯一的"复用器A"。我们再看下blockingSelector.open方法传入这个"复用器A"后做啥事情了:
在这里插入图片描述
可以看到open方法中,将"复用器A"赋值给了一个内部变量,然后new了一个BlockPoller,然后又把这个内部变量赋值给了poller里面的selector变量。绕了一大圈,那这个poller又是个啥?话不多说,上代码:
在这里插入图片描述
可以看到这个poller其实是一个Thread,我们就直接看它的run方法,咦,这代码,怎么跟我们自己写的多路复用器的代码,几乎一模一样呢?所以看到这儿,我们可以认为poller的职责,就是复用器来监听到client连接之后,用于IO读写的,然后它所使用的多路复用器,就是前面创建出来的"复用器A",当open方法里的poller.start(),"复用器A"就开始工作了。

ok,现在我们就已经走完了bind的全部逻辑了。

下面流程再次回转到抽象父类的start方法
在这里插入图片描述
后面跟着的就是startInternal方法,还是把代码跟进去看一下:
在这里插入图片描述
主体逻辑,如果抽象父类的线程池executor对象为空,则创建一个线程池createExecutor():
在这里插入图片描述
那这个线程池大小是多大呢?
在这里插入图片描述
默认的核心线程数是10,最大线程数是200,拒绝策略是LinkedBlockingQueue。

下面流程再次回转到startInternal方法:
在这里插入图片描述
这边的poller具体功能后边再讲。

这边定义了两个poller,每一个poller里面有一个多路复用器,用于处理IO事件。

下面我们再看下startAcceptorThreads方法:
在这里插入图片描述
这个逻辑就更简单了,创建了一个Acceptor,然后启动线程。

我们有必要来看下这个Acceptor做了啥事:
在这里插入图片描述
在Acceptor的代码中,我们看到了熟悉的accept方法,表明Acceptor是用来处理接收客户端连接的。

到这边,我们已经弄明白了,Tomca里面会定义一个单线程的Acceptor,内部持有抽象父类中定义的serverSock,这个serverSock通过bind方法进行创建,同时指定它是blocking的(因为:configureBlocking(true),这边设置的是true)。同时还创建出两个poller,每个poller里面有一个selector用于处理IO。那现在问题来了,根据我们前面多路复用的介绍,当客户端连接到达server之后,连接的这个channel和复用器selector之间需要进行注册,这样复用器才能对这个channel进行IO多路复用。到目前为止还没有看到这个注册的过程,是在哪一步做的呢?

我们将目光放回Acceptor类:
在这里插入图片描述

当NioEndpoint线程处于running状态,并且非中断,在server accept了客户端连接之后,会调用setSocketOptions方法:
在这里插入图片描述
在setSocketOptions方法中我们惊喜的看到了“register“这个关键词,调用者是getPoller0这个方法,照理跟进去:
在这里插入图片描述
很明显,是从上面定义的两个poller中选择了一个出来,然后调用了register方法,方法的入参,是当前client与server连接的channel。
感觉离真相越来越近了,我们再看下这个register方法究竟做了什么,完成了poller中的复用器和连接的channel进行注册:
在这里插入图片描述
我们结合poller 的 register方法和run方法一起分析一下,在run方法中,决定是否需要是否需要进行IO读写的,有两方面因素:

  1. 一是复用器中有事件到达
  2. 或者是events方法中能取到数据。

而在register方法中,获取出来一个PollerEvent对象,而这个对象又是从event栈里面弹出的,如果不存在则new出来了一个PollerEvent,这里面有什么关系呢?

所以我们有必要来看下这个PollerEvent是怎么玩的:
在这里插入图片描述
此时豁然开朗,我们终于找到了外层连接的channel是如何注册 到 poller里面复用器的。

现在我们梳理清楚了server在accept客户端连接之后,怎么通过多路复用器selector完成NIO读取数据的。

现在还有一个问题,我们前面定义了一个线程池,到现在还没有起作用。并且poller读取到数据之后又干了些什么事情呢?
要解决这些问题,肯定还是要回到poller里面研究下在NIO读取到数据之后做了些什么事情:
在这里插入图片描述
在poller中,当有事件到达之后,会执行processKey方法:
在这里插入图片描述
如果当前事件,可读取IO或者可写入IO,都会调用processSocket方法:
在这里插入图片描述
在这个processSocket方法中,我们可以看到先从event中创建出了一个socketProcess对象,然后将该放入我们前面创建出来的线程池进行执行,我们再往下看下这个socketProcess是个啥吧:
在这里插入图片描述
后面代码就不再跟进了,相信大家一定可以想象到,读取出client传输过来的数据,封装成request,后面进入业务逻辑执行得到结果,最后再返回http response。

总结下上面的过程:
在这里插入图片描述

  • Acceptor 接收socket线程,使用传统的serverSocket.accept()方式,获得SocketChannel对象,然后封装在一个tomcat的实现类org.apache.tomcat.util.net.NioChannel对象中。然后将NioChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入events queue。Acceptor和Poller有点类似于消息的生产者和消费者关系
  • Poller Poller线程中维护了一个Selecto。在socket的读写数据时, Poller是NIO实现的主要线程。首先作为events queue的消费者,从queue中取出PollerEvent对象,然后将此对象中的channel以OP_READ事件注册到主Selector中,然后主Selector执行select操作,遍历出可以读数据的socket,并从Worker线程池中拿到可用的Worker线程,然后将socket传递给Worker。
  • Worker Worker线程拿到Poller传过来的socket后,将socket封装在SocketProcessor对象中。在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,也就是我们上面称之为"复用器A"的selector
  • NioSelectorPool NioEndpoint对象中维护了一个NioSelecPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑。以执行servlet后,得到response,往socket中写数据为例,最终写的过程调用NioBlockingSelector的write方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值