Tomcat 第四篇:请求处理流程(上)

1. 引言
既然是在讲 Tomcat ,那么一个 HTTP 请求的请求流程是无论如何也绕不开的。
首先抛开所有,使用我们现有的知识面,猜测一下一个请求被 Tomcat 处理的过程:
1. 客户端(浏览器)发送一个请求(HTTP)
2. 建立 Socket 连接
3. 通过 Socket 读取数据
4. 根据协议(HTTP)解析请求
5. 调用对应的代码完成响应
上面这套流程,我相信任何一个 Java 码农都能想得到,当 Tomcat 接受到请求后,经过一系列的基础处理,最终会调用到我们自己的业务程序上,或者说是 Servlet 上,在早期,这些请求会由我们自己实现的 jsp 或者是 Servlet 进行接收,随着时代的发展以及演进,出现了 Struts 和 Spring 等中间件来帮助我们完成基础的请求处理,使得开发人员更加关注具体的业务。
我想很多人都很好奇, Tomcat 是如何将这些 HTTP 请求转交给我们的 Servlet 的?
2. Connector 初始化
上一篇我们在聊 Tomcat 启动流程的时候,最后执行初始化的是 org.apache.catalina.connector.Connector#initInternal() ,这时整个初始化流程到了 Connector ,看一下这段代码:
// 去除部分代码
protected void initInternal() throws LifecycleException {
super.initInternal();
// Initialize adapter
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
// ......
try {
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}
这段代码中主要做了两件事情:
- 构造了 CoyoteAdapter 对象,并且将其设置为 ProtocolHandler 的 Adapter 。
- 调用了
org.apache.coyote.ProtocolHandler#init()的方法。
先说第二件事情,调用了 org.apache.coyote.ProtocolHandler#init() ,ProtocolHandler 是在构造方法中进行的初始化,这里的核心代码是:
setProtocol(protocol)
再看下 setProtocol() 这个方法做了啥:
public void setProtocol(String protocol) {
boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
AprLifecycleListener.getUseAprConnector();
if ("HTTP/1.1".equals(protocol) || protocol == null) {
if (aprConnector) {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
} else {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
}
} else if ("AJP/1.3".equals(protocol)) {
if (aprConnector) {
setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
} else {
setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
}
} else {
setProtocolHandlerClassName(protocol);
}
}
看到这里可以知道,主逻辑分成了两块,一块是使用 HTTP/1.1 协议,另一块是使用了 AJP/1.3 的协议,这里通过协议的不同,最终初始化了不同的类。
如果是使用 HTTP/1.1 的协议,则采用了 org.apache.coyote.http11.Http11AprProtocol 或者 org.apache.coyote.http11.Http11NioProtocol,如果是采用 AJP/1.3 则采用 org.apache.coyote.ajp.AjpAprProtocol 或者是 org.apache.coyote.ajp.AjpNioProtocol 。
看下 org.apache.coyote.http11.Http11AprProtocol 和 org.apache.coyote.ajp.AjpAprProtocol 继承关系图:


可以看到这两个类都继承自 org.apache.coyote.AbstractProtocol ,通过查看 org.apache.coyote.AbstractProtocol#init() 方法,可以看到是调用了 org.apache.tomcat.util.net.AbstractEndpoint#init() ,而 AbstractEndpoint 的实例化操作是在实例化 AjpProtocol 和 Http11Protocol 的时候在其构造函数中实例化的,而在 AjpProtocol 和 Http11Protocol 的构造函数中,实际上是都初始化了 org.apache.tomcat.util.net.JIoEndpoint ,只不过根据不同的 HTTP 或者是 AJP 协议,它们具有不同的连接处理类。其中 Http11Protocol 的连接处理类为 org.apache.coyote.http11.Http11Protocol.Http11ConnectionHandler ,而连接处理类为 org.apache.coyote.ajp.AjpProtocol.AjpConnectionHandler 。
除此之外, ProtocolHandler 还有其他实现,都在 org.apache.coyote 这个包中,一些常见的类图如下:

因此到这里我们基本清楚了 Connector 的初始化流程,总结如下:
//1 HTTP/1.1协议连接器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.http11.Http11AprProtocol#init
-->org.apache.tomcat.util.net.AprEndpoint#init
(org.apache.coyote.http11.Http11AprProtocol.Http11ConnectionHandler)
// 2 AJP/1.3协议连接器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.ajp.AjpAprProtocol#init
-->org.apache.tomcat.util.net.AprEndpoint#init
(org.apache.coyote.ajp.AjpAprProtocol.AjpConnectionHandler)
3. Connector 启动
ProtocolHandler 的初始化稍微有些特殊,Server、Service、Connector 这三个容器的初始化顺序为: Server -> Service -> Connector 。值得注意的是, ProtocolHandler 作为 Connector 的子容器,其初始化过程并不是由 Connector 的 initInternal 方法调用的,而是与启动过程一道被 Connector 的 startInternal 方法所调用。
@Override
protected void startInternal() throws LifecycleException {
// Validate settings before starting
if (getPort() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPort())));
}
setState(LifecycleState.STARTING);
try {
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}
这里也总共做了两件事儿:
- 将 Connector 容器的状态更改为启动中(LifecycleState.STARTING) 。
- 启动 ProtocolHandler 。
简单起见,以 Http11Protocol 为例介绍 ProtocolHandler 的 start 方法:
由于 ProtocolHandler 是一个接口,它的 start 方法有两个抽象类进行实现:
- org.apache.coyote.AbstractAjpProtocol
- org.apache.coyote.ajp.AbstractProtocol
这里我们仅讨论 org.apache.coyote.ajp.AbstractProtocol ,看下它的 start 的方法:
@Override
public void start() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
}
endpoint.start();
// Start timeout thread
asyncTimeout = new AsyncTimeout();
Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
int priority = endpoint.getThreadPriority();
if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
priority = Thread.NORM_PRIORITY;
}
timeoutThread.setPriority(priority);
timeoutThread.setDaemon(true);
timeoutThread.start();
}
这里最核心的一句代码是调用了 endpoint.start() ,这里的 endpoint 是抽象类 AbstractEndpoint#start() :
public final void start() throws Exception {
if (bindState == BindState.UNBOUND) {
bind();
bindState = BindState.BOUND_ON_START;
}
startInternal();
}
这一段也做了两件事儿:
- 判断当前绑定状态,如果没有绑定,则会先去绑定,调用
bind()。 - 然后调用
startInternal()进行初始化。
AbstractEndpoint 有三个子类:

