Tomcat 内部实现(下)
前言
继上篇文章已经将近已有两个多月。那么,现在,我们来完结这Tomcat相关的内容吧。通过Tomcat上我们知道了Tomcat最核心的内容就是连接器和容器。接下来我们来谈谈Tomcat剩下的部分吧。
Session管理
在 Tomcat 中,session 是指在 Web 应用程序中用于跟踪用户状态和存储用户数据的一种机制。
Session具有一下特点:
用户状态管理:Tomcat 的会话管理机制允许开发人员在用户与应用程序之间建立持久性连接。每当用户访问应用程序时,Tomcat 可以为其创建一个会话,并为每个会话分配一个唯一的会话 ID。
数据存储:会话允许开发人员将数据存储在会话对象中,这些数据可以在用户不同请求之间保持连贯性。这对于存储用户登录信息、购物车内容、用户偏好设置等。
跨页面传递数据:会话对象可以在不同的页面之间传递数据,从而避免了每次请求都需要重新传递数据的问题。这对于用户的连续性体验非常重要。
会话超时管理:Tomcat 允许开发人员设置会话的最大生存时间,即会话超时时间。如果用户在一段时间内没有活动,会话可能会被自动销毁,以释放资源。
会话监听器:Tomcat 提供了会话事件监听器,允许开发人员在会话创建、销毁、属性变更等事件发生时执行特定的操作。
Session接口
在Tomcat中,Session对象由 HttpSession 接口表示。标准实现是StandardSession类。但最终提供servlet使用的是StandardSessionFacade外观类。
Session接口,是控制Session实例对象的有效期性以及设置Session对象的唯一标识符。通过getId来获取Session的Id,getLastAccessedTime()方法判断Session对象的有效性。最后,会通过expire()方法将其设置为过期。并且当我们通过getSesion()方法获取一个Session的时候会返回Session的外观类包装的HttpSession对象。
StandarSession类
StandardSession类是Tomcat默认的实现,用于实现HttpSession接口。
- StandardSession类负责存储Session的数据。它内部会维护一个属性集合,用于存储各种用户特定的数据。可以使用
setAttribute
方法设置属性,使用getAttribute
方法获取属性值。 - StandardSession类跟踪每个Session的创建时间和最后访问时间。创建时间通过构造函数初始化,而最后访问时间会在每次访问Session时更新。
StandarSessionFacade类
Catalina传递给Session对象是StandarSessionFacade类,Session的外观类,它只实现了HttpSession接口中的方法,这样servlet程序员就不可以将HttpSession对象转型为StandardSession类型,缩小可以使用的方法。
csharp
public HttpSession getSession(){
if(facade == null){
facade = new StandardSessionFacade(this);
return (facade);
}
}
Manager接口
Manager
接口是用于管理 Session
对象的核心接口之一。Manager
接口定义了一些方法,用于创建、删除、获取和维护 Session
对象,以及处理 Session
的过期和持久化等操作。不同的 Manager
实现可以有不同的策略来管理 Session
,比如内存存储、持久化到数据库或缓存等。
StandarManager类
StandardManager
作为默认的 Manager
实现,提供基本的 Session
管理功能。它最重要的一个特点就是将Session存储在内存中。并且它实现了Lifecycle接口,这样当相关联的容器启动或者关闭时,就会调用unload方法,将Session对象序列化到"Session.ser"文件中。当Tomcat重新启动的时候就会加载该文件,将session数据读到内存中。
PersistenManager类
PersistentManagerBase
类是 Tomcat 中实现 Manager
接口的一个抽象基类,它为 Session
数据的持久化和恢复提供了框架。这个类并不直接将 Session
数据存储在文件中,而是依赖一个名为 Store
的接口实现来进行数据的存储和检索。
- 持久化和恢复会话数据: 当会话数据需要被持久化(保存在磁盘或数据库中)以便在服务器重启或应用程序重新部署后恢复时,
PersistentManagerBase
负责管理这些操作。 - 定期过期检查: 会话对象可能需要定期清理,以移除过期的会话数据。
PersistentManagerBase
负责定期检查会话的过期状态,并删除过期的会话。 - 基于 Store 接口的数据存储和检索:
PersistentManagerBase
并不直接将会话数据存储在文件中,而是依赖一个名为Store
的接口实现来进行数据的存储和检索。Store
接口定义了会话数据的存储和检索方法,允许实现根据需求将数据存储在不同的后端存储中,如文件系统、数据库等。
注意:Tomcat支持集群复制Session的对象,即DistributedManager类,它继承上面的Base类,它适用于集群环境,可以实现Session对象的复制。
StoreBase类
它是一个抽象类型,提供一些基本功能,它的直接子类范围FileStore和JDBCStore。
FileStore类会将Session对象存储到某一个文件中,文件名通常时session对象的标识符+.session构成。
JDBCStore将session对象存入数据库中。
ini
1.配置Tomcat 的 context.xml
<Context>
<!-- Other context configuration -->
<Manager className="org.apache.catalina.session.PersistentManagerBase">
<Store className="org.apache.catalina.session.JDBCStore"
connectionURL="jdbc:mysql://localhost:3306/myappdb"
driverName="com.mysql.jdbc.Driver"
sessionTable="sessions"
sessionAppCol="app_name"
sessionDataCol="session_data"
sessionIdCol="session_id"
sessionLastAccessedCol="last_accessed"
sessionMaxInactiveCol="max_inactive"
sessionValidCol="valid_session"
sessionVersionCol="version"/>
</Manager>
</Context>
2.创建对应的表结构
CREATE TABLE sessions (
session_id VARCHAR(255) NOT NULL PRIMARY KEY,
app_name VARCHAR(255),
session_data BLOB,
last_accessed TIMESTAMP,
max_inactive INT,
valid_session BOOLEAN,
version INT
);
3.编写对应的请求
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
public class SessionExampleServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(true);
session.setAttribute("username", "john_doe");
response.getWriter().println("Session data stored in database.");
}
}
StandardWrapper
再说回来连接器,每当一个Http请求进入连接器,连接器就会调用关联的servlet容器的invoke()方法。接着,servlet容器就会调用子容器的invoke()方法。
具体流程是:
-
Catalina 容器层:
- 在 Catalina 层,每个
StandardContext
实例代表一个 Web 应用程序。StandardContext
是Context
接口的一个具体实现。 - 当收到一个 HTTP 请求时,
StandardContext
的invoke()
方法被调用。接着StandarWrapperValve
实例的invoke()
方法调用Wrapper的allocate()
方法请求一个servlet
实例。
- 在 Catalina 层,每个
-
管道(Pipeline)和阀门(Valve):
StandardContext
内部拥有一个请求处理管道,称为Pipeline
。这个管道由一系列阀门组成,每个阀门负责特定的处理任务。- 首个阀门通常是
StandardContextValve
,它是一个实现了Valve
接口的类,负责启动整个请求处理流程。
-
阀门的
invoke()
方法:- 当
StandardContext
的invoke()
方法被调用时,它会调用管道的首个阀门的invoke()
方法,即StandardContextValve
的invoke()
方法。
- 当
-
阀门链式调用:
StandardContextValve
的invoke()
方法中,它会调用下一个阀门的invoke()
方法,这样就形成了一系列阀门的链式调用。- 每个阀门可以在请求到达、处理阶段和响应返回时进行一些操作,然后将控制权传递给下一个阀门。
-
Servlet 的
StandardWrapper
:- 在阀门链中的某个阶段,请求会被传递到关联的
StandardWrapper
,即 Servlet 的封装器。它的service()
方法被调用来处理请求。 - 这里涉及到的阀门可能包括身份验证、会话管理、安全性检查等。
- 在阀门链中的某个阶段,请求会被传递到关联的
-
阀门链的结束:
- 在阀门链的最后,响应会被传递回上层阀门,最终返回给客户端。
注意:servlet通过实现SingleThreadModel接口实现servelt实例一次只处理一个请求的规范。但是,实现该接口并不是意味着线程是安全的。SingleThreadModel
接口在 Servlet 3.0 规范中已经被废弃,不再被推荐使用,原因是该接口带来了一些性能问题,以及与多线程环境下的一些不稳定性。相反,现代的 Servlet 容器通过线程池等机制来支持并发处理,可以高效地处理多个请求,而无需实现 SingleThreadModel
。
从 Servlet 3.0 开始,不再需要实现 SingleThreadModel
接口,Servlet 容器会自动管理多线程情况下的并发问题。每个请求都会被分派到一个线程,因此你可以专注于编写线程安全的 Servlet,而无需担心多线程问题。
StandardWrapperFacade类
StandardWrapperFacade
是 Apache Tomcat 中的一个类,它实现了 javax.servlet.Servlet
接口,用于封装 StandardWrapper
实例。这个类的目的是为了提供一个代理(Facade)对象,使得外部组件可以使用 Servlet 接口来访问 StandardWrapper
对象,从而对外部隐藏内部的具体实现细节。
ApplicationFilterConfig类
该类用于管理web应用第一次启动时创建所有的过滤器实例。
ApplicationFilterConfig
在 Web 应用程序初始化阶段,为每个配置的过滤器创建一个对应的 FilterConfig
对象,用于在过滤器的 init()
方法中传递初始化参数和上下文信息。这些过滤器实例会在容器初始化期间被创建,并在需要时调用其 init()
方法进行初始化。
注:通过 init()
方法,每个过滤器实例都会获取到一个 FilterConfig
对象。这个对象包含了关于过滤器的配置信息,例如初始化参数、过滤器名称、上下文信息等。
总结:
StandardWrapper
的工作原理:
- 在 Tomcat 启动时,对于每个部署的 Web 应用程序,都会创建相应的
StandardWrapper
实例。这个实例负责管理一个特定的 Servlet。 - 在 Web 应用程序初始化阶段,
StandardWrapper
负责加载 Servlet 类并创建对应的 Servlet 实例。这是通过调用load()
方法来实现的。load()
方法使用ClassLoader
加载 Servlet 类,然后通过反射创建实例。接着,调用 Servlet 的init()
方法进行初始化。 - 当收到一个 HTTP 请求时,
StandardWrapper
的allocate()
方法负责决定是使用现有的 Servlet 实例还是创建一个新的。如果存在可用的现有实例,它会返回现有实例,否则会调用load()
方法创建新实例。这确保每个请求在一个单独的线程中处理,避免多线程问题。 StandardWrapper
负责将请求和响应对象传递给关联的 Servlet 实例的service()
方法。这是处理请求和执行业务逻辑的关键步骤。在service()
方法中,Servlet 可以根据请求类型(GET、POST 等)来执行相应的操作。- 如果在
service()
方法中出现异常,StandardWrapper
会捕获异常并根据配置的错误页面信息来处理。 - 在 Web 应用程序关闭或卸载时,
StandardWrapper
负责销毁关联的 Servlet 实例。这是通过调用 Servlet 的destroy()
方法来完成的,执行资源释放和清理工作。
服务器组件和服务组件
服务器组件可以帮助我们启动和关闭整个系统,而不是针对连接器和容器进行启动和关闭。
当启动服务器组件时,它会启动其中所有的组件,然后它就会无限等待关闭指令。接着我们通过发送一条关闭命令。服务器组件就会接受,并关闭所有组件。
StandardServer类
是服务组件的标准实现。通过我们通过addservice()方法来添加组件,removeService() 和 findservice() 方法来移除和寻找组件。而standardServer的生命周期分为:
initialize()方法->start()->stop()->await()方法。
StandardService中有两种组件,一种时servlet容器,另一个则是连接器,而连接器可以有多个,提供服务不同多个的请求协议。
initialize
初始化添加其他的服务组件
csharp
public void initialize () throws LifecycleException {
if (initialized)
throw new LifecycleException (
sm.getstring ( "standardServer.initialize.initialized") );
initialized = true;
// Initialize our defined services
for (int i = 0 ; i < services. length; i++){
services [i].initialize();
}
};
initialized布尔变量可以防止服务组件初始化两次。stop并不会重置initialized的值。
start
用于启动组件,接着会启动所有组件
java
public void start () throws LifecycleException {
// Validate and update our current component state
if (started)
throw new LifecycleException(sm.getstring("standardServer.start.started" ) );
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent (BEFORE__START_EVENT,null);
lifecycle.fireLifecycleEvent (START__EVENT,null);
started = true;
//start our defined services
synchronized (services) {
for (int i = 0; i < services. length; i++){
if (services[i] instanceof Lifecycle){
((Lifecycle) services[i ]).start ();
}
}
//Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent (AFTER_.START_EVENT,null) ;
}
}
start布尔变量用来防止服务重启,stop()会重置该变量
stop
用于关闭组件
java
public void stop ( ) throws LifecycleException {
// Validate and update our current component state
if ( ! started)
throw new LifecycleException
( sm. getstring ( "standardserver.stop.notstarted" ) ) ;
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent (BEFORE_STOP_EVENT,null);
lifecycle.fireLifecycleEvent (STOP_EVENT,null) ;
started = false;
//stop our defined services
for (int i = 0; i < services.length; i++){
if (services [i ] instanceof Lifecycle){
((Lifecycle) services [i] ) .stop ( );
}
}
// Notify our interested LifecycleListeners
lifecycle.firelifecycleEvent (AFTER_STOP_EVENT,null) ;
}
关闭所有组件,并且重置started
await
负责等待关闭整个Tomcat
java
public void await{
ServerSocket serverSocket = null;
try{
serverSocket = new ServerSocket(port,1,InetAddress.getByName("127.0.0.1"));
}catch(IOException e){
System.err.println("StandardSErver.await:create error");
e.printStackTrace();
System.exit(1);
}
while(true){
Socket socket =null;
InpuStream stream = null;
try{
socket = serverSocket.accpet();
socket.setSoTimeout(10*1000);
stream = socket.getInputStream();
}catch(IOEXception e){
System.err.println("StandardServer。await:accept error");
e.printStackTrace():
System.exit(1);
}
StringBuffer command = new StringBuffer();
int expected = 1024;
while(expected < shutdown.length()){
if(random == null){
random = new Random(System.currentTimeMillis());
expected += (random.nextInt()%1024);
}
}
while(expected > 0 ){
int ch = -1;
try{
ch = stream.read();
}catch(IOException e){
System.err.println("StanardServer.await:read");
e.printStackTrace();
ch = -1;
}
}
if(ch<32)
break;
command.apped((char)ch);
expected--;
}
try{
socket.close();
}catch(IOException e){
;
}
boolean match = command.toString().equals(shudown);
if(match){
break;
}else{
System.err.prinln("error");
}
try{
serverSocket.close();
}catch(IOException e){
;
}
}
await()方法创建一个ServerSocket对象,监听8085端口,并在while循环中调用它的accept方法、当指定端口接受信息时,才会中accept()方法返回。然后在将接受的消息与关闭命令字符串想比较,相同的话就跳出while循环,关闭SocketServer,不然就会再次循环,等待消息。
关闭钩子
这个思想感觉很有用啊。在很多实际应用中,当用户关闭了应用程序时,需要做一些清理工作。但是,有可能用户并不一定会按照推荐的方法关闭应用程序。例如,在Tomcat中,正常关闭应该发送shudown命令。但是,用户可能直接终止进程。如果,我们对其没有做任何处理的话,就会出现意想不到的事情。
对于关闭钩子来说,虚拟机在执行关闭时,会经过两个阶段:
- 虚拟机启动所有的已注册的关闭钩子。关闭钩子时通过Runtime类注册的线程,关闭钩子会并发执行,直到完成任务。
- 虚拟机根据调用情况。调用所有没有调用过的finalizer
如何创建钩子函数:
- 创建Tread类的子类
- 实现run方法
- 实例化关闭钩子
- 使用Runtime类将钩子进行注册
java
public class test {
public void start(){
System.out.println("Dome");
ShutdownHook shutdownHook = new ShutdownHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
public static void main(String[] args) {
test test = new test();
test.start();
try{
System.in.read();
}catch (Exception e){
;
}
}
static class ShutdownHook extends Thread{
@Override
public void run() {
System.out.println("shutting down");
}
}
}
对于Tomcat来说,也是通过类似的方式来完成的
java
protected class CatalinaShutdownHook extends Thread{
public void run(){
if(server != null){
try{
((Lifecycle)server).stop();
}catch (LifecycleException e){
System.out.println(e);
if(e.getThrowable!=null){
System.out.println("Root Cause");
}
}
}
}
}
启动Tomcat
启动Tomcat是有两个类在起作用,一个是Catalina类和Bootstrap类。
Catalina类用于启动和关闭Server对象,并解析Tomcat配置文件server.xml而Bootstrap是负责创建Catalina实例的。
Catalina类
可以通过命令启动参数来启动Catalina中的main方法来启动程序,但是,即使使用catalina也需要Bootstrap类来实例化Catalina类。
Bootstrap类
当我们通过startup.sh 或者是 .bat文件时,则时调用该类的main方法。其中main方法会载入3个类加载器,并实例化Catalina类。并调用Catalina的process()方法。
而这三个载入器分为为:
-
commonLoader
- 它加载/%CATALINA_HOME%/common/classes目录、/xxx/xxx/endorsed目录、/xxx/xxx/lib目录下的java类
-
catalinaLoader
- 它加载/%CATALINA_HOME%/server/classes目录、/xxx/xxx/lib目录下的java类
-
sharedLoader
- 它加载/%CATALINA_HOME%/shared/classes目录、以及commonLoader类载入器访问的目录下的类
-
sharedLoader
- 它加载/%CATALINA_HOME%/shared/classes目录、以及commonLoader类载入器访问的目录下的类