文章概览
相信很多接触java的人都对Tom猫有着多少的熟悉,就个人而言,本来只知道Tom简单的操作与配置,就像裹上一层纱,迷迷糊糊的.
Tomcat的书籍本来就不多,高分的还是很久之前的版本,直到最近看到下面这本书,解答了我的很多疑问,同时这篇文章将总结读书收获.
如果觉得文章写的内容是你感兴趣的或者我的猫使你感兴趣,建议你读读这本书.
该文会介绍Tom的架构,服务器如何从一层层抽象设计到完整的架构
Tomcat介绍
Tom是一款全世界著名的轻量级应用服务器,基于java,服务于java.主要作为应用服务器来处理客户端发来的动态资源响应.
目前版本是9.x,很多人都在使用6.x,但新版其实提供了很多新的功能,比如WebSocket的支持,点击了解WebSocket.
Tomcat启动参数
windows修改$CATALINA_HOME/bin/catalina.bat文件
Set JAVA_OPTS=-server -Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m
linux修改$CATALINA_HOME/bin/catalina.sh文件
JAVA_OPTS="-server -Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m"
-server Server端启动Tomcat,Client启动Tomcat两者的初始化参数会有所不同
-Xms1024m 初始化堆内存大小
-Xmx2048m 允许的最大堆内存大小
-XX:PermSize=256m 初始化非堆内存大小
-XX:MaxPermSize=512m 允许的最大非堆内存大小
Debug方式
依赖于JDK提供的JPDA(Java Platform Debugger Architecture,Java平台调试体系)
catalina jpda start
目录结构
该文使用的版本是apache-tomcat-9.0.1,目录大致都很容易理解.
conf放置的Tomcat的核心配置文件,下文会介绍.
webapps是默认的web app的应用目录,只要把项目目录放置进去就可以运行
work是Tomcat运行时产生的jsp编译文件所存放的位置
总体架构
Tomcat是一款应用服务器,我们从最根本的类一层一层演变,直至Tomcat当前版本.
1.应用服务器是接收其他计算机(客户端)发来的请求数据并对其解析,完成相关业务处理,然后把处理结果作为响应返回给计算机.
2.一个问题摆在眼前,前面Server请求监听和请求处理放在一起,应用服务器通常会与web服务器进行集群部署和负载均衡,但是这两者的协议并不是HTTP.
也就是说,服务器连接的另一端需要适配不同的协议来对请求作出不同的处理.前面的模型扩展性太差,应该分离请求监听和请求处理.
Connector模块管理请求监听,Container模块负责请求处理,两个组件都拥有start()和stop()来加载和释放自己维护的资源.
这样子,Server下可以有多个Connector来传送请求至不同的Container中.
3.上面的设计有个缺陷,既然server可以有多个Connector和Container,那么如何知道哪个Connector将请求发至哪个Container呢?
考虑一下下面这个设计图,
4.我们接触过的Tomcat应该是放置web app的容器,在哪放置web app?这将决定哪个app来处理Engine所获取的请求信息.
再想一下,我们浏览器是个app,对吧?然后服务器其实也是app对吧?网络就是两者的通信.我们来看一下网络是怎么进行通信的.
没错,web app需要端点信息(IP地址,端口号),我们需要提供这一层的抽象.一个Host下可以对应有多个app(Context).
5.现在设计已经可以满足两个应用的连接了,现在设想一下,应用该怎么进行表示?毕竟Tomcat作为一款Servlet容器而存在.首先Apache组织按照Servlet官方的标准,加入了Servlet的包装类Wrapper.
6.就目前为止,我们使用"容器"这一概念来形容处理接收客户端的请求并且返回响应数据的组件,依此,使用一个类Container来统一表示这一想法,让Engine,Host,Context,Wrapper这类组件来继承Container.
Container类能够添加子组件addChild方法,有时需要执行一些异步处理,所以加入backgroundProcess方法.
由于Engine,Host,Context,Wrapper这类的引用变成了父类Container,所以之前的强组合关系变成了弱组合关系.强弱关系指的是两个类直接关联或者是间接关联.
7.为了从抽象和复用层面上再审视一下当前设计,使概念更加清晰,提供通用性定义.由于所有容器都有着自身的生命周期管理方法,那么我们可以将其进行抽象成一个接口Lifecycle,在方法定义上加入初始化方法init,销毁方法destroy,事件监听方法addLifecycleListener和removeLifecycleListener.
8.上面这个设计Container部分具有伸缩性和扩展性,这很棒.接下来Tomcat的开发人员为了提高每个组件的灵活性,使其更易扩展,加入了Pipeline和Valve这两个接口.这两个接口的设计运用了职责链模式.
简单介绍以下职责链模式,关于设计模式可以查看博客里的文章《软件设计 : 聚焦设计模式》
职责链模式使用一个抽象类来统一定义处理器,然后将处理器构造成一条链,当Client端发来请求时,第一个Handler判断是否处理,不处理则往下个Handler传递,直至被处理或则处理链结束.
回头看Tomcat怎么运用这个设计模式,Pipeline接口用于构建职责链,Valve接口代表职责链上的每个处理器.
Pipeline中维护一个基础的Valve,它始终位于Pipeline执行链的末端,封装了具体的请求处理和输出响应过程.
这样就可以构造一条职责链,可是为什么要这么做?记得之前Container不就是可以自包含的容器吗?为什么要弄出多两个接口?
的确,Container可以自包含,但是它是作为容器抽象类而存在,而阀(Valve)作为接口而存在,我们可以在实现这个接口的类中添加属于我们自己的Valve实现类,你想做什么都行.
就像水管一样,你如果是超级马里奥,你可以随时给它加个阀,做任何事.
9.前面的设计基本落在Container部分,来看看Connector的设计方案,Connector必须完成下面的功能项.
①监听服务器端口,读取客户端的请求
②将请求数据按指定协议进行解析
③根据请求地址匹配正确的容器进行处理
④将请求返回客户端
Tomcat支持多协议(HTTP/AJP)和多种IO方式(BIO,NIO,NIO2,APR,HTTP/2)
ProtocolHandler表示协议处理器,针对不同的协议和IO方式,提供不同的实现,ProtocolHandler包含一个Endpoint用来启动socket监听,该接口按照IO方式进行分类实现,还包含一个Process用于按照指定协议读取数据,并交由容器处理.
处理逻辑如下:
1.在Connector启动时,Endpoint会启动线程来监听服务器端口,并在接收到请求后调用Process进行数据读取.
2.当Process读取客户端请求之后,需要按照地址映射到具体的容器进行处理,即请求映射.
3.由于Tomcat各个组件采用通用的生命周期进行管理,而且通过管理工具进行状态变更,因此请求映射除了考虑映射规则的实现外,还要考虑容器组件的注册和销毁.
Tomcat采用Mapper来维护容器映射信息,按照映射规则(Servlet规范定义)查找容器;
MapperListener实现LifecycleListener和ContainerListener,用于在容器组件状态变更时,注册或者取消对应的容器映射信息;
MapperListener实现了Lifecycle接口,当Service启动时,会自动作为监听器注册到各个容器组件之上,同时将已创建的容器注册到Mapper;
Tomcat通过适配器模式实现了Connector与Mapper,Container的解耦,默认实现为CoyotoAdapter;
10.到这里,服务器可以正常接入请求和完成响应,可是我们还没考虑到一个关键的问题——并发
Tomcat使用组件式的设计理念,那么也会有并发组件.
Tomcat组织为此提供了一个Executor接口表示一个可以在组件间共享的线程池,该接口同样继承自Lifecycle接口,按照通用组件进行管理.
Executor由Service进行维护,因此同一个Service中的组件共享一个线程池.值得注意的是如果没有定义线程池,相关组件会自动创建线程池,此时线程池不再共享.
在Tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户请求会创建请求处理对象,并交由线程池处理,由此支持并发处理客户端请求.
11.现在Tomcat基础的核心组件已经完整了,但是架构其实还有很多组件没有显示出来.Tomcat开发人员为了让使用者很好地使用Tomcat,提供了一套配置环境来支持系统的可配置性——Catalina.
Catalina代表了整个Servlet容器架构,包含了上面所有组件,还有还没谈及的安全,会话,集群,部署,管理等Servlet容器组件.它通过松耦合的方式集成了Coyoto,以完成按照请求协议进行数据读写.同时,还包括启动入口、Shell程序等.
Bootstrap是Catalina的启动入口.
为什么Tomcat不通过Catalina启动,而又提供了Bootstrap?
查看一下Tomcat发布包目录,Bootstrap并不存放于Catalina的lib目录下,而是置于bin目录中.Bootstrap通过反射调用Catalina实例,与Tomcat服务器完全松耦合,它可以直接依赖JRE运行并为Tomcat应用服务器创建共享类加载器,用于构建Catalina实例以及整个Tomcat服务器.
至此,Tomcat的基础核心组件介绍结束,我们回顾一下组件的概念
Server 表示整个Servlet容器,一个Tomcat运行环境只存在一个Server,可存在多个Service.
Service 表示链接器和处理器的集合,同一个Service下的链接器将请求传至该Service下的处理器
Connector 表示链接器,用于监听并转化Socket请求,支持不同协议与IO方式
Container 表示容器组件,能执行客户端请求并返回响应的组件
Engine 表示顶级容器,是获取目标容器的入口
Host 表示Servlet引擎中的虚拟机,提供Host之类的域名信息
Context 表示一个web app应用上下文环境
Wrapper 具体的Servlet包装类
Executor 组件间共享的线程池
Tomcat启动与请求响应
Tomcat类加载器
应用服务器通常会自行创建类加载器以实现更加灵活的控制,这是对规范的实现(Servlet规范要求每个Web应用都有独立的类加载器实例),也是架构层面的考虑.
书中p46对类加载器进行了详细说明
JVM默认提供了三个类加载器来进行类加载,Tomcat在加载器上进行扩展,用来加载应用自身的类.
Bootstrap JVM提供,加载JVM运行的基础运行类,即位于%JAVA_HOME%/jre/lib目录下的核心类库
Extension JVM提供,加载%JAVA_HOME%/jre/lib/ext目录下的扩展类库
System JVM提供,加载CLASSPATH指定目录下或者-classpath运行参数指定的jar包
Tomcat的Bootstrap类即由这个加载器载入
Common 以System为父类加载器,是Tomcat应用服务器顶层的公用类加载器,
其路径common.loader,默认指向$Catalina_Home/lib目录.
Catalina 用于加载Tomcat应用服务器的类加载器,路径为server.loader,
默认为空,此时Tomcat使用Common类加载器加载应用服务器.
Shared 所有Web应用的类加载器,路径为shared.loader,默认为空.
此时使用Common类加载器作为Web应用的父加载器.
Web App 加载WEB-INF/classes目录下未压缩的Class和资源文件以及/WEB-INF/lib目录下的jar包.
该类加载器对当前web应用可见,对其他web应用不可见.