Tomcat虽然具有传统Web服务器(如Apache)的功能(处理Html页面,css文件等静态资源的能力),但更多的,Tomcat以Servlet容器著称(处理Jsp和Servlet等动态资源的应用服务器)。由Tomcat的总体架构可知(参见:Tomcat6.x架构概述),Servlet容器由2个主要组件构成:Connector(连接器)和Container(容器)。Connector负责接收客户端的请求,而Container处理并响应该请求。
由JavaEE规范可知,Servlet容器内部只处理HTTP协议的请求,但是对于连接器的设计来说,它可以接收任何协议(如HTTP,AJP等)的请求,因此,在连接器接收到客户端的请求(Socket)后,需要将该请求包装成容器可以处理的对象,然后再传递给容器处理,同时,连接器也要创建一个响应对象一并传给容器,好让容器响应请求。如此一来,容器就与具体传输协议解耦了,而这正是Connector架构所要达到的目的。
基于上面的分析,我们可以将连接器和容器分别抽象为Connector和Container接口,将请求和响应抽象为Request和Response接口。
Connector接口中的重要方法有:
public Request createRequest() ;
public Response createResponse();
public Container getContainer();
Tomcat6.x的设计
下面我们看看Tomcat6.x是如何设计的。我们知道,Tomcat的Servlet容器有个好听的名字叫Catalina,在其内部,Catalina将容器抽象为org.apache.catalina.Container接口,将连接器抽象为org.apache.catalina.connector.Connector类,将请求和响应抽象为org.apache.catalina.connector.Request和org.apache.catalina.connector.Response类。
org.apache.catalina.connector.Connector类也有以下方法:
public Request createRequest() ;
public Response createResponse();
public Container getContainer();
Connector接收到客户端的Socket请求,调用createRequest方法创建请求对象(org.apache.catalina.connector.Request),调用createResponse方法创建响应对象(org.apache.catalina.connector.Response),然后调用getContainer方法获得容器,最后传递Request对象和Response对象并调用Container的invoke方法处理请求。
Connector支持的协议
对于Apache(或其它Web服务器)来说,它可以加载用C语言写的Apache模块(mod_jk , mod_jk2 ,或mod_proxy)作为连接器(Connector),这些连接器(Connector)的工作原理一致,可以根据配置支持不同协议的连接器。对于Tomcat来说,其Servlet容器也有用java语言实现的连接器(Connector)模块,这些模块的工作原理也应该一致并且可以根据配置支持不同的协议。为了实现连接器处理工作的一致性和协议的可配置、可扩展性,在Tomcat6.x中,将一致性的工作抽象到org.apache.catalina.connector.Connector类,而将具体协议的差异抽象到org.apache.coyote.ProtocolHandler接口之中。Tomcat6.x通过在配置文件中设置不同的协议类型来初始化不同的ProtocolHandler实例,从而实现协议的可配置、可扩展性。
在Tomcat6.x中,默认支持2种协议类型的连接器:HTTP/HTTPS协议的HTTP/1.1和AJP协议的AJP/1.3。Tomcat6.x使用Coyote模块来实现Connector框架,因此这些协议处理器(ProtocolHandler)在包org.apache.coyote下可以找到。
注:Coyote是Connector框架实现的名字,在该模块下提供了2种协议类型的ProtocolHandler。
如果想扩展连接器,可以在server.xml文件中指定协议和协议处理器的类名。如下:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
然而,HTTP协议和AJP协议几乎符合所有的需求,因此,我们很少会扩展连接器。
在构造连接器时,连接器根据是否支持Apache Portable Runtime (APR),选择不同的类支持HTTP协议和AJP协议,其对应关系如下:
l 支持APR
HTTP/1.1协议对应org.apache.coyote.http11.Http11AprProtocol类
AJP/1.3协议对应org.apache.coyote.ajp.AjpAprProtocol类
l 不支持APR
HTTP/1.1协议对应org.apache.coyote.http11.Http11Protocol类
AJP/1.3协议对应org.apache.jk.server.JkCoyoteHandler类
注:APR(Apache portable Run-time libraries,Apache可移植运行库)的目的如其名称一样,主要为上层的应用程序提供一个可以跨越多操作系统平台使用的底层支持接口库。在早期 的Apache版本中,应用程序本身必须能够处理各种具体操作系统平台的细节,并针对不同的平台调用不同的处理函数。启用APR将大大提升Tomcat对静态文件的处理性能。
下面看看如何构建不同的连接器:
public Connector(String protocol) throws Exception {
setProtocol(protocol);
// Instantiate protocol handler
try {
Class clazz = Class.forName(protocolHandlerClassName);
this.protocolHandler = (ProtocolHandler) clazz.newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed", e));
}
}
public void setProtocol(String protocol) {
// Test APR support
initializeAPR();
if (aprInitialized) {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
} else {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
}
} else {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11Protocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName("org.apache.jk.server.JkCoyoteHandler");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
}
}
}
Connector构造函数接收一字符串参数protocol,并根据该字符串加载不同的ProtocolHandler。
ProtocolHandler
每个ProtocolHandler代表着一种协议的支持,如Tomcat6.x默认支持的协议http1.1和ajp,相应的处理器为:Http11AprProtocol,AjpAprProtocol,Http11Protocol,AjpProtocol。ProtocolHandler的职责是,接收客户端Socket请求,处理Socket请求,将请求包装之后传给容器来处理。
Coyote架构下,每个ProtocolHandler使用相应的端点类XXXEndpoint(如org.apache.tomcat.util.net.AprEndpoint, org.apache.tomcat.util.net.NioEndpoint,org.apache.tomcat.util.net.JIoEndpoint)监听指定端口并接收Socket请求。在请求传给容器之前,我们需要对请求做一些处理(如是否超时,是否保持连接等),而这些工作都是交给具体协议处理器(ProtocolHandler)的相关类来实现,最后由Adapter包装请求并调用容器的invoke方法。
ProtocolHandler相关类
为了实现协议处理器的相关功能,Coyote按功能职责抽象了协议处理器的相关类:
l XXXEndpoint,监听指定端口,接收Socket请求
l Handler,管理具体协议的Socket请求连接
l Processor,真正的具体协议处理器
l Adapter,包装请求,并调用容器的invoke方法(准确的说是Pipeline的第一个Valve)
在ProtocolHandler相关类的配合下,其处理请求的过程如下:
根据支持的协议,通常ProtocolHandler包含一个实现相应协议的Handler实例和XXXEndpoint实例,Handler实例接收XXXEndpoint 获得的Socket对象,然后创建相应协议的Processor实例,并将Socket请求交给它来处理,最后Processor调用Adapter的service方法,该方法把请求包装后调用容器的invoke方法。
Adapter的service方法签名如下:
public void service(org.apache.coyote.Request req,
org.apache.coyote.Response res)
通过以上的分析,我们发现,Adapter是所有请求进入容器的一个阀。在Tomcat的设计中,阀的使用无处不在。
下面具体分析一下Tomcat6.x是如何支持HTTP/1.1协议的连接器。创建测试代码如下。
代码片段1,创建支持HTTP/1.1的连接器并启动:
public static void main(String[] args) {
try {
Connector connector = new Connector("HTTP/1.1");
connector.setPort(8080);
connector.setContainer(new SimpleContainer());
connector.start();
Thread.sleep(3000000);
} catch (Exception e) {
e.printStackTrace();
}
}
代码片段2,SimpleContainer的实现,省略了部分没有实现的代码:
public class SimpleContainer implements Container {
private Container container;
public SimpleContainer() {
this.container = this;
}
public SimpleContainer(Container container) {
this.container = container;
}
@Override
public Pipeline getPipeline() {
// TODO Auto-generated method stub
return new Pipeline() {
@Override
public Valve getFirst() {
return new Valve() {
@Override
public void invoke(Request request, Response response)
throws IOException, ServletException {
container.invoke(request, response);
}
};
}
@Override
public void invoke(Request request, Response response) throwsIOException, ServletException {
response.setContentType("text/html;charset=gbk");
PrintWriter w = response.getWriter();
Class c = request.getConnector().
getProtocolHandler().getClass();
w.print("当前连接器支持的协议为:" +
request.getConnector().getProtocol());
w.print("<br>");
w.print("当前连接器的类型为:" + c.getName());
w.print("<br>");
w.print("容器方法[invoke]被调用");
}
}
执行该程序,在地址栏输入:http://localhost:8080/index.html,你将看到如下画面:
Connector接收请求的过程如下:
Accepter线程接到客户端的Socket请求后,调用JIoEndpoint的processSocket方法分发请求给worker线程来处理。
Connector处理请求的过程如下:
worker线程接收到Socket请求后,将请求委托给Http11ConnectionHandler处理。Http11ConnectionHandler创建Http11Processor并调用其process方法,该方法解析协议内容并包装请求信息为org.apache.coyote.Request和org.apache.coyote.Response,之后调用CoyoteAdapter的service方法。
Connector传递请求的过程如下:
CoyoteAdapter的service方法内部,将org.apache.coyote.Request和org.apache.coyote.Response转换为org.apache.catalina.connector.Request和org.apache.catalina.connector.Response。然后传递请求给容器。
这几天一直在琢磨Tomcat6的架构,想的很多,收获也颇多。但是一直为一个问题所困扰,学习源码目的是为了什么?熟练配置Tomcat?熟悉servlet/jsp规范?多线程编程?网络编程?OOD?组件编程?我觉得这些都是必然的,但并非必须的,因为在阅读代码的过程中,这些个东西都是很自然的,代码一目了然,但是仁者见仁智者见智,各取所需。后来,再想是不是我们更应该关心Tomcat的架构过程?!也就是说,为什么要这么架构,这么来抽象接口,这么来编织组件?它这么做的好处及不足有哪些?看到了这些,想明白了,我觉得这才是真正的收获。如果想更上一层楼,那么可以总结一个服务器架构的模式,以后可以随手拿来用。如果还想更上一层楼,我觉得应该把你的想法实现了,然后服务器就可以卖钱了!当然,你也可以选择贡献给Apache,改造当前的Tomcat架构,说不定还能以你的名字命名开发版本哦。