我们专注于 AprEndpoint 这个子类,上面 AbstractEndpoint 抽象类中的两个方法 bind() 和 startInternal() 都会在这个类中进行实现。
我们先看 bind() 方法:
/**
* Initialize the endpoint.
*/
@Override
public void bind() throws Exception {
// Create the root APR memory pool
try {
rootPool = Pool.create(0);
} catch (UnsatisfiedLinkError e) {
throw new Exception(sm.getString("endpoint.init.notavail"));
}
// Create the pool for the server socket
serverSockPool = Pool.create(rootPool);
// Create the APR address that will be bound
String addressStr = null;
if (getAddress() != null) {
addressStr = getAddress().getHostAddress();
}
int family = Socket.APR_INET;
if (Library.APR_HAVE_IPV6) {
if (addressStr == null) {
if (!OS.IS_BSD) {
family = Socket.APR_UNSPEC;
}
} else if (addressStr.indexOf(':') >= 0) {
family = Socket.APR_UNSPEC;
}
}
long inetAddress = Address.info(addressStr, family,
getPort(), 0, rootPool);
// Create the APR server socket
serverSock = Socket.create(Address.getInfo(inetAddress).family,
Socket.SOCK_STREAM,
Socket.APR_PROTO_TCP, rootPool);
if (OS.IS_UNIX) {
Socket.optSet(serverSock, Socket.APR_SO_REUSEADDR, 1);
}
if (Library.APR_HAVE_IPV6) {
if (getIpv6v6only()) {
Socket.optSet(serverSock, Socket.APR_IPV6_V6ONLY, 1);
} else {
Socket.optSet(serverSock, Socket.APR_IPV6_V6ONLY, 0);
}
}
// Deal with the firewalls that tend to drop the inactive sockets
Socket.optSet(serverSock, Socket.APR_SO_KEEPALIVE, 1);
// Bind the server socket
int ret = Socket.bind(serverSock, inetAddress);
if (ret != 0) {
throw new Exception(sm.getString("endpoint.init.bind", "" + ret, Error.strerror(ret)));
}
// Start listening on the server socket
ret = Socket.listen(serverSock, getAcceptCount());
if (ret != 0) {
throw new Exception(sm.getString("endpoint.init.listen", "" + ret, Error.strerror(ret)));
}
if (OS.IS_WIN32 || OS.IS_WIN64) {
// On Windows set the reuseaddr flag after the bind/listen
Socket.optSet(serverSock, Socket.APR_SO_REUSEADDR, 1);
}
// Enable Sendfile by default if it has not been configured but usage on
// systems which don't support it cause major problems
if (!useSendFileSet) {
setUseSendfileInternal(Library.APR_HAS_SENDFILE);
} else if (getUseSendfile() && !Library.APR_HAS_SENDFILE) {
setUseSendfileInternal(false);
}
// Initialize thread count default for acceptor
if (acceptorThreadCount == 0) {
// FIXME: Doesn't seem to work that well with multiple accept threads
acceptorThreadCount = 1;
}
// Delay accepting of new connections until data is available
// Only Linux kernels 2.4 + have that implemented
// on other platforms this call is noop and will return APR_ENOTIMPL.
if (deferAccept) {
if (Socket.optSet(serverSock, Socket.APR_TCP_DEFER_ACCEPT, 1) == Status.APR_ENOTIMPL) {
deferAccept = false;
}
}
// Initialize SSL if needed
if (isSSLEnabled()) {
for (SSLHostConfig sslHostConfig : sslHostConfigs.values()) {
createSSLContext(sslHostConfig);
}
SSLHostConfig defaultSSLHostConfig = sslHostConfigs.get(getDefaultSSLHostConfigName());
if (defaultSSLHostConfig == null) {
throw new IllegalArgumentException(sm.getString("endpoint.noSslHostConfig",
getDefaultSSLHostConfigName(), getName()));
}
Long defaultSSLContext = defaultSSLHostConfig.getOpenSslContext();
sslContext = defaultSSLContext.longValue();
SSLContext.registerDefault(defaultSSLContext, this);
// For now, sendfile is not supported with SSL
if (getUseSendfile()) {
setUseSendfileInternal(false);
if (useSendFileSet) {
log.warn(sm.getString("endpoint.apr.noSendfileWithSSL"));
}
}
}
}
这个方法上面的注释已经写的比较清楚了,首先第一个方法注释就告诉我们这个方法是用来初始化 endpoint 的,大体做了这么几件事儿:
- 创建了 APR 的 rootPool ,从命名上看这应该是一个根连接池。
- 创建一个 serverSockPool ,使用刚才创建的 rootPool 进行创建,这个命名大家就都看得懂了。
- 创建用来做绑定的 APR 的地址。
- 创建一个 APR server socket -> serverSock ,这里开启了 socket 。
- 将刚才创建的 server 和 socket 进行绑定。
- 开启 server socket 上面的监听。
- 一些系统层面的设置。
- 如果需要的话,还会进行一些 SSL 的相关设置。
接着看下 startInternal() 方法:
/**
* Start the APR endpoint, creating acceptor, poller and sendfile threads.
*/
@Override
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
poller.init();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
// Start sendfile thread
if (getUseSendfile()) {
sendfile = new Sendfile();
sendfile.init();
Thread sendfileThread =
new Thread(sendfile, getName() + "-Sendfile");
sendfileThread.setPriority(threadPriority);
sendfileThread.setDaemon(true);
sendfileThread.start();
}
startAcceptorThreads();
}
}
这个方法所有的前提条件都在于如果 AprEndpoint 尚未出于运行中,即 running == true ,首先如果没有创建线程池 getExecutor() == null ,则需要调用 createExecutor() 方法创建线程池和任务队列 TaskQueue 。
public void createExecutor() {
internalExecutor = true;
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
剩下两个是创建了两个线程,分别是 poller 和 sendfile ,这两个都是 AprEndpoint 的内部类,这两个线程一个是用来做轮询,另一个是用来做数据发送。
至此, Tomcat 中为请求处理的准备工作已经完成。
Tomcat 第四篇:请求处理流程(上)的更多相关文章
- Go第四篇之流程控制
流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”. Go 语言的常用流程控制有 if 和 for,而 switch 和 goto 主要是为了简化代码.降低重复 ...
- shell之路 shell核心语法【第四篇】流程控制
if语句 if ... fi 语句: if ... else ... fi 语句: if ... elif ... else ... fi 语句. 注意: expression 和方括号([ ])之间 ...
- Tomcat 第五篇:请求处理流程(下)
1. 请求处理流程 AprEndPoint 顺着上一篇接着聊,当一个请求发送到 Tomcat 以后,会由连接器 Connector 转送至 AprEndPoint ,在 AprEndPoint 中调用 ...
- 基于GBT28181:SIP协议组件开发-----------第四篇SIP注册流程eXosip2实现(一)
原创文章,引用请保证原文完整性,尊重作者劳动,原文地址http://www.cnblogs.com/qq1269122125/p/3945294.html. 上章节讲解了利用自主开发的组件SIP组件l ...
- JDownload: 一款可以从网络上下载文件的小程序第四篇(整体架构描述)
一 前言 时间过得真快,距离本系列博客第一篇的发布已经过去9个月了,本文是该系列的第四篇博客,将对JDownload做一个整体的描述与介绍.恩,先让笔者把记忆拉回到2017年年初,那会笔者在看Unix ...
- shell第四篇(上)
第四篇了解Shell 命令执行流程图 {网中人大哥推荐参考Learning the Bash Shell, 2nd Edition,第 178页:中文版229页} Shell 从标准输入或脚本中读取的 ...
- 史上最简单的SpringCloud教程 | 第四篇:断路器(Hystrix)
在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用.为了保证其高可用,单个服务 ...
- 史上最简单的SpringCloud教程 | 第四篇:断路器(Hystrix)(Finchley版本)
转载请标明出处: 原文首发于:https://www.fangzhipeng.com/springcloud/2018/08/30/sc-f4-hystrix/ 本文出自方志朋的博客 在微服务架构中, ...
- ASP.NET Core管道深度剖析(2):创建一个“迷你版”的管道来模拟真实管道请求处理流程
从<ASP.NET Core管道深度剖析(1):采用管道处理HTTP请求>我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成,所以从总体设计来讲是非常简单的,但 ...
随机推荐
- 第4篇 Scrum 冲刺博客
1.站立会议 照骗 进度 成员 昨日完成任务 今日计划任务 遇到的困难 钟智锋 重新设计项目执行流程 实现技能 庄诗楷 游戏窗口的制作 制作了开始游戏的界面,感觉还不错 无 易德康 完成所有棋子的移动 ...
- CentOS 6.x/7.x上安装git
yum安装 # yum info git # yum install -y git 可以通过下面的命令来检查是否安装了git环境 git --version 参考:如何在CentOS 6.x/7.x上 ...
- struts中的一些问题
QueryRunner()方法内要传数据源
- idea 执行maven打包命令时,修改war包名称
- openCV - 2. 矩阵的掩膜操作
获取图像像素指针.掩膜操作解释 获取图像像素指针 CV_Assert(myImage.depth() == CV_8U); Mat.ptr<uchar>(int i=0) 获取像素矩阵的指 ...
- springBoot整合spring security+JWT实现单点登录与权限管理--筑基中期
写在前面 在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与 ...
- linux的五种IO模型
概念: 同步.异步.阻塞.非阻塞的概念 同步:所谓同步,发起一个功能调用的时候,在没有得到结果之前,该调用不返回,也就是必须一件事一件事的做,等前一件做完了,才能做下一件. 提交请求->等待服务 ...
- Java远程连接Linux服务器并执行命令及上传文件
最近再开发中遇到需要将文件上传到Linux服务器上,至此整理代码笔记. 此种连接方法中有考虑到并发问题,在进行创建FTP连接的时候将每一个连接对象存放至 ThreadLocal<Ftp> ...
- nginx 启动报错“var/run/nginx/nginx.pid" no such file or directory
nginx 启动报错“var/run/nginx/nginx.pid" no such file or directory 今天刚搭建的nginx服务器启动时,报错“var/run/ngin ...
- Oracle的timestamp字段更新实验 结论:只有逐条更新才能保证timestamp字段有差别,批量更新只会得到一致的时间,此操作无关时间精度.
有这么一张表: create table hy_testtime( id number(6,0) not null primary key, name nvarchar2(20) not null, ...