作者:图灵学院-周瑜

    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)
    image.png

    Tomcat启动后,在接收到某个请求后,就可以根据请求路径和url-pattern进行匹配,如果匹配成功,则会把这个请求交给对应的servlet进行处理。
    image.png
    上面只是请求处理的一个大概流程梳理,这其中还有很多细节需要我们挖掘,比如:

    1. Tomcat是如何解析字节流的?解析为什么对象?
    2. 我们定义Servlet时会去定义doGet、doPost等方法,那么Tomcat是如何判断该调哪个方法,并且是如何调用的?
    3. 其他问题

    我来看一下我们定义的Servlet:

    1. public class ServletDemo extends HttpServlet {
    2. @Override
    3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    4. resp.getWriter().println("hello servlet");
    5. }
    6. }

    上面代码中,我们定义了一个doGet(HttpServletRequest req, HttpServletResponse resp)方法,并且有两个参数,一个代表请求,一个代表响应,我们知道doGet方法就是在接收到get请求时会被调用,事实上,Tomcat接收到一个请求(字节流)后,就会将字节流解析为一个HttpServletRequest对象,然后根据请求找到对应的Servlet,然后调用Servlet中的service()方法,service()方法在我们定义的Servlet的父类HttpServlet中,而在service方法中,会去判断当前请求方法,如果是get请求,就会调用doGet,如果是post,就会调用doPost,service方法实现如下:

    1. protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    2. String method = req.getMethod();
    3. long lastModified;
    4. if (method.equals("GET")) {
    5. lastModified = this.getLastModified(req);
    6. if (lastModified == -1L) {
    7. this.doGet(req, resp);
    8. } else {
    9. long ifModifiedSince = req.getDateHeader("If-Modified-Since");
    10. if (ifModifiedSince < lastModified) {
    11. this.maybeSetLastModified(resp, lastModified);
    12. this.doGet(req, resp);
    13. } else {
    14. resp.setStatus(304);
    15. }
    16. }
    17. } else if (method.equals("HEAD")) {
    18. lastModified = this.getLastModified(req);
    19. this.maybeSetLastModified(resp, lastModified);
    20. this.doHead(req, resp);
    21. } else if (method.equals("POST")) {
    22. this.doPost(req, resp);
    23. } else if (method.equals("PUT")) {
    24. this.doPut(req, resp);
    25. } else if (method.equals("DELETE")) {
    26. this.doDelete(req, resp);
    27. } else if (method.equals("OPTIONS")) {
    28. this.doOptions(req, resp);
    29. } else if (method.equals("TRACE")) {
    30. this.doTrace(req, resp);
    31. } else {
    32. String errMsg = lStrings.getString("http.method_not_implemented");
    33. Object[] errArgs = new Object[]{method};
    34. errMsg = MessageFormat.format(errMsg, errArgs);
    35. resp.sendError(501, errMsg);
    36. }
    37. }

    那么,Tomcat接收到数据并解析为HttpServletRequest对象后,就直接把请求交给Servlet了吗?

    并不是,Tomcat还有其他考虑,比如:

    1. 假如,现在有一段逻辑,想让多个Servlet公用,就像切面一下,我们希望在请求被这些Servlet处理之前,能先执行公共逻辑。
    2. 假如,现在有一段逻辑,想让多个应用内的Servlet公用,就像切面一下,我们希望在请求被这些应用处理之前,能先执行公共逻辑。
    3. 我们知道,不同的域名可以对应同一个IP地址,对应同一个机器,那么我希望Tomcat能根据不同的域名做不同的处理
    4. 如果Tomcat支持多域名,那么我希望能使得这多个域名能共享某段逻辑

    可能上面这四种假设,大家会有点懵,没关系,请直接往下面看,在Tomcat中存在四大Servlet容器:

    1. Engine:直接理解为一个Tomcat即可,一个Tomcat一个Engine
    2. Host:一个Host表示一个虚拟服务器,可以给每个Host配置一个域名
    3. Context:一个Context就是一个应用,一个项目
    4. Wrapper:一个Wrapper表示一个Servlet的包装,Wrapper在后文详解

    并且这四个Servlet容器是具有层次关系的:一个Engine下可以有多个Host,一个Host下可以有多个Context,一个Context下可以有多个Wrapper,一个Wrapper下可以有多个Servlet实例对象。

    Tomcat接收到某个请求后,首先会判断该请求的域名,根据域名找到对应的Host对象,Host对象再根据请求信息找到请求所要访问的应用,也就是找到一个Context对象,Context对象拿到请求后,会根据请求信息找到对应的Servlet,那么Wrapper是什么?

    我们在定义一个Servlet时,如果额外实现了SingleThreadModel接口,那么就表示该Servlet是单线程模式:

    1. 定义Servlet时如果没有实现SingleThreadModel接口,那么在Tomcat中只会产生该Servlet的一个实例对象,如果多个请求同时访问该Servlet,那么这多个请求线程访问的是同一个Servlet对象,所以是并发不安全的
    2. 定义Servlet时如果实现了SingleThreadModel接口,那么在Tomcat中可能会产生多个该Servlet的实例对象,多个请求同时访问该Servlet,那么每个请求线程会有一个单独的Servlet对象,所以是并发安全的

    所以,我们发现,我们定义的某个Servlet,在Tomcat中可能会存在多个该类型的实例对象,所以Tomcat需要再抽象出来一层,这一层就是Wrapper,一个Wrapper对应一个Servlet类型,Wrapper中有一个集合,用来存储该Wrapper对应的Servlet类型的实例对象。

    所以一个Context表示一个应用,如果一个应用中定义了10个Servlet,那么Context下面就有10个Wrapper对象,而每个Wrapper中可能又会存在多个Servlet对象。
    image.png
    还有一点,在这个四个容器内部,有一个组件叫做Pipeline,也就管道,每个管道中可以设置多个Valve,也就阀门。

    管道与阀门的作用是,每个容器在接收到请求时会先把请求交给容器中的每个阀门处理,所有阀门都处理完了之后,在会将请求交给下层容器,通过这种机制,就解决了上面所提到的四种假设:

    1. Engine:可以处理Tomcat所接收到所有请求,不管这些请求是请求哪个应用或哪个Servlet的。
    2. Host:可以处理某个特定域名的所有请求
    3. Context:可以处理某个应用的所有请求
    4. Wrapper:可以处理某个Servlet的所有请求

    值得一说的是,Wrapper还会负责调用Servlet对象的service()方法。到此,Tomcat接收到字节流并解析为HttpServletRequest对象之后,HttpServletRequest对象是如何流转的给大家分析完了,总结一下就是:
    image.png

    接下来再来给大家分析一下,Tomcat是如何将字节流解析为HttpServletRequest对象的,这个问题其实不难,只要想到这些字节流是谁发过来的?比如浏览器,而浏览器在发送数据时,会先将数据按HTTP协议的格式进行包装,在把HTTP数据包通过TCP协议发送出去,Tomcat就是从TCP协议中接收到数据的,只是从TCP协议中接收的数据是字节流,接下来要做的就是同样按照HTTP协议进行解析,比如解析出请求行、请求头等,从而就可以生成HttpServletRequest对象。不过,整个解析过程还是比较复杂的,包括Tomcat底层的NIO模型、BIO模型、线程模型的实现也是比较复杂的,为了让大家先消化一下,下篇文章再继续分析吧。

    觉得有收获就转发、在看一下哦,感谢!