Tomcat 内部实现(上)
前言
好奇心是推动人们去探索和理解陌生领域的动力。正如 Tomcat 在 Java Web 服务器中的常见性,也在工作中广泛应用。通过深入了解 Tomcat,我们或许可以触及其他 Web 服务器,因为它们在本质上有着相似之处。这种类似性源于对基本原理和工作机制的共同遵循。通过拓展对 Tomcat 的理解,我们可以探索更广阔的领域,开启对其他 Web 服务器的探索之旅。
问题
带着问题来学习,不会让你像无头苍蝇一样,没有目的的学习。
- 什么是Tomcat?它的作用是什么?
- Tomcat的架构和组件有哪些?
- Tomcat的启动和关闭过程是怎样的?
- 什么是Servlet容器?Tomcat是如何实现Servlet容器的?
- Tomcat中的Connector是什么?它的作用是什么?
一个简单的web服务器
一个简单的web服务器,第一印象是什么样子,我认为它是一个springboot的一个服务(我之前认为一个服务就是一个web,其实,这是因为springboot默认内嵌了tomcat,让我误以为一个服务是一个wbe服务器)。但其实web服务器本质是由连接器和容器构成的。
Spring Boot应用程序内部的Tomcat仅仅是一个Servlet容器,负责处理HTTP请求和响应,并将请求传递给应用程序中的相应处理程序(如控制器、Servlet、过滤器等)。这个内嵌的Tomcat只是连接器和容器中的一部分,它提供了HTTP通信的基本功能。
连接器:是web服务器创建http请求的地方
连接器负责接收客户端的 HTTP 请求,并将其转发给服务器进行处理。它通常监听一个特定的端口,并使用一种特定的协议(如 HTTP)来与客户端进行通信。连接器负责处理网络连接的建立和断开,以及处理请求的基本信息(如请求方法、URI 等)。
容器:是web服务器处理请求,调用server的servicee方法的地方
容器是 Web 服务器处理请求并生成响应的核心组件。容器负责管理 Servlet 或 JSP 组件,调用它们的相应方法来处理请求。容器提供了 Servlet 生命周期管理、请求分发、会话管理等功能。它负责将接收到的请求交给相应的 Servlet 或 JSP 进行处理,并将生成的响应返回给客户端。
而一个简单web服务可以通过Socket 或者 ServerSocket 来实现一个阻塞的web服务器,用来在两个类来进行传输数据。
import java.io.*;
import java.net.*;
public class ClientExample {
public static void main(String[] args) {
String serverAddress = "localhost"; // 服务器地址
int serverPort = 8080; // 服务器端口号
try {
Socket socket = new Socket(serverAddress, serverPort);
// 获取输入输出流
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream, true);
// 向服务器发送请求
writer.println("Hello, server!");
// 关闭连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.*;
import java.net.*;
public class ServerExample {
public static void main(String[] args) {
int serverPort = 8080; // 监听的端口号
try {
ServerSocket serverSocket = new ServerSocket(serverPort);
while (true) {
Socket socket = serverSocket.accept(); // 接受客户端连接
// 获取输入输出流
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
// 读取客户端发送的请求
String request = reader.readLine();
System.out.println("Received request: " + request);
// 关闭连接
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意点:
在网络通信中,Socket 是一个通信端点的抽象概念。它代表了客户端和服务器之间的一条通信链路。
客户端使用 Socket 来与服务器建立连接,并在连接上发送和接收数据。
ServerSocket 是服务器端使用的类,用于监听特定的端口,接受客户端的连接请求。
简而言之,客户端使用 Socket 是因为它需要主动发起连接请求,而 ServerSocket 是服务器端用来监听连接请求的。
Socket 和 ServerSocket 都是阻塞式的,不过ServerSocket 是阻塞在accept方法上的。也就意味着,服务端会一直阻塞在等待客户端发送请求,不会执行其他任务。
总结:
tomcat的服务其实是通过 接收-响应 模式来实现的。简单的可以通过socket来实现一个阻塞式的web服务器。现在,你对web服务器有了初步的了解了。
核心概念
了解容器
容器是指 Web 容器或 Servlet 容器,它是一种提供 Servlet 组件管理和执行环境的软件。
Servlet 接口定义了以下五个方法,也就是它的生命周期:
init(ServletConfig config)
: 当 Servlet 被初始化时调用。在 Servlet 生命周期中只会被调用一次。可以在这个方法中执行一些初始化操作,例如加载配置文件、建立数据库连接等。service(ServletRequest request, ServletResponse response)
: 每当有请求到达时,容器会调用该方法来处理请求。该方法负责接收客户端的请求,并生成相应的响应。在这个方法中,可以通过请求对象获取请求参数、处理业务逻辑,并使用响应对象生成返回给客户端的响应。destroy()
: 在 Servlet 生命周期结束时调用。在 Servlet 容器关闭或者 Servlet 被卸载时,会调用该方法。可以在这个方法中执行一些清理操作,例如释放资源、关闭数据库连接等。getServletConfig()
: 返回一个 ServletConfig 对象,该对象包含了 Servlet 的配置信息。ServletConfig 对象可以用来获取 Servlet 的初始化参数、Servlet 的名称等。getServletInfo()
: 返回一个描述 Servlet 的字符串,用于提供关于 Servlet 的基本信息。通常在 Servlet 容器管理工具或者监控中使用。
实现一个简单的容器
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
public class SimpleServletContainer implements Servlet {
private ServletConfig servletConfig;
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
// 在初始化阶段进行一些必要的操作
}
@Override
public ServletConfig getServletConfig() {
return servletConfig;
}
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
// 处理请求并生成响应
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 可以根据请求的方法进行不同的处理逻辑
if (httpRequest.getMethod().equals("GET")) {
// 处理 GET 请求
// ...
} else if (httpRequest.getMethod().equals("POST")) {
// 处理 POST 请求
// ...
}
// 设置响应的内容类型和状态码
httpResponse.setContentType("text/html");
httpResponse.setStatus(HttpServletResponse.SC_OK);
// 向响应中写入内容
PrintWriter writer = httpResponse.getWriter();
writer.println("<html><body>");
writer.println("<h1>Hello, Servlet!</h1>");
writer.println("</body></html>");
}
@Override
public String getServletInfo() {
return "SimpleServletContainer";
}
@Override
public void destroy() {
// 在销毁阶段进行一些清理操作
}
}
简单了解连接器
连接器的实现是基于 Java 的网络编程 API,通常使用 Java NIO(New I/O)库来实现非阻塞的 I/O 操作。其中最常用的连接器实现是 Coyote Connector,它是 Tomcat 默认的连接器。
连接器只是 Tomcat 的一个组件,它负责处理网络连接和通信的底层细节,将请求转发给容器进行处理。
一个简单的连接器:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class SimpleConnector implements Runnable {
private int port;
private boolean running;
public SimpleConnector(int port) {
this.port = port;
this.running = false;
}
public void start() {
if (running) {
return;
}
running = true;
Thread thread = new Thread(this);
thread.start();
}
public void stop() {
running = false;
}
@Override
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(port);
while (running) {
Socket socket = serverSocket.accept();
handleRequest(socket);
socket.close();
}
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleRequest(Socket socket) {
try {
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
// 读取请求数据并进行处理
// ...
// 发送响应数据
// ...
} catch (IOException e) {
e.printStackTrace();
}
}
}
理解连接器和容器直接如何通信
连接器(Connector)和容器(Container)之间通过接口进行通信。在典型的 Servlet 容器中,连接器负责接收客户端请求并将其传递给容器进行处理。
连接器通常会提供一个方法,例如 setContainer(Container container)
,容器在初始化时会调用该方法将自身实例传递给连接器。这样,连接器就知道将请求转发给哪个容器进行处理。
一旦连接器接收到客户端的请求,它会将请求封装成一个请求对象(Request)并调用容器的相应方法(例如 container.processRequest(request)
)来处理该请求。容器会根据请求的 URL 等信息将请求路由到对应的 Servlet 或处理程序进行处理,并生成响应结果。
容器处理完请求后,会将生成的响应封装成一个响应对象(Response),然后将其返回给连接器。连接器负责将响应发送回客户端。
// Connector.java - 连接器
public interface Connector {
void setContainer(Container container);
void processRequest(Request request);
void sendResponse(Response response);
}
// Container.java - 容器
public interface Container {
void processRequest(Request request);
void sendResponse(Response response);
}
// SimpleConnector.java - 简单的连接器实现
public class SimpleConnector implements Connector {
private Container container;
@Override
public void setContainer(Container container) {
this.container = container;
}
@Override
public void processRequest(Request request) {
// 连接器处理请求的逻辑
// ...
// 将请求传递给容器进行处理
container.processRequest(request);
}
@Override
public void sendResponse(Response response) {
// 连接器发送响应的逻辑
// ...
}
}
// SimpleContainer.java - 简单的容器实现
public class SimpleContainer implements Container {
private Connector connector;
@Override
public void processRequest(Request request) {
// 容器处理请求的逻辑
// ...
// 生成响应对象
Response response = new Response();
// 设置响应内容
response.setContent("Hello, World!");
// 将响应传递给连接器进行发送
connector.sendResponse(response);
}
@Override
public void sendResponse(Response response) {
// 容器发送响应的逻辑
// ...
}
}
// Request.java - 请求对象
public class Request {
// 请求对象的属性和方法
// ...
}
// Response.java - 响应对象
public class Response {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
// Main.java - 主程序入口
public class Main {
public static void main(String[] args) {
// 创建连接器和容器实例
Connector connector = new SimpleConnector();
Container container = new SimpleContainer();
// 设置连接器和容器的关联
connector.setContainer(container);
container.setConnector(connector);
// 创建一个请求对象
Request request = new Request();
// 设置请求的参数、URL等信息
// 连接器处理请求
connector.processRequest(request);
}
}
Connector
接口定义了连接器的方法,包括设置容器、处理请求和发送响应等。Container
接口定义了容器的方法,包括处理请求和发送响应等。
SimpleConnector
类实现了Connector
接口,它持有一个容器实例,并在处理请求时将请求传递给容器进行处理。
SimpleContainer
类实现了Container
接口,它持有一个连接器实例,并在处理请求时生成响应对象,并将响应传递给连接器进行发送。
连接器中解析Http请求过程
我们还需要更加了解连机器它内部的操作过程:
-
从客户端 Socket 获取输入流:连接器通过客户端的 Socket 对象获取输入流,以便读取客户端发送的请求数据。
import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class ConnectorExample { public static void main(String[] args) { try { // 创建 ServerSocket 对象并绑定端口 ServerSocket serverSocket = new ServerSocket(8080); // 等待客户端连接 System.out.println("等待客户端连接..."); Socket clientSocket = serverSocket.accept(); System.out.println("客户端连接成功!"); // 获取客户端的输入流 InputStream inputStream = clientSocket.getInputStream(); // 读取客户端发送的请求数据 byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { // 处理读取的请求数据 String requestData = new String(buffer, 0, bytesRead); System.out.println("客户端请求数据:" + requestData); } // 关闭连接 clientSocket.close(); serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
解析请求行:从输入流中读取请求行,它包含请求方法、请求路径和 HTTP 版本信息。通常,可以使用字符串分割或正则表达式等方式将请求行拆分为相应的字段。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; public class RequestLineParserExample { public static void main(String[] args) { try { // 创建 Socket 对象并连接到服务器 Socket socket = new Socket("localhost", 8080); // 获取输入流,用于读取服务器响应 InputStream inputStream = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); // 读取请求行 String requestLine = reader.readLine(); // 解析请求行 String[] requestParts = requestLine.split(" "); if (requestParts.length == 3) { String method = requestParts[0]; String path = requestParts[1]; String version = requestParts[2]; // 输出解析结果 System.out.println("Method: " + method); System.out.println("Path: " + path); System.out.println("Version: " + version); } else { System.out.println("Invalid request line: " + requestLine); } // 关闭连接 socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
解析请求头:读取并解析请求头部的每一行,包括请求头字段和对应的值。请求头中包含诸如 Content-Type、Content-Length、Cookie 等信息。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; public class RequestHeaderParserExample { public static void main(String[] args) { try { // 创建 Socket 对象并连接到服务器 Socket socket = new Socket("localhost", 8080); // 获取输入流,用于读取服务器响应 InputStream inputStream = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); // 读取请求头 String headerLine; while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) { // 解析请求头 String[] headerParts = headerLine.split(": "); if (headerParts.length == 2) { String headerField = headerParts[0]; String headerValue = headerParts[1]; // 输出解析结果 System.out.println("Header Field: " + headerField); System.out.println("Header Value: " + headerValue); } else { System.out.println("Invalid header line: " + headerLine); } } // 关闭连接 socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
解析 Cookie:如果请求头中包含 Cookie,需要解析 Cookie 的值,并将其封装成相应的数据结构,以便后续处理。
import java.util.HashMap; import java.util.Map; public class CookieParserExample { public static void main(String[] args) { String cookieHeader = "Cookie: sessionId=abc123; userId=12345; rememberMe=true"; // 解析 Cookie Map<String, String> cookies = parseCookies(cookieHeader); // 输出解析结果 for (Map.Entry<String, String> entry : cookies.entrySet()) { String cookieName = entry.getKey(); String cookieValue = entry.getValue(); System.out.println(cookieName + ": " + cookieValue); } } private static Map<String, String> parseCookies(String cookieHeader) { Map<String, String> cookies = new HashMap<>(); // 查找 Cookie 字段 int colonIndex = cookieHeader.indexOf(":"); if (colonIndex != -1) { String cookieValue = cookieHeader.substring(colonIndex + 1).trim(); // 解析 Cookie 值 String[] cookieParts = cookieValue.split(";"); for (String cookiePart : cookieParts) { String[] keyValue = cookiePart.trim().split("="); if (keyValue.length == 2) { String cookieName = keyValue[0].trim(); String cookieValue = keyValue[1].trim(); cookies.put(cookieName, cookieValue); } } } return cookies; } }
-
获取请求参数:根据请求的类型(GET 或 POST)以及 Content-Type 等因素,从请求中获取相应的参数。对于 GET 请求,可以从请求路径或查询字符串中解析参数;对于 POST 请求,可以从请求体中解析参数。
import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; public class GetRequestParameterExample { public static void main(String[] args) { String requestPath = "/example?param1=value1¶m2=value2"; // 解析请求路径和查询字符串 int questionIndex = requestPath.indexOf('?'); if (questionIndex != -1) { String queryString = requestPath.substring(questionIndex + 1); // 解析查询字符串中的参数 Map<String, String> parameters = parseQueryString(queryString); // 输出解析结果 for (Map.Entry<String, String> entry : parameters.entrySet()) { String paramName = entry.getKey(); String paramValue = entry.getValue(); System.out.println(paramName + ": " + paramValue); } } } private static Map<String, String> parseQueryString(String queryString) { Map<String, String> parameters = new HashMap<>(); String[] paramPairs = queryString.split("&"); for (String paramPair : paramPairs) { String[] keyValue = paramPair.split("="); if (keyValue.length == 2) { try { String paramName = URLDecoder.decode(keyValue[0], "UTF-8"); String paramValue = URLDecoder.decode(keyValue[1], "UTF-8"); parameters.put(paramName, paramValue); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } return parameters; } } import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; public class PostRequestParameterExample { public static void main(String[] args) { try { // 获取请求体输入流 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); // 读取请求体 StringBuilder requestBody = new StringBuilder(); String line; while ((line = reader.readLine()) != null && !line.isEmpty()) { requestBody.append(line); } // 解析请求体中的参数 Map<String, String> parameters = parseRequestBody(requestBody.toString()); // 输出解析结果 for (Map.Entry<String, String> entry : parameters.entrySet()) { String paramName = entry.getKey(); String paramValue = entry.getValue(); System.out.println(paramName + ": " + paramValue); } } catch (IOException e) { e.printStackTrace(); } } private static Map<String, String> parseRequestBody(String requestBody) { Map<String, String> parameters = new HashMap<>(); String[] paramPairs = requestBody.split("&"); for (String paramPair : paramPairs) { String[] keyValue = paramPair.split("="); if (keyValue.length == 2) { try { String paramName = URLDecoder.decode(keyValue[0], "UTF-8"); String paramValue = URLDecoder.decode(keyValue[1], "UTF-8"); parameters.put(paramName, paramValue); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } return parameters; } }
-
创建请求对象:将解析得到的请求信息封装成一个请求对象(Request),包括请求方法、请求路径、请求头、Cookie、请求参数等。
总结
通过核心概念,我们知道了一个web服务需要实现的内容包括连接器(用于接受和处理http请求)、容器(用于处理相应的请求逻辑)。一个简单的web服务器包括连接器和容器。
通过了解和学习,我们也就更清楚了一个web服务器的构成。
Tomcat 的Connector连接器
Apache Tomcat的连接器(Connector)组件有多个实现,每个实现针对不同的协议和功能提供支持。以下是Tomcat连接器组件的一些常见实现:
-
HTTP/1.1 Connector(org.apache.coyote.http11.Http11NioProtocol):
- 基于NIO(New I/O)的HTTP/1.1协议实现。
- 提供高性能的非阻塞I/O处理,适用于处理大量并发请求。
- 支持HTTP/1.1协议的特性,如持久连接、流水线化、压缩等。
-
HTTP/2 Connector(org.apache.coyote.http2.Http2Protocol):
- 基于NIO的HTTP/2协议实现。
- 提供对HTTP/2协议的支持,包括多路复用、服务器推送、帧优先级等特性。
- 支持与HTTP/1.1协议的兼容性,可以同时处理HTTP/1.1和HTTP/2请求。
-
AJP Connector(org.apache.coyote.ajp.AjpNioProtocol):
- 支持AJP(Apache JServ Protocol)协议的实现。
- AJP协议用于将Tomcat与Web服务器(如Apache HTTP Server)进行集成,实现反向代理和负载均衡。
- 提供高性能的连接和请求传输,适用于与Web服务器之间的高效通信。
在Tomcat的配置文件(如server.xml)中,可以根据需要选择适当的连接器实现,并进行相应的配置。这些配置包括监听端口、线程池大小、SSL证书、连接超时等参数,以满足应用程序的需求和性能要求。
<Connector port="8443" protocol="org.apache.coyote.http2.Http2Protocol" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" sslProtocol="TLS" keystoreFile="/path/to/keystore" keystorePass="password" /> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
Tomcat的连接器默认实现接口
- 这是Tomcat连接器的主要接口,定义了连接器的基本行为和属性。
- 它提供了设置和获取连接器的端口、协议、协议处理器、线程池等属性的方法。
它会等待引入的HTTP请求,创建request对象和response对象,然后调用org.apache.catalina.Container接口的invoke()方法,将request对象和response对象传给servlet容器。invoke()方法的签名如下:
public void invoke(Request request, Response response) {
// 在这里将请求对象和响应对象传递给 Servlet 容器进行处理
// 可能会涉及到过滤器、匹配 URL、查找对应的 Servlet 等操作
}
在invoke()方法内部,servlet容器会载入相应的servlet类,调用其service()方法,管理session对象,记录错误消息等操作。
Connector接口
Tomcat的连接器必须实现org.apache.catalina.Connector接口。在接口中声明了很多方法,其中最重要的是getContainer()、setContainer()、createRequest()和createResponse()。
HttpConnector
1.HttpProcessor
每一个HttpProcessor都会维护一个request对象和response对象来解析Http请求行和请求头。
HttpConnector实例可以处理多个http请求是由于:HttpConnector实例有HttpProcessor对象池,每个HttpProcessor实例都运行在其自己的线程中。它会将创建的对象存放到Stack类型的变量中。
创建的HttpProcessor实例的个数由两个变量决定:minProcessors和maxProcessors。默认情况下,minProcessors的值为5,maxProcessors的值为20,可以通过setMinProcessors()方法和setMaxProcessors()方法对这两个数值进行修改。
对HttpProcessor来说,然后请求超出了设定的最大数量,则剩余请求会被忽略丢弃。如果,希望持续创建可以设置maxProcessors为负数。
while(curProcessors < minProcessors){ if((maxProcessors>0)&&(curProcessors>=maxProcessors)){ break;//如果当前processors超过了max 则就不创建 } recycle(newProcessor());//创建一个新的Processors,并将其入栈 }
扩展:
最大请求数量的控制,Tomcat 使用了另一个参数
maxConnections
,它表示允许的同时活动连接数。这个参数控制着能够与服务器建立连接的客户端数量,而不是每个连接中并发处理的请求数量。
3.早期HttpProcessor 异步请求,处理多个http请求
HttpProcessor 是通过线程池来完成http多个请求。具体线程又是通过基本的线程唤醒机制wait-notify:生产者 – 消费者来进行工作的。
- connector是任务的生产者
- processor是任务的消费者
connector线程唤醒processor线程:
synchronized void assign(Socket socket) {
// Wait for the Processor to get the previous Socket
//available表示是否有请求进来
while (available) {
try {
wait(); //wait 方法会使当前线程进入等待状态
} catch (InterruptedException e) {
}
}
// Store the newly available Socket and notify our thread
this.socket = socket;
available = true;
notifyAll(); //唤醒等待的线程
if ((debug >= 1) && (socket != null))
log(" An incoming request is being assigned");
}
available代表是否已经有socket可处理,刚取出来的processor,肯定是没有socket要处理,为false。所以connector线程会把available设为true,然后 notify挂起在processor对象上的消费者线程。
connector线程挂起processor线程:
/**
* The background thread that listens for incoming TCP/IP connections and
* hands them off to an appropriate processor.
*/
public void run() {
// Process requests until we receive a shutdown signal
while (!stopped) {
// Wait for the next socket to be assigned 挂起
Socket socket = await();
if (socket == null)
continue;
// Process the request from this socket
try {
process(socket);
} catch (Throwable t) {
log("process.invoke", t);
}
// Finish up this request
connector.recycle(this);
}
// Tell threadStop() we have shut ourselves down successfully
synchronized (threadSync) {
threadSync.notifyAll();
}
}
/**
* Await a newly assigned Socket from our Connector, or <code>null</code>
* if we are supposed to shut down.
*/
private synchronized Socket await() {
// Wait for the Connector to provide a new Socket
while (!available) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Notify the Connector that we have received this Socket
Socket socket = this.socket;
available = false;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" The incoming request has been awaited");
return (socket);
}
在Tomcat中,连接器线程(生产者)不会被阻塞,以确保能够处理客户端的连接请求。connector线程的主要任务是接受传入的客户端连接,并将连接请求分配给线程池中的处理器(消费者线程)进行处理。
因此,连接器线程(生产者)不会被阻塞,并且能够持续地接受和处理连接请求,以保证Tomcat能够正常处理客户端的连接。
Tomcat 的Servlet容器
container接口
Tomcat中的servlet容器必须要实现org. apache.catalina.Container接口。需要将servlet容器的实例作为参数传入到连接器的setContainer()方法中,这样连接器才能调用servlet容器的invoke()方法。
Servlet中的invoke方法
当连接器调用servlet容器的invoke()方法之后,就会将用管道(类似过滤器)。servlet会将管道分为多个,最后一个执行的管道为Basic pipeline 基础阀。
这些阀门会将接受到的Request和Response传递到下一个阀门中进行处理,知道基础阀门执行结束。
一个容器有多个管道:
//如果有多个管道,先循环执行
for(int n=0;n<values.length;n++){
value[n].invoke(...);
}
//执行完在最后执行基础管道
basicValue.invoke(...);
Tomcat 管道的实现不同于上面的,而是采用 ValveContext 接口来实现阀的遍历。当我们调用Container的invoke方法的时候,其实,是在调用Pipeline 的invoake方法。接着Pipeline调用自身的内部类ValveContext访问所有的管道。并且调用invokeNext()方法。
invokeNext(Request request, Response response){
//...
}
简单来说,Valve 是用于处理请求和响应的组件,而 Pipeline 是一系列 Valve 的集合。当请求进入 Tomcat 的容器时,会通过 Pipeline 中的 Valve 依次处理请求,并最终生成响应。
来看看tomcat对于管道的具体实现吧
protected class standardPipelineValveContext implements ValveContext {
protected int stage = 0 ;
public string getInfo () {
return info;
}
public void invokeNext (Request request,Response response)
throws IOException,ServletException {
int subscript = stage;
stage = stage + 1;
//Invoke the requested valve for the current request thread
if ( subscript < valves . length){
valves [ subscript ] .invoke(request, response,this);
}else if ((subscript a= valves.length)& (basic != null)){
basic.invoke (request, response, this);
}else{
throw new servletException
( sm.getstring ( "standardPipeline. novalve" ) );
}
}
}
第一次调用时subscript的值为0,此时,第一个阀门将被调用,接着 stage +1 .接着带哦用下一个阀门。直到最后一个基础阀门呗调用。
容器的四种类型
-
Engine(引擎): Engine代表整个Catalina Servlet引擎,是Tomcat服务器中的最高级别容器。它负责处理和分派传入的请求,并将其转发给适当的虚拟主机进行处理。一个Tomcat服务器可以包含一个或多个Engine。
<Engine name="Catalina" defaultHost="localhost"> <Host name="example1.com" appBase="webapps/example1" unpackWARs="true" autoDeploy="true"> <Context path="" docBase="ROOT" /> </Host> <Host name="example2.com" appBase="webapps/example2" unpackWARs="true" autoDeploy="true"> <Context path="" docBase="ROOT" /> </Host> </Engine>
假设我们有两个虚拟主机:
example1.com
和example2.com
。我们可以使用类似以下的模拟请求来访问这些虚拟主机中的应用程序:-
对于
example1.com
,假设有一个名为myapp
的应用程序:- URL:example1.com/myapp
- 对应的文件路径:
webapps/example1/myapp
-
对于
example2.com
,假设有一个名为anotherapp
的应用程序:- URL:example2.com/anotherapp
- 对应的文件路径:
webapps/example2/anotherapp
通过配置多个
<Host>
,我们可以在同一个 Tomcat 实例中托管多个不同的网站或应用程序,使它们通过不同的域名或 IP 地址进行访问。当请求被 Tomcat 接受后,
<Engine>
标签在 Tomcat 中扮演着核心角色,具体起到以下作用:- 调度请求:
<Engine>
标签负责接收并调度来自客户端的请求。它是整个 Tomcat 服务器的顶级容器,作为请求处理的入口点。 - 管理多个虚拟主机:
<Engine>
标签可以包含一个或多个<Host>
标签,每个<Host>
表示一个虚拟主机。它们可以处理针对特定域名或 IP 地址的请求。通过配置多个<Host>
,可以在同一个 Tomcat 实例中托管多个不同的网站或应用程序。 - 处理请求映射:
<Engine>
标签根据请求的域名或 IP 地址,将请求映射到对应的<Host>
标签来处理。它根据请求的主机名来确定要将请求转发给哪个虚拟主机进行处理。 - 管理共享资源:
<Engine>
标签还可以配置和管理共享资源,如连接池、全局数据源等。这些资源可以在不同的虚拟主机之间共享和复用。
-
-
Host(主机): Host是位于Engine之下的容器,表示一个虚拟主机。每个虚拟主机都有自己的域名或IP地址,并且可以独立配置和管理自己的Web应用程序。一个Engine可以包含多个Host。
<Host name="example.com" appBase="webapps/example" unpackWARs="true" autoDeploy="true"> <Context path="" docBase="ROOT" /> </Host>
- 虚拟主机(Virtual Host)的定义:
<Host>
标签用于定义一个虚拟主机,它代表了一个特定的域名或 IP 地址对应的网站或应用程序。通过配置多个<Host>
,可以在同一个 Tomcat 实例中托管多个不同的网站或应用程序。 - 请求路由和处理:
<Host>
标签根据请求的域名或 IP 地址,将请求路由到对应的网站或应用程序进行处理。它负责根据请求的主机头(Host Header)信息,将请求分发给正确的虚拟主机进行处理。 - 管理应用程序:
<Host>
标签可以包含一个或多个应用程序(Web Application)。每个应用程序可以是一个单独的 WAR 文件或一个目录,包含了特定虚拟主机的网站或应用程序的相关文件和资源。 - 上下文管理:
<Host>
标签可以配置和管理每个应用程序的上下文(Context)。上下文包括上下文路径(Context Path)和应用程序的部署目录(DocBase),它定义了应用程序在虚拟主机中的访问路径和位置。 - 共享资源管理:
<Host>
标签还可以配置和管理虚拟主机级别的共享资源,如连接池、全局数据源等。这些资源可以在同一虚拟主机的多个应用程序之间共享和复用。
- 虚拟主机(Virtual Host)的定义:
-
Context(上下文): Context是位于Host之下的容器,表示一个Web应用程序的上下文。每个Web应用程序都有自己的Context,它包含应用程序的部署描述符(如web.xml)以及应用程序的资源和配置。一个Host可以包含多个Context。
Context接口实例表示一个web应用程序,那也就意味着,它包含多个Wrapper级别的实例作为其子容器。通常Web应用程序时多个servlet进行合作的。
但存在多个Wrapper时如何区分这些Wrapper?
在早期版本tomcat采用映射器组件来对servlet容器做相关联。对于多个容器来说通常通过url模式与Wrapper实例名称进行映射>>> 例如 /Primitive” — “Primitive”(PrimitiveServlet) “/Modern” — “Modern”(ModernServlet)。这也就是常见的,url路径匹配,我们再写JSP的时候通过会将配置的XML中的url名称与servlet名称一致。
看看Context是如何使用的吧?
public final class Bootstrap2 { public static void main(String[] args) { HttpConnector connector = new HttpConnector(); Wrapper wrapper1 = new SimpleWrapper(); wrapper1.setName("Primitive"); wrapper1.setServletClass("PrimitiveServlet"); Wrapper wrapper2 = new SimpleWrapper(); wrapper2.setName("Modern"); wrapper2.setServletClass("ModernServlet"); Context context = new SimpleContext(); context.addChild(wrapper1); context.addChild(wrapper2); Valve valve1 = new HeaderLoggerValve(); Valve valve2 = new ClientIPLoggerValve(); ((Pipeline) context).addValve(valve1); ((Pipeline) context).addValve(valve2); Mapper mapper = new SimpleContextMapper(); mapper.setProtocol("http"); context.addMapper(mapper); Loader loader = new SimpleLoader(); context.setLoader(loader); // context.addServletMapping(pattern, name); context.addServletMapping("/Primitive", "Primitive"); context.addServletMapping("/Modern", "Modern"); connector.setContainer(context); //注意此时,我们放到连机器中的是context级别的容器,这些容器又包括子容器即Wrapper级别的容器(Servlet) try { connector.initialize(); connector.start(); // make the application wait until we press a key. System.in.read(); } catch (Exception e) { e.printStackTrace(); } } }
-
Wrapper(包装器): Wrapper是位于Context之下的容器,表示一个Servlet的包装器。它负责将Servlet与特定的URL模式(如路径或扩展名)关联起来,并处理对该URL模式的请求。Wrapper包含了Servlet的实际实例,并提供了Servlet的生命周期管理。一个Context可以包含多个Wrapper。
Wrapper如何选择对应servlet呢?
通过allocate()方法会分配一个servlet实例,并通过load()方法加载servlet类。
Wrapper 是通过 Loader 接口来加载具体实例对象,它知道servelt类的位置,可以通过getClassLoader()方法获得一个实例对象。
其中一个变量为WEB_ROOT表面servlet类所在的目录。
看看Wrapper是如何使用的?
public final class Bootstrap1 { public static void main(String[] args) { /* call by using http://localhost:8080/ModernServlet, but could be invoked by any name */ HttpConnector connector = new HttpConnector(); Wrapper wrapper = new SimpleWrapper(); wrapper.setServletClass("ModernServlet"); Loader loader = new SimpleLoader(); //初始化一些简单管道 Valve valve1 = new HeaderLoggerValve(); Valve valve2 = new ClientIPLoggerValve(); wrapper.setLoader(loader); //将管道放到wrapper中 ((Pipeline) wrapper).addValve(valve1); ((Pipeline) wrapper).addValve(valve2); //将上述的ModernServlet的wrapper放到容器中 connector.setContainer(wrapper); try { connector.initialize(); connector.start(); // make the application wait until we press a key. System.in.read(); } catch (Exception e) { e.printStackTrace(); } } }
总结:
Tomcat请求处理流程
一个http请求的:
- 协议+端口号决定engine:根据协议类型和端口号选定 Service 和 Engine:Service 下属的 Connector 组件负责监听接收特定协议和特定端口的请求。因此,当 Tomcat 启动时,Service 组件就开始监听特定的端口,如前文配置文件示例,Catalina 这个 Service 监听了 HTTP 协议 8080 端口和 AJP 协议的 8009 端口。当 HTTP 请求抵达主机网卡的特定端口之后,Tomcat 就会根据协议类型和端口号选定处理请求的 Service,随即 Engine 也就确定了。通过在 Server 中配置多个 Service,可以实现通过不同端口访问同一主机上的不同应用。
- ip或域名决定host:根据域名或 IP 地址选定 Host:待 Service 被选定之后,Tomcat 将在 Service 中寻找与 HTTP 请求头中指定的域名或 IP 地址匹配的 Host 来处理该请求。如果没有匹配成功,则采用 Engine 中配置的默认虚拟主机 defaultHost 来处理该请求。
- 路径决定context:根据 URI 选定 Context:URI 中的 context-path 指定了 HTTP 请求将要访问的 Web 应用。当请求抵达时,Tomcat 将根据 Context 的属性 path 取值与 URI 中的 context-path 的匹配程度来选择 Web 应用处理相应请求,例如:Web 应用 spring-demo 的 path 属性是”/spring-demo”,那么请求“/spring-demo/user/register”将交由 spring-demo 来处理。
比如访问:http://201.187.10.21:8080/spring-demo/user/register——
- 客户端(或浏览器)发送请求至主机(201.187.10.21)的端口 8080,被在该端口上监听的 Coyote HTTP/1.1 Connector 所接收。Connector 将该请求交给它所在 Service 的 Engine 来负责处理,并等待 Engine 的回应。
- Engine 获得请求之后从报文头中提取主机名称(201.187.10.21),在所有虚拟主机 Host 当中寻找匹配。
- 在未匹配到同名虚拟主机的情况下,Engine 将该请求交给名为 localhost 的默认虚拟主机 Host 处理。
- Host 获得请求之后将根据 URI(/spring-demo/user/register)中的 context-path 的取值“/spring-demo” 去匹配它所拥有的所有 Context,将请求交给代表应用 spring-demo 的 Context 来处理。
- Context 构建 HttpServletRequest、HttpServletResponse 对象,将其作为参数调用应用 spring-demo,由应用完成业务逻辑执行、结果数据存储等过程,等待应答数据。
- Context 接收到应用返回的 HttpServletResponse 对象之后将其返回给 Host。
- Host 将 HttpServletResponse 对象返回给 Engine。
- Engine 将 HttpServletResponse 对象返回 Connector。
- Connector 将 HttpServletResponse 对象返回给客户端(或浏览器)。
注摘自:(四)How Tomcat Works – Tomcat servlet容器Container
Tomcat的生命周期
根据以上我们对Tomcat的了解,知道了Servlet不同级别层次,也明白内部存在许多的组件。当Tomcat启动关闭时,这些内部组件也应该跟随Tomcat启动关闭。此时,Tomcat引入了Lifecycle接口来协调这些组件。
Lifecycle 接口
Tomcat 内部在实现 Lifecycle 接口时使用了观察者模式。通过将实现了Lifecycle接口,可以实现启动父组件,子组件也会随着父组件一起工作。当父组件启动或关闭时,它会通知其注册的子组件,以确保它们与父组件的状态保持一致。
public interface Lifecycle {
public static final String START_EVENT = "start";
public static final String BEFORE_START_EVENT = "before_start";
public static final String AFTER_START_EVENT = "after_start";
public static final String STOP_EVENT = "stop";
public static final String BEFORE_STOP_EVENT = "before_stop";
public static final String AFTER_STOP_EVENT = "after_stop";
public void addLifecycleListener(LifecycleListener listener);
public LifecycleListener[] findLifecycleListeners();
public void removeLifecycleListener(LifecycleListener listener);
/**
* Prepare for the beginning of active use of the public methods of this
* component. This method should be called before any of the public
* methods of this component are utilized. It should also send a
* LifecycleEvent of type START_EVENT to any registered listeners.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
public void start() throws LifecycleException;
/**
* Gracefully terminate the active use of the public methods of this
* component. This method should be the last one called on a given
* instance of this component. It should also send a LifecycleEvent
* of type STOP_EVENT to any registered listeners.
*
* @exception LifecycleException if this component detects a fatal error
* that needs to be reported
*/
public void stop() throws LifecycleException;
}
LifecycleEvent 接口
在 Tomcat 中,LifecycleEvent 接口是用于表示组件生命周期事件的接口。LifecycleEvent 提供了一些方法来获取有关事件的信息,如事件源组件、事件类型等。
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
public class MyLifecycleListener implements LifecycleListener {
@Override
public void lifecycleEvent(LifecycleEvent event) {
// 处理生命周期事件
String type = event.getType();
Lifecycle component = (Lifecycle) event.getSource();
// 根据事件类型和组件执行相应的逻辑
if (type.equals(Lifecycle.BEFORE_INIT_EVENT)) {
// 在初始化之前的逻辑
} else if (type.equals(Lifecycle.AFTER_START_EVENT)) {
// 在启动之后的逻辑
} else if (type.equals("custom_event")) {
// 自定义事件的逻辑
}
}
}
通过 LifecycleEvent 接口,可以在组件的生命周期不同阶段触发自定义的事件,并将事件对象传递给注册的监听器。监听器可以根据事件的类型和事件源组件执行相应的逻辑。
LifecycleListener 接口
LifecycleListener 接口是用于监听组件生命周期事件的接口。实现 LifecycleListener 接口的类可以注册为组件的监听器,以便在组件的生命周期不同阶段接收相应的事件通知。
public interface LifecycleListener {
/**
* Acknowledge the occurrence of the specified event.
*
* @param event LifecycleEvent that has occurred
*/
public void lifecycleEvent(LifecycleEvent event);
}
总结:
Tomcat 4 版本中的 总体 Lifecycle 实现:
import org.apache.catalina.util.LifecycleSupport;
public class MyComponent implements org.apache.catalina.Lifecycle {
private final LifecycleSupport lifecycleSupport;
public MyComponent() {
lifecycleSupport = new LifecycleSupport(this);
}
@Override
public void start() throws LifecycleException {
System.out.println("MyComponent starting...");
// 执行启动逻辑
// ...
lifecycleSupport.fireLifecycleEvent(BEFORE_START_EVENT, null);
// 组件启动后的逻辑
// ...
lifecycleSupport.fireLifecycleEvent(START_EVENT, null);
}
@Override
public void stop() throws LifecycleException {
System.out.println("MyComponent stopping...");
// 执行停止逻辑
// ...
lifecycleSupport.fireLifecycleEvent(STOP_EVENT, null);
// 组件停止后的逻辑
// ...
lifecycleSupport.fireLifecycleEvent(AFTER_STOP_EVENT, null);
}
@Override
public void addLifecycleListener(LifecycleListener listener) {
lifecycleSupport.addLifecycleListener(listener);
}
@Override
public void removeLifecycleListener(LifecycleListener listener) {
lifecycleSupport.removeLifecycleListener(listener);
}
@Override
public LifecycleListener[] findLifecycleListeners() {
return lifecycleSupport.findLifecycleListeners();
}
}
MyComponent
实现了 Tomcat 4 版本的 Lifecycle 接口,并使用 LifecycleSupport 类来处理生命周期事件。在start()
方法和stop()
方法中,我们可以编写自定义的启动和停止逻辑,并通过调用lifecycleSupport.fireLifecycleEvent()
方法来触发相应的生命周期事件。
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
public class LifecycleSupport {
private final LifecycleListener[] listeners;
private final Object listenersLock = new Object();
public LifecycleSupport(Object component) {
// 初始化监听器数组
// ...
}
public void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(component, type, data);
// 获取监听器数组的副本
LifecycleListener[] listenersCopy;
synchronized (listenersLock) {
listenersCopy = Arrays.copyOf(listeners, listeners.length);
}
// 通知每个监听器处理事件
for (LifecycleListener listener : listenersCopy) {
listener.lifecycleEvent(event);
}
}
public void addLifecycleListener(LifecycleListener listener) {
synchronized (listenersLock) {
// 添加监听器到数组
// ...
}
}
public void removeLifecycleListener(LifecycleListener listener) {
synchronized (listenersLock) {
// 从数组中移除监听器
// ...
}
}
public LifecycleListener[] findLifecycleListeners() {
synchronized (listenersLock) {
// 返回监听器数组的副本
return Arrays.copyOf(listeners, listeners.length);
}
}
}
LifecycleSupport 类的
fireLifecycleEvent()
方法会遍历已注册的监听器,并依次调用它们的lifecycleEvent()
方法,将事件对象传递给监听器进行处理。
import org.apache.catalina.LifecycleException;
public class Main {
public static void main(String[] args) {
MyComponent component = new MyComponent();
MyLifecycleListener listener = new MyLifecycleListener();
component.addLifecycleListener(listener);
try {
component.start();
// 模拟一些操作
System.out.println("Performing some operations...");
// 停止组件
component.stop();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
创建了一个
MyComponent
实例和一个MyLifecycleListener
实例,并将监听器注册到组件上。然后,我们依次调用了组件的start()
和stop()
方法。
Tomcat Loader 载入器
servlet 容器实现一个自定义在载入器的原因时servlet不应该完全信任它正在运行的servlet。使用系统载入器加载某一个servlet会导致它可以使用全部的类,包括Java虚拟机中Classpath 指明路径的类和库。这是不安全。
假设在 Servlet 中使用系统类加载器加载一个自定义的第三方库(如Apache HttpClient)。这个库有访问网络的功能,可以发送 HTTP 请求和接收响应。
现在,假设这个 Servlet 受到了恶意攻击,攻击者可能利用该 Servlet 发起网络攻击,例如发送恶意请求到其他服务器或进行信息泄露等。
由于系统类加载器加载的 Servlet 具有访问整个类路径的能力,它可以加载和使用 Apache HttpClient,从而执行与网络相关的操作。这就使得攻击者能够在 Servlet 中执行恶意的网络操作,而且这是不受控制的,可能对整个系统造成严重影响。
为了避免这种潜在的安全风险,Servlet 容器使用自定义类加载器来加载 Servlet。自定义类加载器可以实现一定程度的隔离,限制 Servlet 只能访问特定的类和资源。通过这种方式,可以减少对整个类路径的访问权限,从而增加安全性和控制性。
例如,Tomcat 使用的 Catalina 类加载器就是一个自定义的类加载器,它实现了对 Servlet 进行隔离加载的功能,从而提供了更高的安全性和隔离性。
Loader 接口
Tomcat 的自定义加载器:
- 只能引用部署在WEB-INF/classes(WEB-INF/lib)目录及其子目录下的类
- servlet无法访问其他路径中的类
public interface Loader {
public ClassLoader getClassLoader();
public Container getContainer();
public void setContainer(Container container);
public DefaultContext getDefaultContext();
public void setDefaultContext(DefaultContext defaultContext);
public boolean getDelegate();
public void setDelegate(boolean delegate);
public String getInfo();
public boolean getReloadable();
public void setReloadable(boolean reloadable);
public void addRepository(String repository);//可以通过添加仓库将WEB-INF/lib添作为一个加载类路径
public String[] findRepositories();
/**
* Has the internal repository associated with this Loader been modified,
* such that the loaded classes should be reloaded?
*/
public boolean modified();//通过,它来进行热加载类库,当类发生变化时,modified会返回ture,通过调用reload()方法来实现具体重载
public void addPropertyChangeListener(PropertyChangeListener listener);
public void removePropertyChangeListener(PropertyChangeListener listener);
}
总结:
载入器会负责加载应用程序需要的类,因此,会使用一个内部类加载器,Tomcat 使用这个自定义的类载入器对Web应用程序上下文中要载入的类进行约束。