马上年底了,渗透任务渐少,最近需要写内存马工具了,读了一下《深入剖析Tomcat》,理解的还比较浅,做个记录。第一次发这篇文章是在2021年一月,当时读完深入剖析Tomcat做了个笔记。现在的文章是2022年五月进行了重新的修改,在写完内存马工具之后,对于Tomcat又有了新的理解。另外,文章加入了读李号双深入拆解Tomcat的总结。
1. Servlet
一般我们写个普通的Java程序Hello World,用到的是JavaSE(基于JDK的开发),而要开发Java Web程序用到的是JavaEE(基于JavaSE,基于服务器的组件、API标准)。Servlet是JavaEE规范中的一个,所有支持Servlet API的Web服务器也称为Servlet容器。Servlet容器有Tomcat、Weblogic、Jboss、Jetty、Resin、TongWeb等。这篇文章写的是Tomcat。同样Tomcat也具备了HTTP请求的功能,也就是它可以处理Socket连接(TCP/IP层)。
Servlet需要用到javax.servlet和javax.servlet.http。
javax.servlet.servlet接口中声明了五个方法,包括init\service\destory\getServletConfig\getServletInfo
。前三个属于生命周期,一旦实例化servlet类,就会调用唯一一次init()方法。当servlet的客户端请求(request)到达后,servlet容器调用service方法将javax.servlet.servletRequest对象和javax.servlet.servletResponse对象作为参数传入。service方法在servlet生命周期内会被多次调用。载入servlet类可以使用java.net.URLClassLoader类的loadClass方法。
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
javax.servlet.http中有HttpServlet类,变量中包含了HTTP各种method,如DELETE、HEAD、GET、OPTIONS、POST、PUT、TRACE等。对于每种method都有doXXX的方法,以get为例,doGet方法如下:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_get_not_supported");
this.sendMethodNotAllowed(req, resp, msg);
}
这些doXXX的方法都被集成在该类的service方法中被调用。
我们想要写一个servlet,就需要继承HttpServlet,然后重写doGet或doPost方法。但是有些地址访问的时候会出现重定向,那么要在doGet中加一句resp.sendRedirect(redirectToUrl);
。重定向有两种:一种是302响应,称为临时重定向,一种是301响应,称为永久重定向。两者的区别是,如果服务器发送301永久重定向响应,浏览器会缓存/hi到/hello这个重定向的关联,下次请求/hi的时候,浏览器就直接发送/hello请求了。还有一种内部转发,就是此servlet会交给另一个servlet来处理这个请求。
另外,需要提到的是支持servlet API的服务器除了Tomcat,Jetty、GlassFish、WebLogic、WebSphere这些服务器也支持。所以在内存马应用的时候一般分为Tomcat内存马、Weblogic内存马。
2. 架构
先放上一张网上的Tomcat总体架构图。可以看到主要分为两部分:连接器connector
和容器container
。连接器解决的是HTTP请求和解析的问题,将网络字节流转化成Request和Response对象。容器解决的是加载、管理Servlet并具体Request请求。连接器负责对外交流,容器则是内部管理。
架构图转换成具体的流程架构图如下
连接器Connector
连接器解决了网络请求和解析的问题,无论协议是什么,最终在容器中获取到的都是ServletRequest对象。连接器具备的功能包括:
(1)网络通信的功能:监听网络端口、接收网络请求、读取网络请求字节流——EndPoint
(2)应用层协议解析:根据协议(HTTP/AJP)解析字节流生成Tomcat Request对象、将Tomcat Response转换成网络字节流——Processor
(3)Tomcat和Servlet标准对象之间转化:将Tomcat Request对象转换成符合Servlet标准的ServletRequest对象,调用Servlet容器得到ServletResponse转换成Tomcat Response对象——Adapter
从网络处理的逻辑来看,EndPoint
提供字节流给Processor
,Processor
提供Tomcat Request
对象给 Adapter
,Adapter
提供ServletRequest
对象给容器Container。这里要提一句,为什么要对Request进行转换。因为解析HTTP生成的是Tomcat自定义的Request,但它不符合Servlet规范,所以无法调用Servlet容器中的内容。Tomcat引入的CoyoteAdapter适配器就是为了解决这个问题。
网络通信的I/O模型可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议则可能是HTTP、HTTPS或AJP的,二者之间有多种组合,所以Tomcat设计了ProtocolHandler
接口来封装这二者。换言之ProtocolHandler
组件包含了EndPoint
和Processor
。根据不同的I/O模型和应用协议对接口进行不同的实现,如Http11NioProtocol、AjpNioProtocol等。但是每种协议也有自己的抽象基类:AbstractHttp11Protocol、AbstractAjpProtocol。
EndPoint
:是通信端点,即通信监听的接口,接收和发送具体的Socket,实现了TCP/IP 协议。其抽象实现类是 AbstractEndpoint,具体实现类根据IO模型的不同包括:NioEndpoint 和 Nio2Endpoint 。监听接口的功能由其子类Acceptor来实现,处理Socket请求则是SocketProcessor实现,它会将请求交给线程池(也叫执行器,Executor)。
Processor
:接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response。实现的是HTTP协议,根据协议的不同具体的实现类包括 AJPProcessor、 HTTP11Processor 等,实现了协议的解析方法和请求处理方法。
容器Container
根据流程架构图可以看出,容器采用的不是平行的模块设计,而是父子关系Engine->Host->Context->Wrapper,即引擎->虚拟主机->WEB应用程序->Servlet。父到子是一对多的关系,例如一个虚拟主机。这种层次关系也在Tomcat的server.xml 配置文件有所体现。所有这些容器组件都实现了Container接口,采用组合模式进行管理。Container实现了Lifecycle接口,统一管理各组件的生命周期。
public interface Container extends Lifecycle {
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}
对于多层次的设计结构,有个问题是ServletRequest最终对应的是那个Wrapper?Tomcat设计了Mapper结构,该结构保存了Web应用的配置信息:组件与访问路径的映射关系。将用户请求的URL定位到一个Servlet。
从Engine到Wrapper逐层传递请求的过程,是通过Pipeline-Valve来实现的。它的设计思想是责任链模式。也就是请求处理过程中有多个Valve阀对请求进行处理,每个Valve处理完自己的职责后传递给下一个Valve。
public interface Valve {
public Valve getNext(); //获取下一个节点
public void setNext(Valve valve);
public void invoke(Request request, Response response) //处理请求
}
public interface Pipeline extends Contained { //Pipeline维护valve链表
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}
可以看到Pipeline中并不存在invoke这种调用方法,实际的方法调用都是由Valve来执行。Pipeline中有getBasic和setBasic。这里的一个知识点:Basic Valve是Valve链的末端节点,负责调用下层容器组件Pipeline的第一个Valve。
通过上图可以看到Basic调用的是下层容器的第一个Valve。而Engine作为顶层容器,它的Valve是由Adapter调用的
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Wrapper容器的最后一个 Valve会创建一个FilterChain,并调用 doFilter() 方法,最终调用Servlet的service方法。
3.生命周期管理
Container中提到,它实现了Lifecycle接口,统一管理组件的生命周期。Tomcat有很多组件,在服务启动时需要创建这些组件;服务停止时需要销毁组件。也就是这些组件的生命周期需要被动态的管理。李号双在讨论Tomcat时提到,程序设计需要找到系统的变化点和不变点。不变点可以被抽象成一个接口。无论是哪个组件都要经历初始化、启动、停止、销毁的过程,只是具体的实现方式不同。所以可以认为初始化等过程是一个不变点,可以被抽象为LifeCycle(生命周期),包含init()、start()、stop()、destory()
。上面的容器章节中还提到,Container的管理是采用的组合模式,在生命周期中可以理解为,当父组件调用init()
时会调用其子组件的init()
。
组件的init()和start()都是由父组件状态变化触发的,状态的变化可以看作是一个事件。如果想要修改init()或start()的逻辑,可以通过事件监听器(观察者模式)来实现,而无需更改组件本身的代码。也就是在LifeCycle中加入两个方法:添加和删除监听器(addLifecycleListener、removeLifecycleListener
)。另外需要定义一下组件生命周期有哪些状态,如NEW、INITIALIZING、INITIALIZED、 STARTING_PREP、STARTING、STARTED
等。
Lifecycle接口的实现类有很多,因为每个组件都有自己的生命周期。但是往往这些实现中又存在相同的逻辑,例如状态的改变、事件的触发等。这些公用逻辑就可以被放入到基类中。然后让子类继承这个基类,就实现了代码的重用。基类中会存在一些抽象方法,也就是基类中不作具体实现的方法,由子类来实现。它是逻辑的骨架。
// LifeCycleBase.init
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {//状态合法性检查,例如当前状态必须是new才能进行实例化
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
{
try{
setStateInternal(LifecycleState.INITIALIZING, null, false); //触发INITIALIZING事件监听器
initInternal(); //调用具体子类的初始化方法,进而调用子组件的init方法
setStateInternal(LifecycleState.INITIALIZED, null, false); //触发INITIALIZED事件监听器
}catch(Throwable t){...}
}
LifeCycleBase调用监听器的方法,那么这里还涉及到一个问题,监听器在什么时候如何被加入到系统中?主要分为两种情况:(1)Tomcat自定义监听器,在父组件创建子组件的过程中注册到子组件(2)server.xml中用户自定义的监听器,Tomcat启动时自动解析server.xml。
4. Server和Service
Tomcat的启动是通过/bin目录下的startup.sh来启动JVM,运行启动类Bootstrap。Bootstrap初始化类加载器,并实例化Catalina。Catalina解析server.xml并创建Server组件。Server启动Service组件。Service启动容器和连接器。这些启动相关的类或组件并不处理具体请求,主要负责管理。
startup.sh -> Bootstrap -> Catalina -> Server -> Service
Catalina
public void start() {
if (getServer() == null) { // 如果Server 实例为空,就解析 server.xml 创建Server
load();
}
if (getServer() == null) {
log.fatal(sm.getString("catalina.noServer")); // 如果创建失败,报错退出
return;
}
try {
getServer().start(); // 启动 Server
} catch (LifecycleException e) {
return;
}
if (useShutdownHook) { // 创建并注册关闭钩子
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
if (await) { // 用 await 方法监听停止请求
await();
stop();
}
}
5.I/O模型
UNIX 系统下的 I/O 模型有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。所谓的I/O就是就是计算机内存与外部设备之间拷贝数据的过程。网络I/O通信的过程中会涉及到两个对象:调用I/O操作的用户线程、操作系统内核。用户线程是不能直接访问内核空间的。用户线程发起I/O操作后,网络数据读取主要有两步:
(1)用户线程等待内核将数据从网卡拷贝到内核空间
(2)内核将数据从内核空间拷贝到用户空间
而各类I/O模型的区别就是它们实现这两个步骤的方式不同。I/O模型图示如下:
同步阻塞是用户线程发起read后就阻塞了,让出CPU。直到数据从内核空间拷贝到用户空间,再把用户线程唤醒。同步非阻塞就是用户线程不断发起read,数据如果没到内核空间就会返回失败。直到数据到了内核空间,并向用户空间拷贝的过程中阻塞。多路复用是线程先发起select,询问内核空间的数据是否准备完成。一旦内核数据准备好了用户线程发起read。称为多路复用的原因是,一次select掉用可以向内核查询多个数据通道Channel的状态。异步是用户线程发起read的同时注册一个回调函数,然后返回read。内核数据准备完成后再调用指定回调函数完成处理,这个过程中用户线程一直没有阻塞。
NioEndPoint
Tomcat的NioEndPoint组件实现的就是I/O多路复用模型。创建Selector后注册相应事件,然后调用select方法,等待事件发生。一旦发生就创建一个新的线程从Channel中读数据。
NioEndPoint共有五个组件:
LimitLatch、Acceptor、Poller、SocketProcessor
和Executor
。
LimitLatch:控制最大连接数。NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。
Acceptor:监听连接请求,在死循环中调用accept方法接收新的连接。一旦有新的连接就返回一个Channel对象并交给Poller处理。(位于单独线程)
Poller:检测Channel的I/O事件,一旦有Channel可读就创建SocketProcessor任务类给线程池Executor处理。(位于单独线程,本质是一个Selector)
Executor:线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor(应用层协议的封装)来读取和解析请求数据。
Nio2EndPoint
Nio是同步非阻塞,NIO.2 则是异步。Java NIO.2实现服务器Demo如下
// 异步服务器
public class Nio2Server {
void listen(){
//1. 创建一个线程池,用来执行来自内核的回调请求
ExecutorService es = Executors.newCachedThreadPool();
//2. 创建异步通道群组,并绑定一个线程池
AsynchronousChannelGroup tg = AsynchronousChannelGroup.withCachedThreadPool(es, 1);
//3. 创建服务端异步通道,绑定到异步通道群组
AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open(tg);
//4. 绑定监听端口
assc.bind(new InetSocketAddress(8080));
//5. 监听连接,传入回调类处理连接请求,另外传入的this是Nio2Server对象本身
assc.accept(this, new AcceptHandler());
}
}
// 回调类
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Nio2Server> {
@Override
public void completed(AsynchronousSocketChannel asc, Nio2Server attachment) {
// 调用 accept 方法继续接收其他客户端的请求
attachment.assc.accept(attachment, this);
//1. 先分配好 Buffer,告诉内核,数据拷贝到哪里去
ByteBuffer buf = ByteBuffer.allocate(1024);
//2. 调用 read 函数读取数据,除了把 buf 作为参数传入,还传入读回调类
channel.read(buf, buf, new ReadHandler(asc));
}
public interface CompletionHandler<V,A> { //模板参数 V 和 A,分别表示 I/O 调用的返回值和附件类
void completed(V result, A attachment); // I/O 操作成功时调用
void failed(Throwable exc, A attachment); I/O 操作失败时调用
}
回调类AcceptHandler
类实现了CompletionHandler
接口的completed
方法。它还有两个模板参数,第一个是异步通道,第二个就是 Nio2Server 本身
比较Nio2Endpoint跟NioEndpoint运行流程图会发现,Nio2Endpoint中没有 Poller 组件,也就是没有 Selector。因为在异步 I/O 模式下,Selector 的工作交给内核来做了。
AprEndpoint
AprEndpoint也实现了非阻塞 I/O,但NioEndpoint 通过调用Java的NIO API来实现非阻塞I/O,而AprEndpoint是通过JNI调用APR本地库而实现非阻塞I/O的。对于Tomcat来说,APR本地库的性能是优于Java的NIO API的,尤其是需要与操作系统进行频繁交互的场景,例如Socket、TLS加密传输。在这些场景下Java和C语言的性能相比有差距。APR就是C语言编写的。Java写的Tomcat想要调用APR需要通过JNI(Java Native Interface)方式来调用,JNI可以使Java调用其他语言编写的程序。
6. Java线程池
程序运行时需要通过使用系统资源(CPU、内存、网络、磁盘等)来完成信息的处理。如果程序需要频繁创建、销毁对象,那么就会产生性能问题。“池”就是用来解决这个问题。
常见的“池”包括:数据库连接池、内存池、线程池、常量池。池将用过的对象保存起来,等下一次需要用这个对象的时候,直接从对象池中取出,避免频繁创建和销毁。
Java线程是对操作系统线程的封装,也是一个对象。创建Java线程也消耗系统资源,因此有了线程池。Web容器一般会把处理请求的工作放到线程池中执行,Tomcat就扩展了原生的Java线程池来满足高并发的需求。
Java原生线程池内部维护了一个线程数组和一个任务队列。当任务处理不过来时就把任务放到队列里慢慢处理。Java线程池的一些核心类包括:ThreadPoolExecutor、FixedThreadPool/CachedThreadPool
。
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //超时时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //工作队列
ThreadFactory threadFactory,
RejectedExecutionHandler handler) // 线程拒绝策略
通过ThreadPoolExecutor
的构造方法可以看出,如果线程数还没达到corePoolSize,当任务来临时就创建新的线程来执行。达到corePoolSize之后,新增的任务就不创建线程而是放入工作队列workQueue中。线程池从workQueue中获取任务。当workQueue队列也满了,线程池就创建临时线程来处理,但如果总的线程数大于maximumPoolSize就不能再创建新的线程,执行拒绝策略handler。如果工作队列中也没有线程需要处理的任务了,该线程可能会被销毁。
FixedThreadPool/CachedThreadPool
这两个类是Java中默认的线程池实现,本质是给ThreadPoolExecutor设置了不同的参数。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); //无界队列
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //对线程个书不做限制
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()); //队列长度为0
}
FixedThreadPool是固定长度(nThreads)的线程数组,多余的任务放到无限长的队列中处理。CachedThreadPool不对线程个数进行限制,多余的任务会无限创建临时线程进行处理。这两个线程池的实现针对于参数是否限制线程个数或队列长度
Tomcat线程池
taskqueue = new TaskQueue(maxQueueSize); //任务队列,限制最大长度
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority()); //线程工厂
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf); //线程池,限制核心线程数minSpareThreads和最大线程池数量maxThreads
Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑。区别在于当队列满了开始创建临时线程总线程数达到maximumPoolSize后,原生的线程池是执行拒绝策略,而Tomcat线程池则是继续尝试将任务添加到队列中。如果缓冲队列也满了,插入失败再执行拒绝策略。代码如下:
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
...
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
// 调用 Java 原生线程池的 execute 去执行任务
super.execute(command);
} catch (RejectedExecutionException rx) { //总线程数达到maximumPoolSize,原生线程池抛出异常
// 如果总线程数达到 maximumPoolSize,Java 原生线程池执行拒绝策略
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
// 继续尝试把任务放到任务队列中去
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
// 如果缓冲队列也满了,插入失败,执行拒绝策略。
throw new RejectedExecutionException("...");
}
7. WebSocket
网络上双向链路通信的两个端点称为Socket。每个Socket对应一个IP地址和端口号,是对TCP/IP协议抽象出来的API,Socket本身不是协议。但WebSocket是一个应用层协议,类似HTTP协议。它通过HTTP协议进行一次握手,握手之后数据直接从TCP层的Socket传输,与HTTP协议不再相关。
WebSocket的数据传输会以frame形式传输,将一条消息分为几个frame,按照先后顺序传输出去。这样大数据可以分片传输,不用考虑数据大小。并且和HTTP 的 chunk 一样,可以边生成数据边传输,提高传输效率。
8. 热部署和热加载
热加载的实现方式是 Web 容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空 Session ,一般用在开发环境。
热部署原理类似,也是由后台线程定时检测 Web 应用的变化,但它会重新加载整个 Web 应用。这种方式会清空 Session,比热加载更加干净、彻底,一般用在生产环境。
Jetty
Tomcat处理HTTP和Servlet的组件分别是Connector(多个)和Container(一个)。在Jetty中则是Connector(多个)和Handler(多个),这两个组件所需的资源都是从全局线程池ThreadPool中获取。Handler根据不同的场景选取对应的Handler,如ServletHandler、SessionHandler。这两个组件的调用都是通过Server类来实现——创建并初始化Connector、Handler、ThreadPool组件,然后调用 start方法启动它们。
如果对Jetty和Tomcat的架构进行对比,(1)Jetty中没有Service的概念,Connector是被所有Handler共享的。(2)在 Tomcat中每个连接器都有自己的线程池,而Jetty中共享一个全局的线程池。
Connector
上文提到Tomcat的Connector的网络通信功能支持多种IO模型,而Jetty主要支持NIO。在介绍Connector工作流程之前,先总结一下NIO。
NIO
Java NIO的核心组件有三个:Channels、Buffers、Selectors
。Channel通道表示一个连接,可以理解为一个Socket。通过Channel可以读写数据,但是这个读写过程并不直接操作,需要通过Buffer来中转。也就是将数据从Channel读到Buffer,也可以从Buffer写到Channel。通道包含很多种如网络IOSockerChannel
、文件IOFileChannel
等。Buffer根据传递数据的基本类型不同也有ByteBuffer、CharBuffer、IntBuffer等。Selector则是位于Thread和Channel中间,允许单线程处理多个 Channel,想要使用Selector,需要向其中注册Channel,调用select方法来调用Channel。NIO模式Demo如下:
ServerSocketChannel server = ServerSocketChannel.open(); //创建服务端 Channel
server.socket().bind(new InetSocketAddress(port)); //绑定监听端口
server.configureBlocking(false); //设置为非阻塞
Selector selector = Selector.open(); //创建Selector
server.register(selector, SelectionKey.OP_ACCEPT); //注册事件OP_ACCEPT,如果有新的连接请求就通知
while (true) {
selector.select();// 查询 I/O 事件
for (Iterator<SelectionKey> i = selector.selectedKeys().iterator(); i.hasNext();) {
SelectionKey key = i.next(); // 遍历SelectionKey列表
i.remove();
if (key.isAcceptable()) { // 如果有注册的事件
SocketChannel client = server.accept(); // 建立一个新连接
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ); // 反馈给Selector,可接收IO读事件
}
}
}
总的来说上述代码完成了三件事:监听连接、IO事件查询、数据读写。这三个分别对应于Jetty的Acceptor、SelectorManager、Connection
。
一些框架
服务接入层:反向代理 Nginx;API 网关 Node.js。
业务逻辑层:Web 容器 Tomcat、Jetty;应用层框架 Spring、Spring MVC 和 Spring Boot;ORM 框架 MyBatis;
数据缓存层:内存数据库 Redis;消息中间件 Kafka。
数据存储层:关系型数据库 MySQL;非关系型数据库 MongoDB;文件存储 HDFS;搜索分析引擎 Elasticsearch
RPC框架:Spring Cloud、Dubbo
网络通信:Netty
分布式协调:Zookeeper
Bootstrap
在org.apache.catalina.startup包下包含catalina类和Bootstrap类(引导类加载器)。前者用于启动或关闭Server对象,并解析Tomcat配置文件。后者则是一个入口点,负责创建Catalina实例,并调用其process方法。Catalina类封装了一个server对象,该对象还有一个service对象。而service对象本身包含一个servlet容器和多个连接器。
Server
org.apache.catalina.core.StandardServer类是Server接口的实现,提供了addService、removeService、findServices等方法,其生命周期包括:initialize、start、stop、await
Service接口的实现类StandardService的initialize方法用于初始化添加到其中的所有连接器,start方法可以启动连接器和所有servlet容器
HTTP请求
HTTP请求在客户端和服务端之间交互,而套接字就是两头的端点,使应用程序可以连接、发送或接收字节流。客户端由Socket类实现,其getOutputStream方法可以获取一个OutputStream对象。要发送文本给服务器的话还需要创建一个java.io.PrintWriter对象。如果客户端想接收服务器端发送的字节流则需要调用getInputStream方法。相应的,服务器端由ServerSocket类实现套接字。它与客户端不同的是,它需要等待来自客户端的连接请求。而应用程序交互过程中主要用三个类:HttpServer、Request、Response。
四种容器
servlet容器是用来处理请求servlet资源,Tomcat中共有四种类型的容器:Engine、Host、Context、Wrapper。一个容器可以又0个或多个低层级的子容器。
Engine:表示整个Catalina servlet引擎
Host:表示包含一个或多个Context容器的虚拟主机
Context:表示一个Web应用程序。一个Context可以有多个Wrapper
Wrapper:表示一个独立的servlet
上述四个接口都继承自Container接口,其标准实现分别为StandardEngine、StandardHost、StandardContext、StandardWrapper。再放上一张网图:
以Engine接口为例:
public interface Engine extends Container {
String getDefaultHost();
void setDefaultHost(String var1);
String getJvmRoute();
void setJvmRoute(String var1);
Service getService();
void setService(Service var1);
}
Wrapper
Wrapper本身具有“包装”的含义。org.apache.catalina.Wrapper接口的实现类主要负责管理其基础servlet类的servlet生命周期,其中有两个重要的方法:load和allocate。前者载入并初始化servlet类,后者分配一个已经初始化的servlet实例。StandardWrapper对象的主要任务是载入它所代表的servlet类,并进行实例化,但是StandardWrapper并不调用servlet的service方法,而是由StandardWrapperValve对象完成,该对象通过调用allocate方法从StandardWrapper实例中获取servlet实例,然后再调用servlet实例的service方法。StandardWrapperValve是StandardWrapper实例中的基础阀,要完成两个操作:
1.执行与该servlet实例关联的全部过滤器
2.调用servlet实例的service方法
具体执行过程大致如下:
1.调用StandardWrapper实例的allocate方法获取该StandardWrapper实例所代表的servlet实例
2.调用私有方法createFilterChain,创建过滤器链
3.调用过滤器链的doFilter方法,其中包括调用servlet实例的service方法
4.释放过滤器链
5.调用Wrapper实例的deallocate方法
6.若该servlet类再也不会被使用到,就调用Wrapper实例的upload方法。
Filter
插入一小段Filter的介绍,Filter即过滤器可以把一些公用逻辑从Servlet中抽离出来,在HTTP请求到达Servlet之前,先被filter预处理。Filter类,也在javax.servlet中,该接口有三个方法:init/doFilter/destory
public interface Filter {
void init(FilterConfig var1) throws ServletException;
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
void destroy();
}
如果我们自己实现一个过滤器,就要重写doFilter方法,并调用chain.doFilter
回到上面StandardWrapperValve的执行过程,createFilterChain会创建一个ApplicationFilterChain实例,并将所有需要应用到该Wrapper实例所代表的servlet实例的过滤器都添加到其中。了解ApplicationFilterChain类就要先了解FilterDef类和ApplicationFilterConfig类。
org.apache.catalina.deploy.FilterDef类表示一个过滤器的定义。该类中的每个属性表示在定义filter元素时声明的子元素。Map类型的变量parameters存储了初始化过滤器时所需要的所有参数。
org.apache.catalina.core.ApplicationFilterConfig类实现了javax.servlet.FilterConfig接口,用于管理web应用程序第一次启动时创建的所有的过滤器实例。
一个Context对象(Web应用程序)+FilterDef对象(过滤器)=构建一个ApplicationFilterConfig(web应用程序的过滤器实例)
org.apache.catalina.core.ApplicationFilterChain类实现了javax.servlet.FilterChain接口,StandardWrapperValue类则可以创建一个ApplicationFilterChain类的实例,调用其doFilter方法。
servlet请求过程:
请求servlet类->StandardWrapper.setServletClass获取servlet类完全限定名/StandardWrapper.setName为servlet类指定一个名字->StandardWrapper载入servlet类->该servlet是否实现了SingleThreadModel接口,实现了就只能载入一次。
HTTP连接过程:
connector->
StandardContext->
StandardContextPipeline->
StandardContextValue->
StandardWrapper->
StandardWrapperValue->
Servlet
Context
Context接口的实现表示一个Web应用程序,其中较为重要的方法是addWrapper和createWrapper。StandardContext对象可以读取并解析默认的web.xml文件,该文件位于%CATALINA_HOME%/conf目录下,该文件的内容会应用到所有部署到tomcat中的应用程序中。StandardContext构造函数中会为其管道对象设置基础阀,类型为org.apache.catalina.core.StandardContextValue,该基础阀会处理从连接器中接收到的每个HTTP请求。它处理请求Wrapper实例的过程是调用Context容器的map方法,并传入org.apache.catalina.Request对象,针对某个特定的协议调用findMapper方法返回一个映射器对象,来获取wrapper实例。
Host
Host容器是org.apache.catalina.Host接口的实例,其map方法可以处理引入的HTTP请求的context容器的实例。Host接口的实现是StandardHost类,它的构造函数会将一个基础阀的实例添加到管道对象中,基础阀是org.apache.catalina.core.StandardHostValue。
管道
管道包含某个servlet容器将要调用的任务,一个阀表示一个具体的执行任务。在servlet容器的管道中,有一个基础阀,还可以通过server.xml额外添加任意数量的阀。管道就像过滤链条,而阀就是其中一个一个的过滤器,基础阀是最后执行的,负责处理request和response对象。
| 阀1 | 阀2 | 阀3 |......|阀n |---->管道
管道有几个重要的接口,如Pipeline、Value、ValueContext等。Pipeline接口中的addValue、removeValue分别代表向管道中添加和删除阀。setBasic方法则将基础阀设置到管道中。Value接口中有个invoke方法,连接器调用容器的invoke方法后,容器会调用管道的invoke方法。ValueContext接口可以访问管道的所有成员,具有invokeNext方法,调用后续的阀。
servlet容器中有一个名为验证器的阀来支持安全限制。当servlet容器启动时,验证器阀会被添加到Context容器的管道中。在调用Wrapper阀之前会先调用验证器阀,对当前用户身份进行验证,如果验证成功就继续调用后续的阀,显示请求的servlet。
用户身份验证依靠“领域”,它会对用户输入的用户名密码进行判断。领域对象通过调用Context容器的setRealm方法与一个Context容器相关联。领域对象是org.apache.catalina.Realm接口的实例。该接口的基本实现类是org.apache.catalina.realm.RealmBase类,该类为抽象类。RealmBase还有一些继承类:JDBCRealm、JNDIRealm、MemoryRealm、UserDatabaseRealm等。默认情况下会使用MemoryRealm类的实例作为验证用的领域对象。登陆配置是通过org.apache.catalina.deploy.LoginConfig类的实例,其getRealmName方法获取领域对象的名字,getAuthName方法获取所使用验证方法的名字(如BASIC\DIGEST\FORM\CLIENT-CERT,分别对应验证器BasicAutherticator\DigestAuthenticator\FormAuthticator\SSLAuthenticator)
而验证器则是org.apache.catalina.Authenticator接口的实例。其实现类为AuthenticatorBase类,该类也是一个阀。
载入器
在catalina中,载入器就是org.apache.catalina.Loader接口的实例。如果要实现的是重载,那么实现的则是org.apache.cataline.loader.Reloader接口。这里有两个术语:repository仓库、resource资源。仓库表示载入器会在哪里搜索要载入的类,资源指的是一个类载入器中的DirContext对象,它的文件根路径指的是上下文的文件根路径。
类载入器
每次创建Java类实例时都需要用载入器将类载入到内存中,在这个过程中,类载入器会在一些核心Java类库及环境变量CLASSPATH中指明的目录中搜索相关类,如果找不到就会抛出java.lang.ClassNotFoundException异常。类载入器由上至下有三种:引导类载入器(bootstrap class loader)、扩展类载入器(extension class loader)、系统类载入器(system class loader)。
引导类加载器用于引导启动Java虚拟机,载入运行JVM所需的类及Java核心类,如java.lang包、java.io包等。扩展类加载器载入标准扩展目录中的类。系统类加载器是默认的类加载器,搜索在环境变量CLASSPATH中指明的路径和JVR文件。
每当需要载入一个类时,先调用系统类载入器,它会把任务再交给其父类加载器,最终交到引导类加载器。如果引导类加载器找不到相关类,就再还给子加载器去寻找。
但是类加载也有限制,servlet只能加载WEB-INF/classes目录及其子目录下的类,不能访问其他路径中的类,即使这些类包含在运行当前Tomcat的JVM的CLASSPATH环境变量中。
Tomcat中的载入器一般指的是WEB应用程序载入器而不仅仅是指类载入器。载入器必须实现org.apache.catalina.Loader接口,重载则是org.apache.catalina.loader.Reloader。Reloader接口中最重要的方法是modified方法,如果web中某个servlet或相关类被修改了,modified方法会返回true。addRepository方法可以用来添加仓库。web应用程序的载入器由Loader接口实现类org.apache.catalina.loader.WebappLoader。当调用WebappLoader类的start方法时,会进行如下工作:创建一个类载入器、设置仓库、设置类路径、设置访问权限、启动一个新线程来支持自动重载。
载入类时,webappClassLoader类要遵循如下规则:
1.所有已经载入的类都会缓存起来,所以载入类时要先检查本地缓存
2.若本地缓存中没有就检查上一层,调用java.lang.ClassLoader类的findLoadedClass方法
3.若启用了SecurityManager,则检查是否允许类载入器进行加载,防止Web应用程序中的类覆盖J2EE的类。
4.若打开标志位delegate或者待载入的类是术语包触发器中的包名,则调用父类载入器来载入相关类,如果父类载入器为null,则使用系统的类载入器。
5.从当前仓库中载入相关类
6.若当前仓库中没有需要的类,且标志位delegate关闭,则使用父类载入器,若父类载入器为null,则使用系统的类载入器进行加载
7.若仍未找到需要的类,则抛出ClassNotFoundException异常。
日志记录器
所有的日志记录器必须实现org.apache.catalina.logger接口
Session
由org.apache.catalina.Manager接口表示Session管理器来管理建立的Session对象。它必须与一个Context容器相关联,servlet实例可以通过调用javax.servlet.http.HttpServlet.Request接口的getSession方法来获取一个Session对象
异常处理
tomcat处理错误消息的方法是将其存储在一个properties文件中,便于读取和编辑。每个properties文件都是用org.apache.catalina.util.StringManager类的一个实例来处理。每个实例会读取某个包下的指定properties文件。