tomcat8源码分析-Connector初始化
谈起Tomcat的诞生,最早可以追溯到1995年。近20年来,Tomcat始终是使用最广泛的Web服务器,由于其使用Java语言开发,所以广为Java程序员所熟悉。很多人早期的J2EE项目,由程序员自己实现Jsp页面或者Servlet接受请求,后来借助Struts1、Struts2、Spring等中间件后,实际也是利用Filter或者Servlet处理请求,大家肯定要问了,这些Servlet处理的请求来自哪里?Tomcat作为Web服务器是怎样将HTTP请求交给Servlet的呢?
本文就Tomcat对HTTP的请求处理细节进行分析。
提示:阅读本文前,请确保首先理解了《Tomcat源码分析——生命周期管理》中的内容。
CONNECTOR的初始化
根据《Tomcat源码分析——生命周期管理》一文的内容,我们知道Tomcat中有很多容器,包括Server、Service、Connector等。其中Connector正是与HTTP请求处理相关的容器。Service是Server的子容器,而Connector又是Service的子容器。那么这三个容器的初始化顺序为:Server->Service->Connector。Connector的实现分为以下几种:
- Http Connector:基于HTTP协议,负责建立HTTP连接。它又分为BIO Http Connector与NIO Http Connector两种,后者提供非阻塞IO与长连接Comet支持。
- AJP Connector:基于AJP协议,AJP是专门设计用于Tomcat与HTTP服务器通信定制的协议,能提供较高的通信速度和效率。如与Apache服务器集成时,采用这个协议。
- APR HTTP Connector:用C实现,通过JNI调用的。主要提升对静态资源(如HTML、图片、CSS、JS等)的访问性能。现在这个库已独立出来可用在任何项目中。由于APR性能较前两类有很大提升,所以目前是Tomcat的默认Connector。
现在我们直接来看Connector的initInternal方法吧,见代码清单1。
代码清单1
@Override protected void initInternal() throws LifecycleException { super.initInternal(); // Initialize adapter adapter = new CoyoteAdapter(this); protocolHandler.setAdapter(adapter); // Make sure parseBodyMethodsSet has a default if (null == parseBodyMethodsSet) { setParseBodyMethods(getParseBodyMethods()); } if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) { throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr", getProtocolHandlerClassName())); } if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() && protocolHandler instanceof AbstractHttp11JsseProtocol) { AbstractHttp11JsseProtocol<?> jsseProtocolHandler = (AbstractHttp11JsseProtocol<?>) protocolHandler; if (jsseProtocolHandler.isSSLEnabled() && jsseProtocolHandler.getSslImplementationName() == null) { // OpenSSL is compatible with the JSSE configuration, so use it if APR is available jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName()); } } try { protocolHandler.init(); } catch (Exception e) { throw new LifecycleException( sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e); } }
代码清单1说明了Connector的初始化步骤如下:
步骤一 构造网络协议处理的COYOTEADAPTER
代码清单1构造了CoyoteAdapter对象,并且将其设置为ProtocolHandler的Adapter。ProtocolHandler是做什么的呢?Tomcat处理HTTP请求,需要有一个ServerSocket监听网络端口来完成任务。接口ProtocolHandler被设计成控制网络端口监听组件运行,负责组件的生命周期控制,这个接口实际并没有定义网络端口监听功能的规范,而是用于负责维护组件的生命周期。从ProtocolHandler的名字来看,它应该是网络协议的处理者,但它实际不负责这个功能,而是将其交给org.apache.coyote.Adapter来完成,这么设计估计是为了方便维护和拓展新功能。Http11Protocol是ProtocolHandler接口的一个实现(是Connector的默认处理协议),被设计用来处理HTTP1.1网络协议的请求,通过该类可以完成在某个网络端口上面的监听,同时以HTTP1.1的协议来解析请求内容,然后将请求传递到Connector所寄居的Container容器pipeline流水工作线上处理。此处的ProtocolHandler是何时生成的呢?还记得《TOMCAT源码分析——SERVER.XML文件的加载与解析》一文中的Digester和Rule吗?Digester在解析到<Connector>标签的时候,会执行startElement方法,startElement中会调用Rule的begin(String namespace, String name, Attributes attributes)方法,Connector对应的Rule包括ConnectorCreateRule,ConnectorCreateRule的begin方法的实现见代码清单2。
代码清单2
@Override public void begin(String namespace, String name, Attributes attributes) throws Exception { Service svc = (Service)digester.peek(); Executor ex = null; if ( attributes.getValue("executor")!=null ) { ex = svc.getExecutor(attributes.getValue("executor")); } Connector con = new Connector(attributes.getValue("protocol")); if (ex != null) { setExecutor(con, ex); } String sslImplementationName = attributes.getValue("sslImplementationName"); if (sslImplementationName != null) { setSSLImplementationName(con, sslImplementationName); } digester.push(con); }
代码清单2中调用了Connector的构造器,传递的参数为属性protocol。我们知道server.xml中的Connector有两个:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> <!-- Define an AJP 1.3 Connector on port 8009 --> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
我们看看Connector的构造器实现,见代码清单3。
代码清单3
public Connector(String protocol) {
//Deprcated setProtocol(protocol); // Instantiate protocol handler ProtocolHandler p = null; try { Class<?> clazz = Class.forName(protocolHandlerClassName); p = (ProtocolHandler) clazz.getConstructor().newInstance(); } catch (Exception e) { log.error(sm.getString( "coyoteConnector.protocolHandlerInstantiationFailed"), e); } finally { this.protocolHandler = p; } if (Globals.STRICT_SERVLET_COMPLIANCE) { uriCharset = StandardCharsets.ISO_8859_1; } else { uriCharset = StandardCharsets.UTF_8; } }
setProtocol方法已经作废。以HTTP/1.1为例,由于默认情况下Apr不可用,所以protocolHandlerClassName会被设置为"org.apache.coyote.http11.Http11NioProtocol",那么反射生成的protocolHandler就是Http11NioProtocol实例。Tomcat默认还会配置协议是AJP/1.3的Connector,那么此Connector的protocolHandler就是org.apache.coyote.ajp.AjpProtocol。
代码清单5
public void setProtocolHandlerClassName(String protocolHandlerClassName) { this.protocolHandlerClassName = protocolHandlerClassName; }
除此之外,ProtocolHandler还有其它实现,如图1所示。
图1 ProtocolHandler类继承体系
图1中有关ProtocolHandler的实现类都在org.apache.coyote包中 。前面所说的BIO Http Connector实际就是Http11Protocol,NIO Http Connector实际就是Http11NioProtocol,AJP Connector包括AjpProtocol和AjpAprProtocol,APR HTTP Connector包括AjpAprProtocol、Http11AprProtocol,此外还有一个MemoryProtocolHandler(这个是做什么的,目前没搞清楚,有知道的同学告诉我下啊!)。
步骤二 将PROTOCOLHANDLER注册到JMX
BIO Http Connector的ProtocolHandler(即Http11Protocol)的JMX注册名为Catalina:type=ProtocolHandler,port=8080。AJP Connector的ProtocolHandler(即AjpProtocol)的JMX注册名为Catalina:type=ProtocolHandler,port=8009。有关Tomcat中JMX注册的内容,请阅读《TOMCAT源码分析——生命周期管理》一文。
CONNECTOR的启动
根据《Tomcat源码分析——生命周期管理》一文的内容,我们知道Tomcat中有很多容器。ProtocolHandler的初始化稍微有些特殊,Server、Service、Connector这三个容器的初始化顺序为:Server->Service->Connector。值得注意的是,ProtocolHandler作为Connector的子容器,其初始化过程并不是由Connector的initInternal方法调用的,而是与启动过程一道被Connector的startInternal方法所调用。由于本文的目的是分析请求,所以直接从Connector的startInternal方法(见代码清单6)开始。
代码清单6
@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); } }
代码清单6说明了Connector的startInternal方法的执行顺序如下:
- 将Connector容器的状态更改为启动中(LifecycleState.STARTING);
- 启动ProtocolHandler;
初始化PROTOCOLHANDLER
简单起见,我们以AbstractProtocol为例剖析ProtocolHandler的init方法,其实现见代码清单7。
代码清单7
@Override public void init() throws Exception { if (getLog().isInfoEnabled()) { getLog().info(sm.getString("abstractProtocolHandler.init", getName())); } if (oname == null) { // Component not pre-registered so register it oname = createObjectName(); if (oname != null) { Registry.getRegistry(null, null).registerComponent(this, oname, null); } } if (this.domain != null) { rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName()); Registry.getRegistry(null, null).registerComponent( getHandler().getGlobal(), rgOname, null); } String endpointName = getName(); endpoint.setName(endpointName.substring(1, endpointName.length()-1)); endpoint.setDomain(domain); endpoint.init(); }
从代码清单7看到,AbstractProtocol的初始化步骤如下:
步骤一 将PROTOCOLHANDLER注册到JMX
BIO Http Connector的ProtocolHandler(即Http11Protocol)的JMX注册名为Catalina:type=ProtocolHandler,port=8080。BIO Http Connector的MapperListener的注册名为 Catalina:type=Mapper,port=8080。AJP Connector的ProtocolHandler(即AjpProtocol)的JMX注册名为Catalina:type=ProtocolHandler,port=8009。AJP Connector的MapperListener的注册名为Catalina:type=Mapper,port=8009。有关Tomcat中JMX注册的内容,请阅读《TOMCAT源码分析——生命周期管理》一文。
AbstractProtocol的构造器中还设置了提高socket性能的tcpNoDelay等选项,见代码清单8。
代码清单8
public AbstractProtocol(AbstractEndpoint<S> endpoint) {
this.endpoint = endpoint; //Deprecated
setSoLinger(Constants.DEFAULT_CONNECTION_LINGER); setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY); }
步骤二 初始化endpoint,代码清单9是AbstractEndpoint类的init方法
代码清单9
public void init() throws Exception { if (bindOnInit) { bind(); bindState = BindState.BOUND_ON_INIT; } if (this.domain != null) { // Register endpoint (as ThreadPool - historical name) oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\""); Registry.getRegistry(null, null).registerComponent(this, oname, null); for (SSLHostConfig sslHostConfig : findSslHostConfigs()) { registerJmx(sslHostConfig); } } }
启动PROTOCOLHANDLER
我们继续以AbstractProtocol为例,剖析ProtocolHandler的start方法,其实现见代码清单10。
代码清单10
@Override public void start() throws Exception { if (getLog().isInfoEnabled()) { getLog().info(sm.getString("abstractProtocolHandler.start", getName())); } endpoint.start(); // Start async 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方法(见代码清单11)接受请求的创建线程池并创建一定数量的接收请求线程。清单11是endpoint的start方法:
代码清单11
//AbstractEndpoint.java public final void start() throws Exception { if (bindState == BindState.UNBOUND) { bind(); bindState = BindState.BOUND_ON_START; } startInternal();//要调用子类NioEndpoint.java类的实现方法 } //NioEndpoint.java 代码如下: @Override public void startInternal() throws Exception { if (!running) { running = true; paused = false; processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getProcessorCache()); eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getEventCache()); nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getBufferPool()); // Create worker collection if ( getExecutor() == null ) { createExecutor(); } initializeConnectionLatch(); // Start poller threads pollers = new Poller[getPollerThreadCount()]; for (int i=0; i<pollers.length; i++) { pollers[i] = new Poller(); Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i); pollerThread.setPriority(threadPriority); pollerThread.setDaemon(true); pollerThread.start(); } startAcceptorThreads(); } }
从代码清单11看出NioEndpoint的start方法的执行步骤如下:
步骤一 创建线程池与任务队列
如果NioEndpoint尚未处于运行中(即running等于true),才会创建线程池和任务队列。如果尚未创建线程池(即调用getExecutor方法等于null),则需要调用createExecutor方法(见代码清单12)创建线程池和任务队列TaskQueue。
代码清单12
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); }
protected final void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new Acceptor[count]; for (int i = 0; i < count; i++) { acceptors[i] = createAcceptor(); String threadName = getName() + "-Acceptor-" + i; acceptors[i].setThreadName(threadName); Thread t = new Thread(acceptors[i], threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); t.start(); } }
步骤二 创建接收请线程
如果NioEndpoint尚未处于运行中(即running等于true),才会创建接收请求线程。从代码清单11可以看出接收请求线程的数量主要由acceptorThreadCount控制,代码清单9已经告诉我们acceptorThreadCount的默认值为1,但是我们可以通过给Connector增加acceptorThreadCount属性来修改接收请求线程的数量。这些接收请求线程的主要工作由Acceptor完成,Acceptor的实质是一个Runnable,见代码清单13。
代码清单13
public abstract static class Acceptor implements Runnable { public enum AcceptorState { NEW, RUNNING, PAUSED, ENDED } protected volatile AcceptorState state = AcceptorState.NEW; public final AcceptorState getState() { return state; } private String threadName; protected final void setThreadName(final String threadName) { this.threadName = threadName; } protected final String getThreadName() { return threadName; } }
最后初始化MAPPERLISTENER
MapperListener继承LifecycleMBeanBase,它的startInternal方法用于初始化,见代码清单14。
代码清单14
@Override public void startInternal() throws LifecycleException { setState(LifecycleState.STARTING); Engine engine = service.getContainer(); if (engine == null) { return; } findDefaultHost(); addListeners(engine); Container[] conHosts = engine.findChildren(); for (Container conHost : conHosts) { Host host = (Host) conHost; if (!LifecycleState.NEW.equals(host.getState())) { // Registering the host will register the context and wrappers registerHost(host); } } }
从代码清单14看到MapperListener的初始化步骤如下:
步骤一 查找默认Host
StandardService的子容器包括:StandardEngine、Connector和Executor。MapperListener本身会持有Connector,所以可以通过各个容器的父子关系,找到Connector的同级容器StandardEngine。StandardHost是StandardEngine的子容器,Engine和Host的默认配置如下:
<Engine name="Catalina" defaultHost="localhost"> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b" resolveHosts="false"/> </Host> </Engine>
findDefaultHost方法(见代码清单15)可以获取上面配置中的默认Host,Engine元素的defaultHost属性值必须要与配置的某个Host元素的name属性值相同。如果defaultHost的属性值配置无误,则会添加为MapperListener的Mapper对象属性的defaultHostName。
代码清单15
private void findDefaultHost() { Engine engine = service.getContainer(); String defaultHost = engine.getDefaultHost(); boolean found = false; if (defaultHost != null && defaultHost.length() >0) { Container[] containers = engine.findChildren(); for (Container container : containers) { Host host = (Host) container; if (defaultHost.equalsIgnoreCase(host.getName())) { found = true; break; } String[] aliases = host.findAliases(); for (String alias : aliases) { if (defaultHost.equalsIgnoreCase(alias)) { found = true; break; } } } } if(found) { mapper.setDefaultHostName(defaultHost); } else { log.warn(sm.getString("mapperListener.unknownDefaultHost", defaultHost, service)); } }
步骤二 将Host及其子容器Context,Context的子容器Wrapper注册到MapperListener的Mapper对象
Mapper的数据结构,见代码清单16。
代码清单16
public final class Mapper { private static final Log log = LogFactory.getLog(Mapper.class); private static final StringManager sm = StringManager.getManager(Mapper.class); // ----------------------------------------------------- Instance Variables /** * Array containing the virtual hosts definitions. */ // Package private to facilitate testing volatile MappedHost[] hosts = new MappedHost[0]; /** * Default host name. */ private String defaultHostName = null; private volatile MappedHost defaultHost = null; /** * Mapping from Context object to Context version to support * RequestDispatcher mappings. */ private final Map<Context, ContextVersion> contextObjectToContextVersionMap = new ConcurrentHashMap<>();
// ------------------------------------------------------- Host Inner Class protected static final class MappedHost extends MapElement<Host> { public volatile ContextList contextList; /** * Link to the "real" MappedHost, shared by all aliases. */ private final MappedHost realHost; /** * Links to all registered aliases, for easy enumeration. This field * is available only in the "real" MappedHost. In an alias this field * is <code>null</code>. */ private final List<MappedHost> aliases; /** * Constructor used for the primary Host * * @param name The name of the virtual host * @param host The host */ public MappedHost(String name, Host host) { super(name, host); realHost = this; contextList = new ContextList(); aliases = new CopyOnWriteArrayList<>(); } /** * Constructor used for an Alias * * @param alias The alias of the virtual host * @param realHost The host the alias points to */ public MappedHost(String alias, MappedHost realHost) { super(alias, realHost.object); this.realHost = realHost; this.contextList = realHost.contextList; this.aliases = null; } public boolean isAlias() { return realHost != this; } public MappedHost getRealHost() { return realHost; } public String getRealHostName() { return realHost.name; } public Collection<MappedHost> getAliases() { return aliases; } public void addAlias(MappedHost alias) { aliases.add(alias); } public void addAliases(Collection<? extends MappedHost> c) { aliases.addAll(c); } public void removeAlias(MappedHost alias) { aliases.remove(alias); } } // ------------------------------------------------ ContextList Inner Class protected static final class ContextList { public final MappedContext[] contexts; public final int nesting; public ContextList() { this(new MappedContext[0], 0); } private ContextList(MappedContext[] contexts, int nesting) { this.contexts = contexts; this.nesting = nesting; } public ContextList addContext(MappedContext mappedContext, int slashCount) { MappedContext[] newContexts = new MappedContext[contexts.length + 1]; if (insertMap(contexts, newContexts, mappedContext)) { return new ContextList(newContexts, Math.max(nesting, slashCount)); } return null; } public ContextList removeContext(String path) { MappedContext[] newContexts = new MappedContext[contexts.length - 1]; if (removeMap(contexts, newContexts, path)) { int newNesting = 0; for (MappedContext context : newContexts) { newNesting = Math.max(newNesting, slashCount(context.name)); } return new ContextList(newContexts, newNesting); } return null; } } // ---------------------------------------------------- Context Inner Class protected static final class MappedContext extends MapElement<Void> { public volatile ContextVersion[] versions; public MappedContext(String name, ContextVersion firstVersion) { super(name, null); this.versions = new ContextVersion[] { firstVersion }; } } protected static final class ContextVersion extends MapElement<Context> { public final String path; public final int slashCount; public final WebResourceRoot resources; public String[] welcomeResources; public MappedWrapper defaultWrapper = null; public MappedWrapper[] exactWrappers = new MappedWrapper[0]; public MappedWrapper[] wildcardWrappers = new MappedWrapper[0]; public MappedWrapper[] extensionWrappers = new MappedWrapper[0]; public int nesting = 0; private volatile boolean paused; public ContextVersion(String version, String path, int slashCount, Context context, WebResourceRoot resources, String[] welcomeResources) { super(version, context); this.path = path; this.slashCount = slashCount; this.resources = resources; this.welcomeResources = welcomeResources; } public boolean isPaused() { return paused; } public void markPaused() { paused = true; } } // ---------------------------------------------------- Wrapper Inner Class protected static class MappedWrapper extends MapElement<Wrapper> { public final boolean jspWildCard; public final boolean resourceOnly; public MappedWrapper(String name, Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) { super(name, wrapper); this.jspWildCard = jspWildCard; this.resourceOnly = resourceOnly; } }
根据代码清单16,我们知道Mapper中维护着一个MappedHost数组,每个MappedHost中有一个ContextList,这个ContextList中维护着一个Context数组。每个Context维护着一个defaultWrapper,三个Wrapper数组(exactWrappers、wildcardWrappers、extensionWrappers)。下面对Host、Context及Wrapper进行功能上的介绍:
- Host:代表一个虚拟主机,各Host的name不能相同,appBase代表各虚拟主机的应用发布位置;
- Context:代表一个应用,Context可以根据应用的/WEB-INF/web.xml文件中定义的servlet来处理请求。一个Host下可以有多个Context;
- Wrapper: 代表一个Servlet或者jsp,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。
以我本地为例,在MapperListener类中的startInternal方法中的最后打个断点,并运行debugger模式,就可以看到注册到Mapper中的Host及其子容器,如图2所示。
图2 注册到Mapper中的Host及其Context子容器
图2说明Host内一共5个Context,由于我的Tomcat是从svn拉下来的,所以webapps目录下的.svn文件夹也是一个Context,除了这个天外来客,我将其它与请求有关的容器整理后用图3来展示。
图3 我本地的Host、Context及Wrapper
可以看docs这个context的所有属性,如下图:
至此,Tomcat中为请求处理的准备工作已经完成。有关请求的处理过程请继续阅读《Tomcat源码分析——请求原理分析(中)》一文。
本文原创首发:博客园,原文链接:http://www.cnblogs.com/jiaan-geng/p/4875249.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
2017-03-29 HTTP协议之http状态码详解