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 的。

Tomcat 的 Endpoint 组件在接收网络数据时需要预先分配好一块 Buffer,所谓的 Buffer 就是字节数组 byte[] ,Java 通过 JNI 调用把这块 Buffer 的地址传给 C 代码,C 代码通过操作系统 API 读取 Socket 并把数据填充到这块 Buffer。
Java NIO API 提供了两种 Buffer 来接收数据: HeapByteBuffer 和 DirectByteBuffer 。
HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆,而不是直接从内核拷贝到 JVM 堆上。
数据从内核拷贝到 JVM 堆的过程中,JVM 可能会发生 GC , GC 过程中对象可能会被移动,也就是说 JVM 堆上的字节数组可能会被移动,这样的话 Buffer 地址就失效了。如果这中间经过本地内存中转,从本地内存到 JVM 堆的拷贝过程中 JVM 可以保证不做 GC。

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通信。
参考
作者:极客挖掘机
定期发表作者的思考:技术、产品、运营、自我提升等。
本文版权归作者极客挖掘机和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果您觉得作者的文章对您有帮助,就来作者个人小站逛逛吧:极客挖掘机
                    
                
                
            
        
浙公网安备 33010602011771号