Tomcat要实现两个核心功能:
处理Socket连接,负责网络字节流与Request和Response对象的转化;
加载和管理Servlet,以及具体处理Request请求
======》Tomcat设计了两个核心组件连接器(Connector)和容器(Container),连接器负责对外交流,容器负责内部处理
1、连接器是如何设计的?
Tomcat支持的I/O模型有:NIO-->非阻塞I/O,采用Java NIO类库实现;NIO.2-->异步IO,采用JDK 7最新的NIO.2类库实现;APR-->采用Apache可移植运行库实现,是C/C++编写的本地库
Tomcat支持的应用层协议有:HTTP/1.1-->这是大部分Web应用采用的访问协议;AJP-->用于和Web服务器集成(如Apache);HTTP/2-->HTTP 2.0大幅度的提升了Web性能
Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫做Service组件。Tomcat内可能有多个Service,通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用
最顶层是serer,这里的server指的就是一个Tomcat实例。一个server中有一个或者多个service,一个service中有多个连接器和一个容器。连接器与容器之间通过标准的ServletRequest和ServletResponse通信
连接器对Servlet容器屏蔽了协议及I/O模型等的区别,无论是HTTP还是AJP,在容器中获取到的都是一个标准的ServletRequest对象
连接器的功能:server端
- 监听网络端口;
- 接受网络连接请求;
- 读取网络请求字节流;
- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象;
- 将Tomcat Request对象转成标准的ServletRequest;
- 调用Servlet容器,得到SevletResponse;
- 将ServletResponse转成Tomcat Response对象;
- 将Tomcat Response转成网络字节流;
- 将响应字节流写回给浏览器
优秀的模块化设计应该考虑高内聚、低耦合==》高内聚是指相关度比较高的功能要尽可能集中,不要分散;低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖
高内聚功能:
网络通信 ======》 实现:Endpoint
应用层协议解析 ======》 实现:Processor
Tomcat Request/Response与ServletRequest/ServletResponse的转化 ======》 实现:Adapter
组件之间通过抽象接口交互,可以封装变化
Endpoint负责提供字节流给Processor,Processor负责提供Tomcat Request对象给Adapter, Adapter负责提供ServletRequest对象给容器
ProtocolHandler组件:连接器用ProtocolHandler来处理网络连接和应用层协议,包含了2个重要部件:Endpoint和Processor
Endpoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此Endpoint是用来实现TCP/IP协议的
Endpoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)==》Tomcat如何扩展原生的Java线程池?
Processor用来实现HTTP协议,Processor接收来自Endpoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象
Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AjpProcessor、Http11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。
从图中我们看到,Endpoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。
Endpoint如何最大限度地利用Java NIO的非阻塞以及NIO.2的异步特性,来实现高并发?
Adapter组件
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的service方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的service方法
总结:
Tomcat 的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流,容器负责内部处理。连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异,ProtocolHandler 内部又分为 Endpoint 和 Processor 模块,Endpoint 负责底层 Socket 通信,Processor 负责应用层协议解析。连接器通过适配器 Adapter 调用容器。
通过对 Tomcat 整体架构的学习,我们可以得到一些设计复杂系统的基本思路。首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。
2、多层容器的设计
连接器负责外部交流,容器负责内部处理==》连接器处理Socket通信和应用层协议的解析,得到Servlet请求;容器负责处理Servlet请求
容器的层次结构:Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper
Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性
Tomcat的server.xml配置文件:
Tomcat是用组合模式来管理这些容器,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine
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); } //Container接口扩展了Lifecycle接口,Lifecycle接口用来统一管理各组件的生命周期
请求定位Servlet的过程
Tomcat是怎么确定请求是由哪个Wrapper容器里的Servlet来处理的呢==》Tomcat是用Mapper组件来完成这个任务的
Mapper组件的功能就是将用户请求的URL定位到一个Servlet,它的工作原理是:Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里Servlet映射的路径,这些配置信息就是一个多层次的Map
当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里去查找,就能定位到一个Servlet。一个请求URL最后只会定位到一个Wrapper容器,也就是一个Servlet
调用过程具体实现:Pipeline-Valve管道
Pipeline-Valve是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。Valve表示一个处理点,比如权限认证和记录日志
public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void invoke(Requset request, Response response) } //invoke方法就是来处理请求的 //getNext和setNext方法==》有一个链表将Valve链起来了
public interface Pipeline extends Contained { public void addValve(Valve valve); public Valve getBasic(); public void setBasic(Valve valve); public Valve getFirst(); }
Pipeline 中有 addValve 方法。Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。 Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用getNext.invoke来触发下一个 Valve 调用
每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到
不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline,因为 Pipeline 中还有个 getBasic 方法。这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve
整个调用过程是由连接器中的Adapter触发的,它会调用Engine的第一个Valve
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFilter方法,最终会调用Servlet的service方法
Valve和Filter有什么区别:
Valve是Tomcat的私有机制,与Tomcat的基础架构/API是紧耦合的。Servlet API是公有的标准,所有的Web容器包括Jetty都支持Filter机制
Valve工作在Web容器级别,拦截所有应用的请求;而Servlet Filter工作在应用级别,只能拦截某个Web应用的请求
Servlet规范中ServletContext是tomcat的Context实现的一个成员变量,而Spring的ApplicationContext是Servlet规范中ServletContext的一个属性。
3、Tomcat如何实现一键式启停
如果想让一个系统能够对外提供服务,需要创建、组装并启动这些组件;在服务停止的时候,还需要释放资源,销毁这些组件,因此这是一个动态的过程==》Tomcat需要动态地管理这些组件的生命周期
==》如何统一管理组件的创建、初始化、启动、停止和销毁?==》一键式启停:Lifecycle接口
不变点==》每个具体组件的初始化方法,即启动方法是不一样的==》把不变点抽象出来成为一个接口,这个接口跟生命周期有关,叫作Lifecycle。Lifecycle接口里应该定义这几个方法:init、start、stop和destroy,每个具体的组件去实现这些方法
==》在父组件的init方法里需要创建子组件并调用子组件的init方法;在父组件的start方法里也需要调用子组件的start方法,调用者可以无差别的调用各组件的init方法和start方法,这就是组合模式的使用,并且只要调用最顶层组件,也就是Server组件的init和start方法,整个Tomcat就被启动起来了
<interface> LifeCycle void init() void start() void stop() void destroy()
可扩展性:Lifecycle事件
把组件的生命周期定义成一个个状态,把状态的转变看作一个事件。而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式
在Lifecycle接口里加入两个方法:添加监听器和删除监听器。除此之外,还需要定义一个Enum来表示组件有哪些状态,以及处在什么状态会触发什么样的事件
组件的生命周期有NEW、INITIALIZING、INITIALIZED、STARTING_PREP、STARTING、STARTED等,一旦组件到达相应的状态就触发相应的事件,比如NEW状态表示组件刚刚被实例化;而当init方法被调用时,状态就变成INITIALIZING状态,这时就会触发BEFORE_INIT_EVENT事件,如果有监听器在监听这个事件,它的方法就会被调用?
重用性:LifecycleBase抽象基类
定义一个基类实现共同的逻辑,然后让各个子类去继承它,就达到了重用的目的。基类中往往会定义一些抽象方法,调用这些方法来实现骨架逻辑,抽象方法是留给各个子类去实现的,并且子类必须实现,否则无法实例化。
Tomcat定义一个基类LifecycleBase来实现Lifecycle接口,把一些公共的逻辑放到基类中去,比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法。为了避免跟基类中的方法同名,把具体子类的实现方法名字后面加上Internal
//LifecycleBase的init方法实现
@Override public final synchronized void init() throws LifecycleException { //final修饰方法==》此方法不能被继承
//1.状态检查====>检查状态的合法性,比如当前状态必须是NEW然后才能进行初始化 if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { //2.触发INITIALIZING事件的监听器====>在这个setStateInternal方法里,会调用监听器的业务方法 setStateInternal(LifecycleState.INITALIZING, null, false); //3.调用具体子类的初始化方法====>调用具体子类实现的抽象方法initInternal方法,为了实现一键式启动,具体组件在实现initInternal方法时,又会调用它的子组件的init方法 initInternal(); //4.触发INITIALIZED事件的监听器====>子组件初始化后,触发INITIALIZED事件的监听器,相应监听器的业务方法就会被调用 setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { ...... } } //LifecycleBase调用了抽象方法来实现骨架逻辑。LifecycleBase负责触发事件,并调用监听器的方法,那是什么时候、谁把监听器注册进来的呢? 情况一:Tomcat自定义了一些监听器,这些监听器是父组件在创建子组件的过程中注册到子组件的。比如MemoryLeakTrackingListener监听器,用来检测Context容器中的内存泄露,这个监听器是Host容器在创建Context容器时注册到Context中的 情况二:还可以在server.xml中定义自己的监听器,Tomcat在启动时会解析server.xml,创建监听器并注册到容器组件
ContainerBase提供了针对Container接口的通用实现,所以最重要的职责包含两个:
1) 维护容器通用的状态数据
2) 提供管理状态数据的通用方法
容器的关键状态信息和方法有:
1) 父容器, 子容器列表
getParent, setParent, getParentClassLoader, setParentClassLoader;
getStartChildren, setStartChildren, addChild, findChild, findChildren, removeChild.
2) 容器事件和属性监听者列表
findContainerListeners, addContainerListener, removeContainerListener, fireContainerEvent;
addPropertyChangeListener, removePropertyChangeListener.
3) 当前容器对应的pipeline
getPipeline, addValve.
除了以上三类状态数据和对应的接口,ContainerBase还提供了两类通用功能:
1) 容器的生命周期实现,从LifecycleBase继承而来,完成状态数据的初始化和销毁
startInternal, stopInternal, destroyInternal
2) 后台任务线程管理,比如容器周期性reload任务
threadStart, threadStop,backgroundProcess.
4、可以通过Tomcat的/bin目录下的脚本startup.sh来启动Tomcat,执行这个脚本后发生了什么?
- Tomcat本质上是一个Java程序,因此startup.sh脚本会启动一个JVM来运行Tomcat的启动类Bootstrap
- Bootstrap的主要任务是初始化Tomcat的类加载器,并且创建Catalina
- Catalina是一个启动类,它通过解析server.xml、创建相应的组件,并调用Server的start方法
- Server组件的职责就是管理Service组件,它会负责调用Service的start方法
- Service组件的职责就是管理连接器和顶层容器Engine,因此它会调用连接器和Engine的start方法
这些启动类或者组件不处理具体请求,任务主要是“管理”,管理下层组件的生命周期,并且给下层组件分配任务,也就是把请求路由到负责“干活”的组件
Catalina:主要任务就是创建Server,需要解析server.xml,把在server.xml里配置的各种组件一一创建出来,接着调用Server组件的init方法和start方法,这样整个Tomcat就启动起来了。Catalina还需要处理各种“异常”情况,比如通过control+c关闭Tomcat时,Tomcat将如何优雅的停止并且清理资源==》Catalina在JVM中注册一个“关闭钩子”
public void start() { //1.如果持有的Server实例为空,就解析server.xml创建出来 if (getServer() == null) { load(); } //2.如果创建失败,报错退出 if (getServer() == null) { log.fatal(sm.getString("catalina.noServer")); return; } //3.启动Server try { getServer().start(); } catch (LifecycleException e) { return; } //创建并注册关闭钩子====》如果需要在JVM关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向JVM注册一个“关闭钩子”。
//“关闭钩子”其实就是一个线程,JVM在停止之前会尝试执行这个线程的run方法 if (useShutdownHook) { if (shutdownHook == null) { shutdownHook = new CatalinaShutdownHook(); } Runtime.getRuntime().addShutdownHook(shutdownHook); } //用await方法监听停止请求 if (await) { await(); stop(); } }
//Tomcat的“关闭钩子”实际上就执行了Server的stop方法,Server的stop方法会释放和清理所有的资源
protected class CatalinaShutdownHook extends Thread {
@Override
public void run() {
try {
if (getServer() != null) {
Catalina.this.stop();
}
} catch (Throwable ex) {
......
}
}
}
Server组件:Server组件的具体实现类是StandardServer,Server继承了LifecycleBase,它的生命周期被统一管理,并且它的子组件是Service,因此它还需要管理Service的生命周期,在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。Server在内部维护了若干Service组件,它是以数组来保存的,Server如何添加一个Service到数组中?
@Override public void addService(Service service) { service.setServer(this); synchronized (servicesLock) { //创建一个长度+1的新数组 Service results[] = new Service[services.length + 1];
//并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的Service实例时,
//会创建一个新数组并把原来数组内容复制到新数组,目的就是为了节省内存空间 //将老的数据复制过去 System.arraycopy(services, 0, results, 0, services.length); results[services.length] = service; services = results; //启动Service组件 if (getState().isAvailable()) { try { service.start(); } catch (LifecycleException e) { // Ignore } } //触发监听事件 support.firePropertyChange("service", null, service); } }
Server组件还有一个重要的任务是启动一个Socket来监听停止端口,因此可以通过shutdown命令来关闭Tomcat。Catalina的启动方法的最后一行代码就是调用了Server的await方法。在await方法里会创建一个Socket监听8005端口,并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就建立连接,然后从Socket中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入stop流程
Service组件:Service组件的具体实现类是StandardService
public class StandardService extends LifecycleBase implements Service { //名字 private String name = null; //Server实例 private Server server = null; //连接器数组 protected Connector connectors[] = new Connector[0]; private final Object connectorsLock = new Object(); //对应的Engine容器 private Engine engine = null; //映射器及其监听器 protected final Mapper mapper = new Mapper(); protected final MapperListener mapperListener = new MapperListener(this); } //MapperListener:Tomcat支持热部署,当Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,
//它监听容器的变化,并把信息更新到Mapper中,这是典型的观察者模式 protected void startInternal() throws LifecycleException { //1.触发启动监听器 setState(LifecycleState.STARTING); //2.先启动Engine, Engine会启动它的子容器 if (engine != null){ synchronized (engine) { engine.start(); } } //3.再启动Mapper监听器 mapperListener.start(); //4.最后启动连接器,连接器会启动它子组件,比如Endpoint synchronized (connectorsLock) { for (Connector connector: connectors) { if (connector.getState() != LifecycleState.FAILED) { connector.start(); } } } }
Engine组件:Engine本质是一个容器,它继承了ContainerBase基类,并且实现了Engine接口
public class StandardEngine extends ContainerBase implements Engine { } //Engine的子容器是Host,所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBase中,ContainerBase中有这样一个数据结构: protected final HashMap<String, Container> children = new HashMap<>(); //ContainerBase用HashMap保存了它的子容器,并且ContainerBase还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,
//比如ContainerBase会用专门的线程池来启动子容器 for (int i = 0; i < children.length; i ++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); } //Engine在启动Host子容器时就直接重用了这个方法 //容器组件最重要的功能是处理请求,Engine容器对请求的处理,就是把请求转发给某一个Host子容器来处理,具体是通过Valve来实现的。
//每一个容器组件都有一个Pipeline,而Pipeline中有一个基础阀(Basic Valve),Engine容器的基础阀定义如下: final class StandardEngineValve extends ValveBase { public final void invoke(Request request, Response response) throws IOException, ServletException { //拿到请求中的Host容器====>请求对象中为什么会有Host容器?因为请求到达Engine容器中之前,Mapper组件已经对请求进行了路由处理,
//Mapper组件通过请求的URL定位了相应的容器,并且把容器对象保存到了请求对象中 Host host = request.getHost(); if (host == null) { return; } //调用Host容器中的Pipeline中的第一个Valve host.getPipeline().getFirst().invoke(request, response); } }
要选用合适的数据结构来保存子组件,比如 Server 用数组来保存 Service 组件,并且采取动态扩容的方式,这是因为数组结构简单,占用内存小;再比如 ContainerBase 用 HashMap 来保存子容器,虽然 Map 占用内存会多一点,但是可以通过 Map 来快速的查找子容器。
5、NioEndpoint组件
Tomcat的NioEndpoint组件实现了I/O多路复用模型
LimitLatch是连接控制器,它负责控制最大连接数,NIO模式下默认是10000,达到这个阈值后,连接请求被拒绝
Acceptor跑在一个单独的线程里,它在一个死循环里调用accept方法来接收新连接,一旦有新的连接请求到来,accept方法返回一个Channel对象,接着把Channel对象交给Poller去处理
Poller的本质是一个Selector,也跑在单独线程里。Poller在内部维护一个Channel数组,它在一个死循环里不断检测Channel的数据就绪状态,一旦有Channel可读,就生成一个SocketProcessor任务对象扔给Executor去处理
Executor就是线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor来读取和解析请求数据。Http11Processor是应用层协议的封装,它会调用容器获得响应,再把响应通过Channel写出
LimitLatch
LimitLatch用来控制连接个数,当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减1。到达最大连接数后操作系统底层还是会接收客户端连接,但用户层已经不再接收
public class LimitLatch { private class Sync extends AbstractQueuedSynchronizer { @Override protected int tryAcquireShared() { long newCount = count.incrementAndGet(); if (newCount > limit) { count.decrementAndGet(); return -1; } else { return 1; } } @Override protected boolean tryReleaseShared(int arg) { count.decrementAndGet(); return true; } } private final Sync sync; private final AtomicLong count; private volatile long limit; //线程调用这个方法来获得接收新连接的许可,线程可能被阻塞 public void countUpOrAwait() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //调用这个方法来释放一个连接许可,那么前面阻塞的线程可能被唤醒 public long countDown(){ sync.releaseShared(0); long result = getCount(); return result; } }
LimitLatch内部定义了内部类Sync,而Sync扩展了AQS,AQS是Java并发包中的一个核心类,它在内部维护一个状态和一个线程队列,可以用来控制线程什么时候挂起,什么时候唤醒。可以扩展它来实现自己的同步器,实际上Java并发包里的锁和条件变量等等都是通过AQS来实现的,这里的LimitLatch也不例外