前言
终于步入 Connector
的解析阶段,这无疑是 Tomcat 架构中最为复杂的一环。作为连接器,它的职责显而易见——连接。那么,它连接的究竟是什么呢?
Connector 宛如一座桥梁,将来自客户端的请求,经过精心封装成 Request 和 Response 对象,传递给 Container 进行处理。Container 完成业务逻辑后,Connector 再将处理后的结果,通过 Response 对象返回给远方的客户端。
要深入理解 Connector 的精髓,需要我们从四个关键问题出发,逐一探索。
- Connector 如何接收来自远方的请求?
- 如何将这呼唤化作 Request 和 Response 的身影?
- 封装后的 Request 和 Response 如何被递交给 Container 处理?
- Container 处理完毕后,如何将结果托付给 Connector,并最终送回客户端手中?
为了更好地理解 Connector 的内部运作,让我们先来欣赏一幅 Connector 结构图,它将帮助我们更直观地感受其内部的精妙设计。
★【注意】:不同的协议和通信方式,将催生出不同的 ProtocolHandler 实现。在 Tomcat 8.5 版本中,ProtocolHandler 的类继承关系图谱如下:
针对这幅类继承层级图,我们可以做如下解读:
ajp 和 http11 代表着两种截然不同的协议,而 nio、nio2 和 apr 则分别代表着三种不同的通信方式。值得注意的是,协议与通信方式并非相互独立,它们可以灵活组合,以适应不同的场景需求。
ProtocolHandler 内部,包含着三个核心部件:Endpoint、Processor 和 Adapter,它们共同协作,完成请求的接收、处理和响应。
- Endpoint 负责处理底层的 Socket 网络连接,它就像是一位网络守卫,负责迎接来自网络的呼唤,并将其转化为可供处理的 Socket 连接。Processor 则肩负着将 Endpoint 接收到的 Socket 封装成 Request 对象的重任,它就像一位翻译官,将网络语言转化为服务器可以理解的语言。Adapter 则充当着连接器,它将 Request 对象传递给 Container,以便 Container 进行具体的处理。
- 由于 Endpoint 负责处理底层的 Socket 网络连接,因此它需要实现 TCP/IP 协议,而 Processor 则需要实现 HTTP 协议,以解析 HTTP 请求。Adapter 则将请求适配到 Servlet 容器,使其能够理解并处理来自外部的请求。
- Endpoint 的抽象实现类 AbstractEndpoint 定义了 Acceptor、AsyncTimeout 两个内部类和一个 Handler 接口。Acceptor 负责监听来自网络的请求,一旦有新的请求到来,便会将其捕获。AsyncTimeout 则负责检查异步 Request 的超时,确保请求在合理的时间内得到处理。Handler 则负责处理接收到的 Socket,它将调用 Processor 进行处理,将 Socket 转换为 Request 对象,并最终传递给 Container。
至此,我们已经解开了 Connector 如何接收请求、如何将请求封装成 Request 和 Response,以及封装后的 Request 和 Response 如何被传递给 Container 进行处理这三个关键问题。而对于最后一个问题,即 Container 处理完后如何将结果返回给客户端,我们将在深入了解 Container 的运作机制后自然明了,前面章节已对此进行了详细的分析。
Connector 源码分析入口
在 Service 的标准实现 StandardService 的源码中,我们发现其 init()、start()、stop() 和 destroy() 方法分别会对 Connectors 的同名方法进行调用。值得注意的是,一个 Service 通常会对应多个 Connector,这意味着 Service 的生命周期管理会影响到所有与其关联的 Connector。
Service.initInternal()
代码语言:javascript复制@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
if (engine != null) {
engine.init();
}
// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// Initialize mapper listener
mapperListener.init();
// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}
Service.startInternal()
代码语言:javascript复制@Override
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);
// Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
mapperListener.start();
// Start our defined Connectors second
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}
正如我们所知,Connector 实现了 Lifecycle 接口,这使得它成为一个拥有生命周期的组件。因此,Connector 的启动逻辑入口自然而然地落在 init() 和 start() 方法之中。
Connector 构造方法
在深入分析 Connector 的启动逻辑之前,不妨先来观摩一下 server.xml 文件。这份文件如同 Tomcat 架构的蓝图,清晰地展现了各个组件之间的联系和布局,为我们理解 Connector 的运作提供了一个宏观的视角。
代码语言:javascript复制<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<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" />
</Host>
</Engine>
</Service>
</Server>
在 server.xml 文件中,我们发现 Connector 拥有多个关键属性,其中 port 和 protocol 尤为重要。默认情况下,server.xml 支持两种协议:HTTP/1.1 和 AJP/1.3。HTTP/1.1 用于支持传统的 HTTP 1.1 协议,而 AJP/1.3 则专门用于支持与 Apache 服务器的通信,为 Apache 服务器提供一个与 Tomcat 交互的桥梁。
现在,让我们将目光转向 Connector 的构造方法:
代码语言:javascript复制public Connector() {
this(null); // 1. 无参构造方法,传入参数为空协议,会默认使用`HTTP/1.1`
}
public Connector(String protocol) {
setProtocol(protocol);
// Instantiate protocol handler
// 5. 使用protocolHandler的类名构造ProtocolHandler的实例
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;
}
}
@Deprecated
public void setProtocol(String protocol) {
boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
AprLifecycleListener.getUseAprConnector();
// 2. `HTTP/1.1`或`null`,protocolHandler使用`org.apache.coyote.http11.Http11NioProtocol`,不考虑apr
if ("HTTP/1.1".equals(protocol) || protocol == null) {
if (aprConnector) {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
} else {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
}
}
// 3. `AJP/1.3`,protocolHandler使用`org.apache.coyote.ajp.AjpNioProtocol`,不考虑apr
else if ("AJP/1.3".equals(protocol)) {
if (aprConnector) {
setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
} else {
setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
}
}
// 4. 其他情况,使用传入的protocol作为protocolHandler的类名
else {
setProtocolHandlerClassName(protocol);
}
}
在 Connector 的构造方法中,我们发现它主要完成了以下几项工作:
- 当传入的参数为空协议时,它会默认使用 HTTP/1.1 协议。
- 当传入的协议为 HTTP/1.1 或 null 时,它会选择 org.apache.coyote.http11.Http11NioProtocol 作为 ProtocolHandler,并忽略 apr 选项。
- 当传入的协议为 AJP/1.3 时,它会选择 org.apache.coyote.ajp.AjpNioProtocol 作为 ProtocolHandler,同样忽略 apr 选项。
- 对于其他情况,它会直接使用传入的 protocol 作为 ProtocolHandler 的类名。
- 最后,它会使用 ProtocolHandler 的类名来构造 ProtocolHandler 的实例。
Connector.initInternal()
代码语言:javascript复制@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// Initialize adapter
// 1. 初始化adapter
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
// Make sure parseBodyMethodsSet has a default
// 2. 设置接受body的method列表,默认为POST
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());
}
}
// 3. 初始化protocolHandler
try {
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}
Connector 的 init() 方法主要完成了三项重要的初始化工作:
- 初始化 adapter:Adapter 负责将请求传递给 Container,因此需要在 init() 方法中完成初始化,以便后续能够正常地将请求传递给 Container 进行处理。
- 设置接受 body 的 method 列表:默认情况下,Connector 只允许 POST 方法提交 body 数据,但在某些情况下,可能需要允许其他方法提交 body 数据,因此需要在 init() 方法中设置允许提交 body 的方法列表。
- 初始化 protocolHandler:ProtocolHandler 是 Connector 的核心组件,负责处理请求和响应,因此需要在 init() 方法中完成 protocolHandler 的初始化,以便后续能够正常地处理请求和响应。
从 ProtocolHandler 的类继承层级关系图
中,我们可以看到 ProtocolHandler
的子类都必须实现 AbstractProtocol
抽象类。而 protocolHandler.init()
; 方法的具体实现则取决于具体的 ProtocolHandler
子类,它会根据不同的协议和通信方式进行相应的初始化操作。
代码正是在这个抽象类里面。我们来分析一下。
代码语言:javascript复制@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);
}
// 1. 设置endpoint的名字,默认为:http-nio-{port}
String endpointName = getName();
endpoint.setName(endpointName.substring(1, endpointName.length()-1));
endpoint.setDomain(domain);
// 2. 初始化endpoint
endpoint.init();
}
接下来,让我们一同探究 Endpoint.init() 方法的内部。它位于 AbstractEndpoint 抽象类中,采用模板方法模式,巧妙地将核心逻辑委托给子类的 bind() 方法。
代码语言:javascript复制public abstract void bind() throws Exception;
public abstract void unbind() throws Exception;
public abstract void startInternal() throws Exception;
public abstract void stopInternal() throws Exception;
public void init() throws Exception {
// 执行bind()方法
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);
ObjectName socketPropertiesOname = new ObjectName(domain
":type=ThreadPool,name="" getName() "",subType=SocketProperties");
socketProperties.setObjectName(socketPropertiesOname);
Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);
for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
registerJmx(sslHostConfig);
}
}
}
继续追寻着代码的踪迹,我们终于来到了 bind()
方法,它揭示了 Connector 初始化的精髓所在。关键的代码片段 serverSock.socket().bind(addr, getAcceptCount())
;用于 将 ServerSocket 绑定到指定的 IP 地址和端口
@Override
public void bind() throws Exception {
if (!getUseInheritedChannel()) {
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
//绑定ServerSocket到指定的IP和端口
serverSock.socket().bind(addr,getAcceptCount());
} else {
// Retrieve the channel provided by the OS
Channel ic = System.inheritedChannel();
if (ic instanceof ServerSocketChannel) {
serverSock = (ServerSocketChannel) ic;
}
if (serverSock == null) {
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
serverSock.configureBlocking(true); //mimic APR behavior
// Initialize thread count defaults for acceptor, poller
if (acceptorThreadCount == 0) {
// FIXME: Doesn't seem to work that well with multiple accept threads
acceptorThreadCount = 1;
}
if (pollerThreadCount <= 0) {
//minimum one poller thread
pollerThreadCount = 1;
}
setStopLatch(new CountDownLatch(pollerThreadCount));
// Initialize SSL if needed
initialiseSsl();
selectorPool.open();
}
至此,我们已将 Connector 的 init()
方法剖析完毕,接下来,让我们将目光转向 start() 方法。start() 方法的核心逻辑,仅仅是简洁的一行代码:调用 ProtocolHandler.start() 方法,将 Connector 的启动大任委托给 ProtocolHandler。
Connector.startInternal()
代码语言:javascript复制@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);
}
}
现在,让我们深入 ProtocolHandler.start()
方法,探索启动过程中的关键步骤。它首先会调用 Endpoint.start()
方法,启动 Endpoint,以便监听来自网络的请求。接着,它会开启异步超时线程,负责监控异步请求的超时情况。该线程的执行单元为 AsyncTimeout。
@Override
public void start() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
}
// 1. 调用`Endpoint.start()`方法
endpoint.start();
// Start async timeout thread
// 2. 开启异步超时线程,线程执行单元为`Asynctimeout`
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,为 Connector 迎接来自网络的请求做好准备。
public final void start() throws Exception {
// 1. `bind()`已经在`init()`中分析过了
if (bindState == BindState.UNBOUND) {
bind();
bindState = BindState.BOUND_ON_START;
}
startInternal();
}
@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
// 2. 创建工作者线程池
if ( getExecutor() == null ) {
createExecutor();
}
// 3. 初始化连接latch,用于限制请求的并发量
initializeConnectionLatch();
// Start poller threads
// 4. 开启poller线程。poller用于对接受者线程生产的消息(或事件)进行处理,poller最终调用的是Handler的代码
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();
}
// 5. 开启acceptor线程
startAcceptorThreads();
}
}
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();
}
}
在 Endpoint.start()
方法中,我们首先会调用 bind()
方法,完成 Socket 的绑定,确保 Connector 能够监听来自网络的请求。接着,我们会创建工作者线程池,为后续处理请求提供充足的线程资源。随后,我们会初始化连接 latch,用于限制请求的并发量,避免过多的请求涌入,造成系统崩溃。
接下来,我们会创建一个轮询 Poller 线程,负责处理来自 Acceptor 线程的事件,并将处理后的事件传递给 Handler。Poller 线程会调用 Handler 的代码进行处理,最终完成对请求的处理。最后,我们会创建一个 Acceptor 线程,专门负责监听网络请求,并将接收到的请求传递给 Poller 线程进行处理。
至此,我们已将 Connector 源码入口的分析告一段落,揭开了 Connector 启动过程的神秘面纱。接下来我们将继续深入探索 Connector 的请求逻辑,深入理解 Connector 如何接收请求,如何将请求封装成 Request 和 Response 对象,以及如何将这些对象传递给 Container 进行处理。让我们一起探索 Tomcat 的内部世界。 欢迎点赞关注,持续更新。
好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。