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
