Tomcat——总体架构解析
Tomcat
是基于Java
语言的轻量级应用服务器,是一款完全开源Servlet
容器实现。同时,它支持HTML、JS
等静态资源的处理,因此又可以作为轻量级Web
服务器使用。
因此 Tomcat
就是一个HTTP
服务器 + Servlet
容器,也叫Web
容器。
HTTP 服务器的实现
我们先用Java
来实现一个很简单的HTTP
服务器,就是接收浏览器请求,根据不同的请求路径返回对应的信息。
public static void main(String[] args) {
try {
//监听端口号8080
ServerSocket ss = new ServerSocket(8080);
while (true) {
Socket socket = ss.accept();
//获取客户端请求信息
BufferedReader bd = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requestHeader;
while ((requestHeader = bd.readLine()) != null && !requestHeader.isEmpty()) {
if (requestHeader.startsWith("GET")) {
//获取请求路径
String url = requestHeader.split(" ")[1];
if (url.contains("/test")){
responseHeader (socket,"success");
}else if (url.contains("/demo")){
responseHeader (socket,"error");
}else if (url.contains("/aaa")){
responseHeader (socket,"aaa");
}else if (url.contains("/bbb")){
responseHeader (socket,"bbb");
}else if (url.contains("/ccc")){
responseHeader (socket,"ccc");
}else if (url.contains("/ccc")){
// ......
}
}
}
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void responseHeader (Socket socket,String response) throws IOException {
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println("HTTP/1.1 200 OK");
pw.println("Content-type:text/html");
pw.println();
pw.println("<h1>"+response+"</h1>");
pw.flush();
}
Servlet
容器的实现
可以发现,如果我们接口很多的话,我们需要在上面的HTTP
服务器代码里写一大堆if else
逻辑判断:如果是 A 请求就调 A 类的 M1 方法,如果是 B 请求就调 B 类的 M1 方法。这样的话 HTTP
服务器的代码跟业务完全耦合在一起了,如果新加一个业务方法还要改 HTTP
服务器的代码。
为了解决HTTP
服务器与业务的耦合,便有了 Servlet
接口,各种业务类都必须实现这个接口。
HTTP
服务器如何知道由哪个 Servlet
来处理呢?
Servlet
容器闪亮登场,Servlet
容器负责加载和实例化 Servlet
,管理它们的生命周期以及提供运行时环境。HTTP
服务器不直接跟业务类打交道,而是把请求交给 Servlet
容器去处理,Servlet
容器会将请求,转发到具体的 Servlet
来处理该请求。然后将请求的数据传递给 Servlet
,并接收 Servlet
处理后的响应数据。Servlet
容器再将响应数据发送回客户端。
public static void main(String[] args) {
try {
//监听端口号8080
ServerSocket ss = new ServerSocket(8080);
while (true) {
Socket socket = ss.accept();
//获取客户端请求信息
BufferedReader bd = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requestHeader;
while ((requestHeader = bd.readLine()) != null && !requestHeader.isEmpty()) {
ServletProcessor processor = new ServletProcessor();
//request response 转换类大家可以自己去实现
processor.process(request, response);
}
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static final String WEB_ROOT = System.getProperty("user.dir")
+ File.separator + "webroot";
public void process(ServletRequest request, ServletResponse response) {
String uri = "";
String servletName = "";
//类加载器,用于从指定JAR文件或目录加载类
URLClassLoader loader = null;
try {
URLStreamHandler streamHandler = null;
//创建类加载器
loader = new URLClassLoader(new URL[]{new URL(null, "file:" +WEB_ROOT, streamHandler)});
} catch (IOException e) {
e.printStackTrace();
}
Class<?> myClass = null;
try {
//加载对应的servlet类
myClass = loader.loadClass(servletName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Servlet servlet = null;
try {
//初始化servlet实例
servlet = (Servlet) myClass.newInstance();
//调用service方法
servlet.service(request,response);
} catch (Exception e) {
e.printStackTrace();
}
}
一个简单的 HTTP
服务器 + Servlet
容器实现了,通过动态类加载实现了业务逻辑与服务器解耦。使得程序员可以专注业务逻辑的开发。Servlet
接口跟 Servlet
容器这一整套规范叫作Servlet
规范。
Tomcat
总体架构
前面我们已经了解了Tomcat
是一个HTTP
服务器 + Servlet
容器,所以需要实现以下2个核心功能:
-
Socket
监听客户端请求连接,返回响应数据。 -
加载和管理
Servlet
,以及具体处理请求
Tomcat
设计了两个核心组件来分别做这两件事情:
-
Connector
(连接器)负责开启Socket
并监听客户端请求、返回响应数据; -
Container
(容器)负责具体的请求处理。
它们分别拥有自己的start()
和stop()
方法来加载和释放自己维护的资源。
最顶层是
Server
也就是一个 Tomcat
实例。一个 Server
中有一个或者多个 Service
,一个 Service
中有多个连接器Connector
和一个 Container
容器。连接器与容器之间通过标准的 ServletRequest
和 ServletResponse
通信
连接器(Connector
)
连接器的主要功能:
-
监听服务器端口,读取客户端请求
-
将请求数据按照具体的协议解析(
HTTP/AJP
)生成统一的请求对象。 -
调用
Servlet
容器,进行业务处理 -
得到响应对象,返回客户端
连接器模块主要的两个核心组件:ProtocolHandler
和 Adapter
下面详细介绍这两个顶层组件
协议处理器(ProtocolHandler
)
Tomcat
支持多协议(HTTP/AJP
)以及多种I/O
方式(BIO/NIO
)。由于 I/O
模型和应用层协议可以自由组合,比如 NIO + HTTP
或者 NIO2 + AJP
,因此使用了 ProtocolHandler
的接口来封装这两种变化点。
ProtocolHandler
表示一个协议处理器,针对不同协议和I/O
方式,提供了不同的实现,如下图Http11NioProtocol
表示基于NIO
的HTTP
协议处理器。
它还包含了2个重要部件:EndPoint
和 Processor
EndPoint
EndPoint
是一个接口,对应的抽象实现类是AbstractEndpoint
。用于启动Socket
监听,该接口按照I/O
方式进行分类实现,如Nio2Endpoint
表示非阻塞式Socket I/O
。这有两个重要的子组件: Acceptor
和 SocketProcessor
。
其中 Acceptor
用于监听 Socket
连接请求。SocketProcessor
用于处理接收到的 Socket
提交到线程池(Executor
)来执行。调用协议处理组件 Processor
进行处理。
Processor
Processor
用于按照指定协议读取数据,并将请求交由容器处理,如Http11Processor
实现了HTTP 1.1
协议的解析方法和请求处理方式。
Adapter
组件
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat
定义了自己的 Request
类来存放这些请求信息。通过 Processor
调用 CoyoteAdapter
的 Sevice
方法,将 Tomcat Request
转成ServletRequest
小结
连接器用 ProtocolHandler
接口来封装通信协议和I/O
模型的差异,
ProtocolHandler
内部又分为 EndPoint
和 Processor
模块,EndPoint
负责底层 Socket
通信,Proccesor
负责应用层协议解析。连接器通过适配器 Adapter
调用容器
容器(Container
)
在Tomcat
里,容器就是用来装载Servlet
的。那 Tomcat
的 Servlet
容器是如何设计的呢?
Tomcat
设计了 4 种容器,分别是 Engine、Host、Context
和 Wrapper
。这 4 种容器不是平行关系,而是父子关系。
请求定位 Servlet
的过程
设计了这么多层次的容器,Tomcat
是怎么确定请求是由哪个 Wrapper
容器里的 Servlet
来处理的呢?
首先根据协议和端口号选定Service
和 Engine
Tomcat
的每个连接器都监听不同的端口,一个 Service
组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine
容器,因此 Service
确定了也就意味着 Engine
也确定了
然后,根据域名选定 Host
通过 URL
中的域名去查找相应的 Host
容器
之后,根据 URL 路径找到 Context
组件
根据 URL
的路径来匹配相应的 Web
应用的路径,找到了Context
容器
最后,根据 URL 路径找到 Wrapper
(Servlet
)
Mapper
再根据 web.xml
中配置的 Servlet
映射路径来找到具体的
Wrapper
和 Servlet
Pipeline-Valve
连接器中的 Adapter
会调用容器的 Service
方法来执行 Servlet
,最先拿到请求的是 Engine
容器,对请求做一些处理后,会把请求传给自己子容器 Host
继续处理,依此类推, 最后这个请求会传给 Wrapper
容器调用最终的 Servlet
来处理。
这个调用过程具体是怎么实现的呢?
通过 Pipeline-Valve
责任链模式,在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。
public interface Pipeline extends Contained {
Valve getBasic();
void setBasic(Valve var1);
void addValve(Valve var1);
Valve[] getValves();
void removeValve(Valve var1);
Valve getFirst();
boolean isAsyncSupported();
void findNonAsyncValves(Set<String> var1);
}
public interface Valve {
Valve getNext();
void setNext(Valve var1);
void backgroundProcess();
void invoke(Request var1, Response var2) throws IOException, ServletException;
boolean isAsyncSupported();
}
整个调用过程起点是由 CoyoteAdapter
中的 service()
方法触发的,它会调用 Engine
的第一个 Valve
:
//CoyoteAdapter#service Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
Tomcat
请求处理流程
了解完Tomcat
整体架构之后,最后我们来看下请求处理流程
-
当连接器
Connector
启动时,会同时启动其持有的Endpoint
实例。Endpoint
并行运行多个线程(由属性acceptorThreadCount
确定),循环监听端口通信。 -
当监听到请求时,
Acceptor
将Socket
封装为SocketWrapper
实例(此时并未读取数据),并交由一个SocketProcessor
对象处理(此过程也由线程池异步处理)。此部分根据I/O
方式的不同处理会有所不同。调用协议处理组件Processor
进行处理。 -
根据连接器
Connector
的请求Request
和响应Response
对象创建Servlet
请求对象和响应对象。 -
转换请求参数并完成请求映射
-
得到当前
Engine
的第一个Valve
并执行(invoke
),以完成客户端请求处理。
参考
-
Tomcat架构解析
-
深入拆解Tomcat & Jetty
转载自:https://juejin.cn/post/7241487780463362085