Tomcat 第五篇:请求处理流程(下)
1. 请求处理流程 AprEndPoint
顺着上一篇接着聊,当一个请求发送到 Tomcat 以后,会由连接器 Connector
转送至 AprEndPoint
,在 AprEndPoint
中调用了 startInternal()
方法,这个方法总共做了做了四件事儿:
-
LimitLatch 限制连接次数。 -
创建了 poller 线程。 -
创建了 sendfile 线程。 -
创建了 acceptor 。
其中, poller
、 sendfile
、 acceptor
都是 AprEndPoint
的内部类,因为他们的父类都实现了 Runnable
,所以核心逻辑都在他们自己的 run()
方法中。
其中的涉及到的源代码太多了,我就是懒得往出列了,所以画了下面这个图给各位做个示意。
-
LimitLatch
是连接控制器,它负责控制最大连接数。 -
Acceptor
跑在一个单独的线程中,它在一个死循环里面通过调用accept()
方法来接收新连接,会返回一个 long 类型的socket
,然后将这个socket
封装成AprSocketWrapper
对象。 -
Poller
本身也跑在一个单独的线程中,它早内部维护了一个SocketList
对象,这个对象中含有socket
数组,它在一个死循环里不断检测socket
的数据就绪状态,一旦有socket
可读,就生成一个SocketProcessor
任务对象扔给Executor
去处理。 -
Executor
就是一个线程池,负责运行SocketProcessor
任务类,SocketProcessor
的run()
方法会调用Http11Processor
来读取和解析请求数据。
肯能有的朋友看完了,都不知道 AprEndPoint
或者说 Apr
这种连接模式是什么。
稍微做下简介:
APR(Apache Portable Runtime Libraries)是 Apache 可移植运行时库,它是用 C 语言实现的,其目的是向上层应用程序提供一个跨平台的操作系统接口库。Tomcat 可以用它来处理包括文件和网络 I/O,从而提升性能。
在 Tomcat8.5.x 中,默认的 I/O 模式使用的是 NIO ,使用的链接器是 org.apache.coyote.http11.Http11NioProtocol
,当然,由于是默认的,无需显示配置,在 server.xml
中只需要这么写就可以了:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
但是如果要换成 APR ,就需要这么写了:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
maxThreads="150" SSLEnabled="true" >
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
<SSLHostConfig>
<Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
certificateFile="conf/localhost-rsa-cert.pem"
certificateChainFile="conf/localhost-rsa-chain.pem"
type="RSA" />
</SSLHostConfig>
</Connector>
接下来聊一个拷问灵魂的问题, 「APR 是如何提升性能的?」
跟 NioEndpoint
一样, AprEndpoint
也实现了非阻塞 I/O,它们的区别是:NioEndpoint
通过调用 Java 的 NIO API 来实现非阻塞 I/O,而 AprEndpoint
是通过 JNI 调用 APR 本地库而实现非阻塞 I/O 的。
Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer 。
HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆,而不是直接从内核拷贝到 JVM 堆上。
Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。
2. 请求处理流程 NioEndPoint
前面介绍了 AprEndpoint
的请求处理流程,我们在顺便看下 Tomcat 默认的 NioEndPoint
处理流程。
实际上这两个处理流程非常的相似,区别基本上是因为非阻塞 I/O 的实现方式。
-
在 Acceptor
中的accept()
方法返回一个Channel
对象,接着把Channel
对象交给Poller
去处理。 -
Poller
在内部维护一个Channel
数组,它在一个死循环里不断检测Channel
的数据就绪状态,一旦有Channel
可读,就生成一个SocketProcessor
任务对象扔给Executor
去处理。每个Poller
线程都有自己的Queue
。每个Poller
线程可能同时被多个Acceptor
线程调用来注册PollerEvent
。Poller
不断的通过内部的Selector
对象向内核查询Channel
的状态,一旦可读就生成任务类SocketProcessor
交给Executor
去处理。Poller
的另一个重要任务是循环遍历检查自己所管理的SocketChannel
是否已经超时,如果有超时就关闭这个SocketChannel
。 -
Executor
是线程池,负责运行SocketProcessor
任务类,SocketProcessor
的run()
方法会调用Http11Processor
来读取和解析请求数据。ServerSocketChannel
通过accept()
接受新的连接,accept()
方法返回获得SocketChannel
对象,然后将SocketChannel
对象封装在一个PollerEvent
对象中,并将PollerEvent
对象压入Poller
的Queue
里,这是个典型的生产者 - 消费者模式,Acceptor
与Poller
线程之间通过Queue
通信。
参考
https://jonhuster.blog.csdn.net/article/details/93297251