作者:图灵学院-周瑜
Tomcat历史悠久,从正式发布到现在已经20多年了,但是就算在现在仍然占有一席之地,Spring Boot目前很火,而Tomcat则是它默认的Web服务器实现,那么Tomcat为什么这么重要,接下来我们来分析一下Tomcat是如何接收,并处理一个请求的?
大家都知道,Tomcat启动时需要绑定一个端口,默认为8080,一旦Tomcat启动成功就占用了机器的8080端口,也就是表示,机器中8080端口接收到的数据会交给Tomcat来处理,机器接收到的数据肯定是字节流,那么Tomcat是如何解析这些字节流的?解析为什么对象?
我们知道,Tomcat是一个Servlet容器,虽然我们在使用Tomcat时,会把整个项目打成一个war包部署到Tomcat里去,整个项目中包括了很多类,但是对于Tomcat而言,它只关心项目中的那些实现了Servlet接口的类,或者说只关心在web.xml中或通过@WebServlet注解所定义的Servlet类。
比如,我们把一个项目部署到Tomcat的wabapps文件夹里后,Tomcat启动时会去找该项目文件夹中的/WEB-INF/web.xml文件,并解析它,解析完了之后,就知道当前项目中定义了哪些Servlet,并且这些Servlet对应接收什么格式的请求(servlet-mapping)
Tomcat启动后,在接收到某个请求后,就可以根据请求路径和url-pattern进行匹配,如果匹配成功,则会把这个请求交给对应的servlet进行处理。
上面只是请求处理的一个大概流程梳理,这其中还有很多细节需要我们挖掘,比如:
- Tomcat是如何解析字节流的?解析为什么对象?
- 我们定义Servlet时会去定义doGet、doPost等方法,那么Tomcat是如何判断该调哪个方法,并且是如何调用的?
- 其他问题
我来看一下我们定义的Servlet:
public class ServletDemo extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {resp.getWriter().println("hello servlet");}}
上面代码中,我们定义了一个doGet(HttpServletRequest req, HttpServletResponse resp)方法,并且有两个参数,一个代表请求,一个代表响应,我们知道doGet方法就是在接收到get请求时会被调用,事实上,Tomcat接收到一个请求(字节流)后,就会将字节流解析为一个HttpServletRequest对象,然后根据请求找到对应的Servlet,然后调用Servlet中的service()方法,service()方法在我们定义的Servlet的父类HttpServlet中,而在service方法中,会去判断当前请求方法,如果是get请求,就会调用doGet,如果是post,就会调用doPost,service方法实现如下:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String method = req.getMethod();long lastModified;if (method.equals("GET")) {lastModified = this.getLastModified(req);if (lastModified == -1L) {this.doGet(req, resp);} else {long ifModifiedSince = req.getDateHeader("If-Modified-Since");if (ifModifiedSince < lastModified) {this.maybeSetLastModified(resp, lastModified);this.doGet(req, resp);} else {resp.setStatus(304);}}} else if (method.equals("HEAD")) {lastModified = this.getLastModified(req);this.maybeSetLastModified(resp, lastModified);this.doHead(req, resp);} else if (method.equals("POST")) {this.doPost(req, resp);} else if (method.equals("PUT")) {this.doPut(req, resp);} else if (method.equals("DELETE")) {this.doDelete(req, resp);} else if (method.equals("OPTIONS")) {this.doOptions(req, resp);} else if (method.equals("TRACE")) {this.doTrace(req, resp);} else {String errMsg = lStrings.getString("http.method_not_implemented");Object[] errArgs = new Object[]{method};errMsg = MessageFormat.format(errMsg, errArgs);resp.sendError(501, errMsg);}}
那么,Tomcat接收到数据并解析为HttpServletRequest对象后,就直接把请求交给Servlet了吗?
并不是,Tomcat还有其他考虑,比如:
- 假如,现在有一段逻辑,想让多个Servlet公用,就像切面一下,我们希望在请求被这些Servlet处理之前,能先执行公共逻辑。
- 假如,现在有一段逻辑,想让多个应用内的Servlet公用,就像切面一下,我们希望在请求被这些应用处理之前,能先执行公共逻辑。
- 我们知道,不同的域名可以对应同一个IP地址,对应同一个机器,那么我希望Tomcat能根据不同的域名做不同的处理
- 如果Tomcat支持多域名,那么我希望能使得这多个域名能共享某段逻辑
可能上面这四种假设,大家会有点懵,没关系,请直接往下面看,在Tomcat中存在四大Servlet容器:
- Engine:直接理解为一个Tomcat即可,一个Tomcat一个Engine
- Host:一个Host表示一个虚拟服务器,可以给每个Host配置一个域名
- Context:一个Context就是一个应用,一个项目
- Wrapper:一个Wrapper表示一个Servlet的包装,Wrapper在后文详解
并且这四个Servlet容器是具有层次关系的:一个Engine下可以有多个Host,一个Host下可以有多个Context,一个Context下可以有多个Wrapper,一个Wrapper下可以有多个Servlet实例对象。
Tomcat接收到某个请求后,首先会判断该请求的域名,根据域名找到对应的Host对象,Host对象再根据请求信息找到请求所要访问的应用,也就是找到一个Context对象,Context对象拿到请求后,会根据请求信息找到对应的Servlet,那么Wrapper是什么?
我们在定义一个Servlet时,如果额外实现了SingleThreadModel接口,那么就表示该Servlet是单线程模式:
- 定义Servlet时如果没有实现SingleThreadModel接口,那么在Tomcat中只会产生该Servlet的一个实例对象,如果多个请求同时访问该Servlet,那么这多个请求线程访问的是同一个Servlet对象,所以是并发不安全的
- 定义Servlet时如果实现了SingleThreadModel接口,那么在Tomcat中可能会产生多个该Servlet的实例对象,多个请求同时访问该Servlet,那么每个请求线程会有一个单独的Servlet对象,所以是并发安全的
所以,我们发现,我们定义的某个Servlet,在Tomcat中可能会存在多个该类型的实例对象,所以Tomcat需要再抽象出来一层,这一层就是Wrapper,一个Wrapper对应一个Servlet类型,Wrapper中有一个集合,用来存储该Wrapper对应的Servlet类型的实例对象。
所以一个Context表示一个应用,如果一个应用中定义了10个Servlet,那么Context下面就有10个Wrapper对象,而每个Wrapper中可能又会存在多个Servlet对象。
还有一点,在这个四个容器内部,有一个组件叫做Pipeline,也就管道,每个管道中可以设置多个Valve,也就阀门。
管道与阀门的作用是,每个容器在接收到请求时会先把请求交给容器中的每个阀门处理,所有阀门都处理完了之后,在会将请求交给下层容器,通过这种机制,就解决了上面所提到的四种假设:
- Engine:可以处理Tomcat所接收到所有请求,不管这些请求是请求哪个应用或哪个Servlet的。
- Host:可以处理某个特定域名的所有请求
- Context:可以处理某个应用的所有请求
- Wrapper:可以处理某个Servlet的所有请求
值得一说的是,Wrapper还会负责调用Servlet对象的service()方法。到此,Tomcat接收到字节流并解析为HttpServletRequest对象之后,HttpServletRequest对象是如何流转的给大家分析完了,总结一下就是:
接下来再来给大家分析一下,Tomcat是如何将字节流解析为HttpServletRequest对象的,这个问题其实不难,只要想到这些字节流是谁发过来的?比如浏览器,而浏览器在发送数据时,会先将数据按HTTP协议的格式进行包装,在把HTTP数据包通过TCP协议发送出去,Tomcat就是从TCP协议中接收到数据的,只是从TCP协议中接收的数据是字节流,接下来要做的就是同样按照HTTP协议进行解析,比如解析出请求行、请求头等,从而就可以生成HttpServletRequest对象。不过,整个解析过程还是比较复杂的,包括Tomcat底层的NIO模型、BIO模型、线程模型的实现也是比较复杂的,为了让大家先消化一下,下篇文章再继续分析吧。
觉得有收获就转发、在看一下哦,感谢!
