深入理解-Tomcat(八)源码剖析之连接器
这是我们分析tomcat的第八篇文章,这次我们分析连接器,我们早就想分析连接器了,因为各种原因拖了好久。不过也确实复杂。
首先我们之前定义过连接器:
Tomcat都是在容器里面处理问题的, 而容器又到哪里去取得输入信息呢? Connector就是专干这个的。 他会把从socket传递过来的数据, 封装成Request, 传递给容器来处理。 通常我们会用到两种Connector,一种叫http connectoer, 用来传递http需求的。 另一种叫AJP, 在我们整合apache与tomcat工作的时候,apache与tomcat之间就是通过这个协议来互动的。 (说到apache与tomcat的整合工作, 通常我们的目的是为了让apache 获取静态资源, 而让tomcat来解析动态的jsp或者servlet。)
简单来说,连接器就是接收http请求并解析http请求,然后将请求交给servlet容器。
那么在 Tomcat中 ,那个类表示连接器呢? 答案是 org.apache.catalina.connector.Connector,该类继承自 LifecycleMBeanBase, 也就是说,该类的生命周期归属于容器管理。而该类的父容器是谁呢? 答案是 org.apache.catalina.core.StandardService,也就是我们的Service 组件,StandardService是该接口的标准实现。StandardService 聚合了 Connector 数组和一个Container 容器,也就验证了我们之前说的一个Service 组件中包含一个Container和多个连接器。
那么连接器什么时候初始化被放入容器和JMX呢?这是个大问题,也是我们今天的主要问题。
1. Tomcat 解析 server.xml 并创建对象
我们之前扒过启动源码,我们知道,在Catalina 的 load 方法中是初始化容器的方法,所有的容器都是在该方法中初始化的。Connector 也不例外。我们还记得 Tomcat 的conf 目录下的server.xml 文件吗?
1 |
|
可以看到该配置文件中有2个Connector 标签,有就是说默认有2个连接器。一个是HTTP协议,一个AJP协议。
我们的Connector是什么时候创建的呢?就是在解析这个xml文件的时候,那么是怎么解析的呢?我们平时在解析 xml 的时候经常使用dom4j(真的不喜欢xml,最爱json),而tomcat 使用的是 Digester 解析xml,我们来看看 Catalina.load() 关于解析并创建容器的关键代码:
1 | public void load() { |
首先创建一个 Digester, 其中的关键代码我们看看:
1 | Digester digester = new Digester(); |
上面的代码的意思是将对应的字符串创建成对应的角色,以便后面和xml对应便解析。
我们再看看它是如何解析的,由于 digester.parse(inputSource) 这个方法调用层次太深,而且该方法只是解析xml,因此楼主把就不把每段代码贴出来了,我们看看IDEA生成的该方法的方法调用栈:这些方法都是在 rt.jar 包中,因此我们不做分析了。主要就是解析xml。
上图是楼主在 Digester.startDocument 的方法中打的断点。该方法作用为开始解析 xml 做准备。
上图是楼主在 Digester.startElement 的方法中打的断点,startDocument 和 startElement 是多次交替执行的,确定他们执行逻辑的是什么方法呢?从堆栈图中我们可以看到:是 com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse()这个方法,代码我就不贴出来了,很长没有意义,该方法在 819行间接调用 startDocument,在841行间接调用startElement。上下执行,并且回执行多次。因为 xml 会解析多次嘛。
我们重点说说 startElement 方法:
1 | public void startElement(String namespaceURI, String localName, |
楼主只贴了关键代码,就是for循环中的逻辑,便利 Rule 集合,Rule 是什么呢?是我们之前 createStartDigester 方法里创建的。而 Rule 是一个接口,tomcat 中有很多不同的实现。然后循环调用他们的 begin 方法。我们看看有哪些实现:
我们从上图中看到了有很多的实现,而我们今天只关注连接器:也就是 ConnectorCreateRule 的 begin 方法:
1 |
|
方法不长,我们看看该方法逻辑,该方法首先从List中取出一个Service,然后会分别创建2个连接器,一个是HTTP, 一个是AJP,也就是我们配置文件中写的。
现在,我们已经剖析了tomcat 是如何解析xml的,并如何创建对象的,接下来,我们就看看创建对象的逻辑。
2. 创建连接器对象
我们来到我们的Connector类的构造方法:
1 | public Connector() { |
我记得阿里规约里说,构造器不要太复杂,复杂的逻辑请放在init里,不知道tomcat这么写算好还是不好呢?嘿嘿。我们还是来看看我们的逻辑吧。
- 根据传进来的字符串设置协议处理器类名(setProtocol 方法中调用了setProtocolHandlerClassName 方法)。
- 根据刚刚设置好的 protocolHandlerClassName 反射创建 ProtocolHandler 类型的对象。
既然是反射创建,那么我们就要看看完整的类名是什么了,所以需要看看设置 protocolHandlerClassName 方法的细节:
1 | public void setProtocol(String protocol) { |
此方法会直接进入下面的else块,我们知道,该处可能会传 HTTP 或者 AJP ,根据不同的协议创建不同的协议处理器。也就是连接器,我们看到这里的全限定名是 org.apache.coyote.http11.Http11Protocol
或者 org.apache.coyote.ajp.AjpProtocol
,这两个类都是在 coyote 包下,也就是连接器模块。
好了,到现在,我们的 Connector 对象就创建完毕了,创建它的过程同时也根据配置文件创建了 protocolHandler, 他俩是依赖关系。
3. Http11Protocol 协议处理器构造过程
创建了 Http11Protocol 对象,我们有必要看看他的构造过程是什么样的。按照tomcat的性格,一般构造器都很复杂,所以,我们找到该类,看看他的类和构造器:
该类的类说明是这样说的:
抽象协议的实现,包括线程等。处理器是单线程的,特定于基于流的协议,不适合像JNI那样的Jk协议。
我们看看构造方法
1 | public Http11Protocol() { |
我就说嘛,肯定和复杂。复杂也要看啊。
- 创建以了一个 JIoEndpoint 对象。
- 创建了一个 Http11ConnectionHandler 对象,参数是 Http11Protocol;
- 设置处理器为 Http11ConnectionHandler 对象。
- 设置一些属性,比如超时,优化tcp性能。
那么我们来看看 JIoEndpoint 这个类,这个类是什么玩意,如果大家平时调试tomcat比较多的话,肯定会熟悉这个类,楼主今天就遇到了,请看:
1 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243) |
异常信息,我们看倒数第四行,就是 JIoEndpoint 的内部类 SocketProcessor 的 run 方法报错了,我们今天就亲密接触一下这个类,顺便扒了它的衣服:
赤裸裸的在我们面前。所有错误的根源都在该方法中。
不扯了,我们继续看 JIoEndpoint 的构造器,该构造器很简单,就是设置最大连接数。默认是0,我们看代码:
上图中什么看到该方法将 maxConnections 设置为0,本来是10000,然后进入else if(maxCon > 0) 的逻辑。这里也就完成了 JIoEndpoint 对象的创建过程。
我们回到 Http11Protocol 的构造方法中,执行完了 JIoEndpoint 的创建过程,下面就执行 Http11ConnectionHandler 的构造。参数是Http11Protocol自己,Http11ConnectionHandler 是 Http11Protocol 的静态内部类,该类中有一个属性就是Http11Protocol,一个简单的创建过程,然后设置 Http11Protocol 的 Handler 属性为 Http11ConnectionHandler。可以感觉的到,Http11Protocol, JIoEndpoint , Http11ConnectionHandler 这三个类是互相依赖关系。
至此,完成了 Http11Protocol 对象的创建。同时也完成了 Connector 对象的创建。 创建完对象干嘛呢。。。。不要想歪了,不是啪啪啪,而是初始化。
4. Connector 连接器的初始化 init 方法
我们知道 Connector 的父容器是 Service ,Service 执行 initInternal 方法初始化的时候会同时初始化子容器,也就是 Connector,在一个 for 循环重启动。
该段代码抽取自 StandardService.initInternal 方法,也就是Service 组件。通过debug我们知道了该连接器数组中只有2个连接器,就是我们的HTTP和AJP,刚刚创建的。并调用他们的 init 方法。我们看看该方法,该方法同所有容器一样,执行了LifecycleBase 的模板方法,重点在子类重写的抽象方法 initInternal 中。
这既是 Connector 的 initInternal 方法实现,该方法有几个步骤:
- 调用父类的 initInternal 方法,将自己注册到JMX中。
- 创建一个 CoyoteAdapter 对象,参数是自己。
- 设置 Http11Protocol 的适配器为刚刚创建的 CoyoteAdapter 适配器。
- 设置解析请求的请求方法类型,默认是 POST。
- 初始化 Http11Protocol(不要小看这个类,Connector就是一个虚的,真正做事的就是这个类和 JIoEndpoint);
- 初始化 mapperListener;
我们重点关注 CoyoteAdapter 和 Http11Protocol 的初始化,CoyoteAdapter 是连接器的一种适配,构造参数是 Connector ,很明显,他是要适配 Connector,这里的设计模式就是适配器模式了,所以,写设计模式的时候,一定要在类名上加上设计模式的名字。方便后来人读代码。接下就是设置 Http11Protocol 的适配器为 刚刚构造的 CoyoteAdapter ,也就是说,tomcat 的设计者为了解耦或者什么将 Http11Protocol 和 Connector 中间插入了一个适配器。最后来到我们的 Http11Protocol 的初始化。
这个 Http11Protocol 的初始化很重要。Http11Protocol 不属于 Lifecycle 管理,他的 init 方法在他的抽象父类 org.apache.coyote.AbstractProtocol 中就已经写好了,我们来看看该方法的实现(很重要):
上图就是 AbstractProtocol 的 init 方法,我们看看红框中的逻辑。
- 将 endpoint 注册到JMX中。
- 将 Http11ConnectionHandler(Http11Protocol 的 内部类)注册到JMX中。
- 设置 endpoint 的名字,Http 连接器是
http-bio-8080
; - endpoint 初始化。
设置JMX的逻辑我们就不讲了,之前讲生命周期的时候讲过了,设置名字也没生命好讲的。最后讲最重要的 endpoint 的初始化。我们来看看他的 init 方法。该方法是 JIoEndpoint 抽象父类 AbstractEndpoint 的模板方法。该类被3个类继承:AprEndpoint, JIoEndpoint, NioEndpoint,我们今天只关心JIoEndpoint。我们还是先看看 AbstractEndpoint 的 init 方法吧:
其中 bind()是抽象方法,然后设置状态为绑定已经初始化。我们看看 JIoEndpoint 的 bind 方法。有兴趣也可以看看其他 Endpoint 的 bind 方法,比如NIO。我们看看JIo的:
这个方法很重要,我们仔细看看逻辑:
- 设置最大线程数,默认是200;
- 创建一个默认的 serverSocketFactory 工厂(就是一个封装了ServerSocket 的类);
- 使用刚刚工厂创建一个 serverSocket。因此,JIoEndpoint 也就有了 serverSocket。
至此,我们完成了 Connector, Http11Protocol,JIoEndpoint 的初始化。
接下来就是启动了
5. 连接器启动
如我们所知,Connector 启动肯定在 startInternal 方法中,因此我们直接看此方法。
该方法步骤如下:
- 设置启动中状态。状态更新会触发事件监听机制。
- 启动 org.apache.coyote.http11.Http11Protocol 的 srart 方法。
- 启动 org.apache.catalina.connector.MapperListener 的 start 方法。
我们感兴趣的是 org.apache.coyote.http11.Http11Protocol 的 srart 方法。该方法由其抽象父类 AbstractProtocol.start 执行,我们看看该方法:
该方法主要逻辑是启动 endpoint 的 start 方法。说明干事的还是 endpoint 啊 ,我们看看该方法实现,该方法调用了抽象父类的模板方法 AbstractEndpoint.start:
其主要逻辑是调用子类重写的 startInternal 方法,我们来看 JIoEndpoint 的实现:
该方法可以说是 Tomcat 中 真正做事情的方法,绝对不是摸鱼员工。说说他的逻辑:
- 创建一个线程阻塞队列,和一个线程池。
- 初始化最大连接数,默认200.
- 调用抽象父类 AbstractEndpoint 的 startAcceptorThreads 方法,默认创建一个守护线程。他的任务是等待客户端请求,并将请求(socket 交给线程池)。AbstractEndpoint 中有一个 Acceptor 数组,作用接收新的连接和传递请求。
- 创建一个管理超时socket 的线程。
让我们看看他的详细实现:
6. JIoEndpoint startInternal(Tomcat socket 管理) 方法的详细实现
先看第一步:创建一个线程阻塞队列,和一个线程池。我们进入该方法:
该方法步骤:
- 创建一个 “任务队列”,实际上是一个继承了 LinkedBlockingQueue
的类。该队列最大长度为 int 最大值 0x7fffffff。 - 创建一个线程工厂,TaskThreadFactory 是一个继承 ThreadFactory 的类,默认创建最小线程 10, 最大线程200, 名字为 “http-bio-8080-exec-” 拼接线程池编号,优先级为5。
- 使用上面的线程工厂创建一个线程池,预先创建10个线程,最大线程200,线程空闲实际60秒.
- 将线程池设置为队列的属性,方便后期判断线程池状态而做一些操作。
再看第二步:初始化最大连接数,默认200.
该方法很简单,就是设置最大连接数为200;
第三步:用抽象父类 AbstractEndpoint 的 startAcceptorThreads 方法,默认创建一个守护线程。他的任务是等待客户端请求,并将请求(socket 交给线程池)。AbstractEndpoint 中有一个 Acceptor 数组,作用接收新的连接和传递请求。我们看看该方法:
步骤:
- 获取可接收的连接数,并创建一个连接线程数组。
- 循环该数组,设置优先级为5,设置为守护线程。
- 启动该线程。
该方法也不是很复杂,获取的这个连接数,在完美初始化的时候,调用bind 方法的时候设置的,请看:
设置为1.
复杂的是 Acceptor 中的逻辑,Acceptor 是一个抽象静态内部类,实现了 Runnable 接口,JIoEndpoint 类中也继承了该类,其中 run 方法如下(高能预警,方法很长)。
1 |
|
其实逻辑也还好,不是那么复杂,我们化整为零,一个一个分析,首先判断状态,进入循环,然后设置一些状态,最后进入一个 try 块。
- 执行 countUpOrAwaitConnection 方法,该方法注释说:如果已经达到最大连接,就等待。
- 阻塞获取socket。
- setSocketOptions(socket) 设置 tcp 一些属性优化性能,比如缓冲字符大小,超时等。
- 执行 processSocket(socket) 方法,将请求包装一下交给线程池执行。
我们看看第一个方法 countUpOrAwaitConnection:
主要是 latch.countUpOrAwait() 这个方法,我们看看该方法内部实现:
这个 Sync 类 变量是 继承了java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类(该类是JDK 1.8 新增的),说实话,楼主不熟悉这个类。再一个今天的主题也不是并发,因此放过这个类,给大家一个链接 深度解析Java 8:AbstractQueuedSynchronizer的实现分析(下);
我们暂时知道这个方法作用是什么就行了,就像注释说的:如果已经达到最大连接,就等待。我们继续我们的分析。
我们跳过设置 tcp 优化,重点查看 processSocket 方法,这个方法是 JIoEndpoint 的,我们看看该方法实现:
该方法逻辑是:
- 封装一个 SocketWrapper。
- 设置长连接时间为100,
- 然后封装成 SocketProcessor(还记得这个类吗,就是我们刚开始异常信息里出现的类,原来是在这里报错的,哈哈) 交给线程池执行。
到这里,我们必须停下来,因为如果继续追踪 SocketProcessor 这个类,这篇文章就停不下来了,楼主想留在下一篇文章慢慢咀嚼。慢慢品味。
第四步:好了,回到我们的 JIoEndpoint.startInternal 方法,我们已经解析完了 startAcceptorThreads 方法,那么我们继续向下走,看到一个 timeoutThread 线程。创建一个管理超时socket 的线程。设置为了守护线程,名字叫 “http-bio-8080-AsyncTimeout”,优先级为5.
我们看看该程序的实现,还好,代码不多:
我们看看主要逻辑:
- 获取当前时间;
- waitingRequests 的类型是 ConcurrentLinkedQueue<SocketWrapper
>,一个并发安全的阻塞对垒,里面有包装过的 SocketWrapper。 - 判断如果该队列中有,则取出,判断如果该socket 设定的超时时间大于0(默认-1),且当前时间大于访问时间,则交给线程池处理。
那么什么时候会往该 waitingRequests 里添加呢?我们看过之前的 SocketProcessor. run 方法, 如果 SocketState 的状态是LONG,就设置该 socket 的访问时间为当前时间,并添加进超时队列。而这个超时的判断也非常的复杂,想想也对,任何一个连接都不能随意丢弃。所以需要更严谨的对待,万一是个支付请求呢?
好了,JIoEndpoint 的 startInternal 方法已经执行完毕,总结一下该方法:创建线程池,初始化最大连接数,启动接收请求线程,设置超时线程。可以说tomcat 考虑的很周到。很完美。不过还有更完美的NIO还没有体验。
7. 总结
今天的文章真是超长啊,谁叫连接器是tomcat中最重要的组件呢?其实还没有讲完呢;我们讲了连接器的如何根据server.xml 创建对象,如何初始化connector 和 endpoint ,如何启动connector,如何启动 endpoint中各个线程。楼主能力有限,暂时先讲这么多,还有很多的东西我们都还没讲,没事,留着下次讲。
天色已晚,楼主该回家了。各位再见!!!!
good luck !!!!