今天是10.1国庆节,放假的第一天因为疫情影响也没有地方可去,所以今天还是来学习吧,为什么要学习Tomcat呢?目前我们一般工作都使用自研框架或者是SpringBoot框架,初级开发的话不太能够接触到Tomcat了,但是这确实我们后端接口使用过程中不可缺少的一部分,只不过在SpringBoot项目中Tomcat已经内置了,只需要我们根据项目需求进行优化或自定义就可以了。学习了Tomcat我们可以加深对请求响应的理解,以及设计模式的使用方法。
注:因为这篇文章不涉及到Servlet与http协议等内容,而且解释起来内容也比较多,但还有所提及。所以小伙伴们不知道这些概念的话希望小伙伴们可以先简单学习一下比较有帮助。
1.下载Tomcat
Tomcat官网地址
下载之后我们要进行解压,此时要记住我们解压之后的文件所在目录。
2.配置环境变量
变量名:CATALINA_HOME
变量值:tomcat所在目录位置
新增之后在path变量下添加新的变量值:%CATALINA_HOME%\bin。
此时我们的Tomcat就安装好了,可以尝试用CMD命令打开命令行,输入startup.bat启动Tomcat。
如果打印信息有乱码,此时我们可以进入Tomcat安装目录 -> conf -> logging.properties文件,找到java.util.logging.ConsoleHandler.encoding这个键值对,将他的值改为GBK,就不会再有乱码了。
此时我们在本地浏览器通过Tomcat测试地址就可以查看本地Tomcat是否安装成功了。安装比较简单,网上教程也有很多,这里只是简单提及一下,毕竟这不是最重点的。
Tomcat源码下载的话可以看一下网上的教程,我这里就不展示了,因为可能图片较多篇幅较长,如果实在不清楚如何下载源码的可以私聊我获取哈。(小提示:如果源码下载后用IDEA打开文件角标为橙色Java,可以在左上角File -> Project Structure -> Modules -> 右侧Add Content Root,先删除后增加)。
如果你在网上搜索什么是Tomcat?一定有很多答案在说Tomcat是一个开源的Servlet容器。那么这句话我们应该怎么理解呢?
Servlet简单来说是一个服务端的小应用,他等于server + applet(不知道很正常,已经过时了),是一个为了简便服务器应用开发而诞生的一个规范,主要是包括初始化、接收请求、销毁的方法需要实现的规范。
在JavaEE框架下的开发流程是这样的,我们在前端发送一个http请求后,会根据域名、IP、端口等传到我们后端的服务器上,Tomcat就是这样一个服务器,他会解析这个http请求,将这个请求根据XML文件的配置传递给我们指定的Servlet,然后我们的Servlet接收请求后再进行处理,处理完毕后再通过Tomcat返回消息给前端。
从这个逻辑里我们不难发现,Tomcat好像是一个管理者的形象,负责管理Servlet的,所以他可以被称为是一个Servlet的容器,与IOC容器类似。
下图为Tomcat的架构图,下面我们来一起分析一下各个部分的作用。
在Tomcat源码中有一个Container的接口,它的下面有这么几个主要的接口。如下接口名称我们可以在server.xml文件中找到同名标签进行配置。
Engine:管理Host主机的管理引擎
Host:管理Context应用的主机,(虚拟主机)可以通过server.xml进行配置,默认只有name为localhost的主机。这也是我们可以通过http://localhost:端口号访问的原因,如果我们有域名的话,可以使用域名来代替localhost,浏览器发送的请求就可以通过域名来判断访问我们服务器Tomcat中的哪个虚拟主机了。appBase属性:访问路径目录,默认是webapps;
Context:管理Servlet对象的应用上下文
Wrapper:由于Servlet默认是单例的,每一个根据指定路径进入的请求都会请求同一个servlet,但是如果是实现了SingleThreadModel或是其他方式将Servlet变为多例的时候,相同的一个Servlet类,但是不同的实例就会包含在一个Wrapper中。
Servlet:相当于一个小程序,一个被标准化的对象。
在这个层级中,除了Servlet之外都可以看作是下一级的父容器,如上面的架构图一样。每个容器中还有着对应的PipeLine(管道),管道中还有valve(阀门),他的作用就像是过滤器一样,可以在servlet执行过程前后添加单独的逻辑。
添加方式:可以在server.xml文件各层级容器标签下添加
我们可以使用java.org.apache.catalina.valves文件夹下准备好的阀门,也可以通过继承RequestFilterValve类,重写invoke方法,来自定义创建阀门,并通过getNext().invoke(request,response)方法来执行下一个阀门。这两种方式都需要通过添加方式在xml中通过classname属性进行注入。
在Tomcat应用部署的时候我们可以部署war包与jar包,那么这两种包的区别是什么呢?其实这两种包没有什么区别,Tomcat应用使用时主要还是需要我们的class文件进行逻辑处理与web.xml文件进行servlet配置,但是如果我们打成了jar包,将他放进Tomcat中,Tomcat无法分辨这个jar包是应用还是普通的依赖,所以Tomcat就需要我们将应用打成war包,区分于jar包,这样才能精准识别哪些是应用,而哪些只是普通的依赖。
在Tomcat源码java.org.apache.catalina.startup.HostConfig类中有一个方法表明了Tomcat的部署方式。
protected void deployApps() {
File appBase = appBase();
File configBase = configBase();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// 通过描述符进行部署
deployDescriptors(configBase, configBase.list());
// 通过war包部署,默认在webapps路径下
deployWARs(appBase, filteredAppPaths);
// 通过文件夹部署,默认在webapps路径下
deployDirectories(appBase, filteredAppPaths);
}
其中的描述符部署需要稍微解释一下,描述符部署是通过创建与server.xml标签层级相同的文件夹并与本地项目相关联。
除了这三种显示启动的情况,我们还可以在server.xml文件中在host标签下添加Context标签与本地文件相关联进行项目部署。
上面我们简单的提到了一下整个JavaEE框架的前端访问后端的大概流程,接下来我们就要关注一下Tomcat具体是怎么做的,并且整个过程中有哪些地方值得我们去关注。
首先给小伙伴们看一下一个基本的Servlet的文件格式吧。(发现报红是因为需要导入javax的依赖包)
javax.servlet
javax.servlet-api
4.0.1
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ServletDemo extends HttpServlet {
/**
* 初始化
* @throws ServletException
*/
@Override
public void init() throws ServletException {
super.init();
}
/**
* 执行业务逻辑
* @param req the {@link HttpServletRequest} object that
* contains the request the client made of
* the servlet
*
* @param resp the {@link HttpServletResponse} object that
* contains the response the servlet returns
* to the client
*
* @throws ServletException
* @throws IOException
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
/**
* 销毁
*/
@Override
public void destroy() {
super.destroy();
}
}
上面的代码有三个,我们需要关注的是第二个方法,执行业务逻辑的方法,我们可以看到这个方法有两个分别是request与response参数,这两个对象的类型都是接口,因为除了Tomcat之外还有其他跟Tomcat拥有一样功能的产品,他们有各自封装的不同请求对象来适应他们的特性,但是只要实现了HttpServletRequest接口,我们就都可以在自己的Servlet中获取请求中的信息并将响应结果返回。
我们今天主要需要了解的就是Tomcat的实现是什么样子的。
我们在Tomcat源码中打开HttpServletRequest接口,查看他的实现类,我们可以看到如下实现类。
其中这两个类就是Tomcat接收请求后封装的对象,也就是Tomcat传输给我们的request对象。
这两个类的关系运用到了设计模式中的门面模式,有时间我会将门面模式整理一下放在下方。
简单来说就是Request是Tomcat的核心类,其中有些内容并不想让其他人看到或使用,所以提出了一个RequestFacade类,将RequestFacade类作为门面,也可以称为简化版进行传递并给他人提供使用。
通过上面我们知道了Tomcat是通过RequestFacade对象传送信息给Servlet的,但是他是如何传送的呢?
我们找到java.org.apache.catalina.core.StandardWrapperValve.invoke方法,当我们发送请求的时候就会调用这个方法,这个方法中有一段逻辑是这样的。
在上述红框代码中我们创建了filterChain对象(过滤链使用了责任链模式,有想要仔细了解的同学可以自行了解哈,暂时我还没有写这篇博客),此时还没有执行他的过滤器处理逻辑。
在这里我们才开始执行他的过滤器逻辑。我们可以发现这个方法执行的参数我们是通过request.getRequest()方法进行获取的。点进去我们可以看到这个方法的逻辑。
由此可见,Tomcat进行传输的对象就是RequestFacade对象。
下面我们进入doFilter方法查看他是如何调用Servlet的。
进入上图红框中方法后,我们可以看到
原来Tomcat是在此处调用了servlet的service方法。
如果学习过Servlet的小伙伴们应该还知道有doGet与doPost方法,他们与service方法有什么区别呢?
其实在service方法中,默认情况也是通过请求类型来区分,区分之后再调用doGet与doPost等方法,这个是Servlet中的规范,Tomcat并不进行区分。源码如下:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
总结:通过上面的内容,我们可以知道Tomcat最终将对象封装成了Request对象,并生成他的门面RequestFacade对象,然后将对象传递到层级容器中,通过层级容器逐层的管道、阀门后再执行Servlet标准下的过滤链,执行通过后再执行servlet对象中的service方法进行业务逻辑处理。
下面我们就要继续学习Tomcat是如何将一个请求封装成RequestFacade对象的。
Socket
在Java中不知道小伙伴们曾经有没有做过聊天室的练习,其中就使用到了Socket,那么什么是Socket呢?
在网络连接中的传输层协议一般熟知的有TCP协议与UDP协议。TCP协议的优点就是可靠性,通过三次握手四次挥手实现,但效率较UDP要慢。UDP的优点就是传输效率快,但是没有可靠性,会因为网络原因丢失部分数据。一般这种协议都是由操作系统来实现的。而Socket就是Java封装的用来调用系统的网络数据传输方法的对象。
我们常使用的端口也是TCP协议去定义的,用于操作系统层面上使用端口去指定应用进行消息传递。
HTTP协议是应用层协议,主要解决数据如何在网络中传输。是不保存状态的协议,即无状态协议。也就是说HTTP协议对于发送过的请求或响应都不做持久化处理。每当有新的请求发送时,就会有新的响应产生。
HTTP协议是建立在TCP协议之上的,个人认为两者的分工不同,在浏览器发送请求 -> 服务器响应这个过程中需要两者相互配合使用。
数据传输的前部分流程就是由http协议确定传输的数据格式。然后再通过TCP协议进行数据传输,服务器同样通过TCP协议进行数据的接收。
在Tomcat源码中的service.xml文件中有这样一个标签,用于定义连接器属性。
我们需要关注protocol属性,此属性在启动时会调用java.org.apache.catalina.connector.Connector.setProtocol方法进行解析。
我们此时进入他解析使用的类中(Tomcat对于http协议的实现类,默认使用BIO模式)。
因为Tomcat需要从Socket中取到数据,这里就需要涉及到IO模型,一般分为BIO(同步阻塞IO)与NIO(同步非阻塞IO) 。
这个endpoint端点对象是用来接收数据的,打开JIoEndpoint类的源码,我们可以找到一个内部类Acceptor,他开启了一个线程用于进行BIO处理。同理,我们在NioEndpoint类的源码中也可以找到同名的内部类。
我们可以使用如下方式将BIO模式切换成NIO模式(修改Connector标签中的protocol属性为NIO全路径类名)。
在JIoEndpoint类的源码中,run方法进行处理时会调用如下processSocket方法。
在JIoEndpoint类的源码中有一个SocketProcessor的内部类,这个类中的run方法比较重要,如下图解释。
进入process方法
进入此实现方法
好啦,到这里整个Tomcat的请求流程就结束了,我们可以再回头看一下Tomcat的架构图,我们就可以慢慢理解这个架构图的含义了。本来想一篇文章结束的,但是感觉内容太多,篇幅太长,所以就分两篇了。
在文章最后的最后再补充一点,我们在本地可以通过http://localhost:8080/去访问Tomcat,也可以通过http://xxx:port/(指定Context名称与端口号去进行访问),但是类似于http://www.baidu.com这种地址他是如何解析的呢?
像这种地址我们需要去注册域名,并将域名与我们的公网IP进行绑定,但是这样就应该是http://www.baidu.com:8080/去访问了,为什么没有加端口号呢?
因为这种暴露端口号的行为是很不安全的,所以我们都会去除端口号,走http协议的默认80端口,再通过nginx反向代理,将80端口的请求分到其他的端口。
下一篇请戳↓
Tomcat源码学习(二)——Tomcat中的Java机制及热部署