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