vlambda博客
学习文章列表

Tomcat 第五篇:请求处理流程(下)

1. 请求处理流程 AprEndPoint

顺着上一篇接着聊,当一个请求发送到 Tomcat 以后,会由连接器 Connector 转送至 AprEndPoint ,在 AprEndPoint 中调用了 startInternal() 方法,这个方法总共做了做了四件事儿:

  • LimitLatch 限制连接次数。
  • 创建了 poller 线程。
  • 创建了 sendfile 线程。
  • 创建了 acceptor 。

其中, poller 、 sendfile 、 acceptor 都是 AprEndPoint 的内部类,因为他们的父类都实现了 Runnable ,所以核心逻辑都在他们自己的 run() 方法中。

其中的涉及到的源代码太多了,我就是懒得往出列了,所以画了下面这个图给各位做个示意。

Tomcat 第五篇:请求处理流程(下)
  • 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 第五篇:请求处理流程(下)

Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer 。

HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆,而不是直接从内核拷贝到 JVM 堆上。

Tomcat 第五篇:请求处理流程(下)

Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。

Tomcat 第五篇:请求处理流程(下)

2. 请求处理流程 NioEndPoint

前面介绍了 AprEndpoint 的请求处理流程,我们在顺便看下 Tomcat 默认的 NioEndPoint 处理流程。

实际上这两个处理流程非常的相似,区别基本上是因为非阻塞 I/O 的实现方式。

Tomcat 第五篇:请求处理流程(下)
  • 在 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




感谢阅读