美文网首页中间件
tomcat源码浅析-从一次http请求谈起

tomcat源码浅析-从一次http请求谈起

作者: 默写流年 | 来源:发表于2020-08-05 08:53 被阅读0次

    在之前的专题spring源码解读系列中,我们分析了spring的源码,详细分析了spring的ioc和aop的实现原理。而我们日常使用的无论是spring+spring mvc,还是spring boot。都离不开spring mvc,而spring mvc是基于Servlet实现。而Servlet又是必须运行在应用服务器/web容器中。web容器用的最多的就是tomcat。那么tomcat是如何运行Servlet容器的呢?一次http请求在tomcat中究竟发生了什么过程。这个将是这篇文章我们探讨的内容。本文包括以下部分:

    1. 前言
      1.1 servlet与web容器
      1.2 spring mvc
      1.3 几个问题
    2. tomcat架构概览
      2.1 tomcat的配置文件
      2.2 tomcat整体架构
    3. tomcat 启动流程
      3.1 静态资源初始化
      3.2 初始化类加载器
      3.3 load
          3.3.1 解析配置文件并实例化组件
          3.3.2 组件初始化
      3.4 start
          3.4.1 启动server
          3.4.2 启动service
          3.4.3 启动engine(container)
              3.4.3.1 部署war包
              3.4.3.2 创建context
              3.4.3.3 为context赋值
          3.4.4 启动Connector
              3.4.4.1 启动Http11NioProtocol
              3.4.4.2 启动NioEndPoint
              3.4.4.3 创建Acceptor&Poller
    4. 请求处理
      4.1 工作线程处理
      4.2 请求流转
      4.3 CoyoteAdapter处理
          4.3.1 构造HttpServletRequest/HttpServletResponse
          4.3.2 解析tcp数据包,填充request/response
          4.3.3 查找context
          4.3.3 查找context
          4.3.4 调用Pipeline进行链式处理
    5. 总结

    1. 前言

    正如我们开头所说,日常中使用的最多是spring boot,而spring boot则基于spring+spring mvc 实现。下面我们简单分析下servlet及servlet 容器,以及演示下spring mvc servlet 的配置。

    1.1 servlet与servlet 容器

    • servlet即运行在应用服务器或者web服务器上的web组件,充当客户端与服务器端的中间层,用于动态的生成交互内容。简单点来说就是实现Http协议的程序,接收HttpServletRequest对象,进行业务处理后返回HttpServletResponse。相信这个定义每个做过web开发的童鞋都很熟悉。
    • servlet容器,顾名思义承载servlet的容器,同时承担这些servlet的管理(创建-初始化-请求-销毁)。
    • Http协议是基于建立在TCP协议之上的应用层协议。那么监听特定端口与客户端建立连接——→读取TCP数据包——→将转换为HttpServletRequest——→并将请求转发给特定Servlet处理——→处理完毕后将HttpResponse对象写入数据流。就是应用服务器或者web服务器做的工作了。
      流程图如下:
      servlet容器处理流程.png

    其中蓝色部分为servlet 容器所做的工作。

    1.2 spring mvc

    我们都知道,使用spring mvc 要在web.xml,配置org.springframework.web.servlet.DispatcherServlet做请求分发,其也是spring mvc 的核心入口。下面演示下spring mvc的简单使用,便于后面的源码分析。

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">
    
        <display-name>Archetype Created Web Application</display-name>
        <!--welcome pages-->
        <welcome-file-list>
            <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    
        <!--配置springmvc DispatcherServlet-->
        <servlet>
            <servlet-name>springMVC</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <!--配置dispatcher.xml作为mvc的配置文件-->
                <param-name>contextConfigLocation</param-name>
                <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
            <async-supported>true</async-supported>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>springMVC</servlet-name>
            <url-pattern>/*</url-pattern>
        </servlet-mapping>
        <!--把applicationContext.xml加入到配置文件中-->
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/applicationContext.xml</param-value>
        </context-param>
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
    </web-app>
    
    @Controller
    public class MyController {
        @GetMapping(value = "hello")
        @ResponseBody
        public String sayHi(){
            return "hello man!";
        }
    }
    

    访问的时候,会返回"hello,man!"。
    上面是最简单常见的mvc应用。我们看到mvc作用的根本在于DispatcherServlet。

    1.3 几个问题

    通过上面的演示,我们不禁要问
    1. tomcat 是如何建立8080端口监听的?
    2. tomcat是何时建立8080端口监听的?
    3. tomcat是如何读取TCP数据包内容转换为HttpServletRequest的?
    4. tomcat是如何根据请求路径找到相应的Servlet处理的?
    让我们带着问题开启下面的探索。


    2. tomcat架构概览

    在读tomcat源码前,我们有必要先了解下tomcat的整体架构,通过对tomcat整体架构的简单了解,更有利于我们熟悉tomcat的工作原理。

    2.1 tomcat的配置文件

    先看下我们最熟悉的部分,tomcat的配置文件(下面的配置文件为默认的初始文件,且去掉注释部分,版本为8.5.57)。

    <?xml version="1.0" encoding="UTF-8"?>
    <!--server-->
    <Server port="8005" shutdown="SHUTDOWN">
      <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
      <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
      <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
      <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
      <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
    
     
      <GlobalNamingResources>
       
        <Resource name="UserDatabase" auth="Container"
                  type="org.apache.catalina.UserDatabase"
                  description="User database that can be updated and saved"
                  factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
                  pathname="conf/tomcat-users.xml" />
      </GlobalNamingResources>
    <!--service-->
      <Service name="Catalina">
    
        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443" />
     <!--=engine 即 container-->
        <Engine name="Catalina" defaultHost="localhost">
          、
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                   resourceName="UserDatabase"/>
          </Realm>
    
          <Host name="localhost"  appBase="webapps"
                unpackWARs="true" autoDeploy="true">
            <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                   prefix="localhost_access_log" suffix=".txt"
                   pattern="%h %l %u %t &quot;%r&quot; %s %b" />
          </Host>
        </Engine>
      </Service>
    </Server>
    
    

    配置文件能很好的体现tomcat的组件及组件的关系。

    2.2 tomcat整体架构

    从上面的配置文件中可以看到tomcat大致有以下组件,server、service、connector、engine、host。架构图如下:


    tomcat 整体架构图.png
    • server:tomcat的顶层节点,可以理解为一个tomcat实例。
    • service:一个tomcat可以有多个service,主要包括connector和Container。
    • connector:一个connector代表一种应用协议,主要负责socket处理,监听请求,将请求转换为request和response。之后交container处理。因为可以提供多种协议的服务,所以可以有多个connector。但是只有一个service下只有一个container。
    • container: 用于封装和管理servlet,以及具体请求的处理。
    • engine:全局Servlet引擎。
    • host:虚拟主机。可以放置多个context。
    • context: web应用。我们的每个war包对应一个context。
    • wapper: Servlet包装。

    3. tomcat 启动流程

    tomcat的启动过程,按照代码的执行流程,可以分为静态资源初始化,初始化类加载器,load,start。下面我们来分别说明这个过程。

    3.1 静态资源初始化

    我们都知道jar启动必须有个main方法,tomcat的启动入口为org.apache.catalina.startup.Bootstrap。而静态资源初始化正是通过Bootstrap中的static代码块实现的,主要目的是初始化catalinaBaseFile和catalinaHomeFile。catalinaHomeFile:tomcat的安装目录。catalinaBaseFile:tomcat实例的工作目录。可以通过共享tomcat安装文件的方式启动多个tomcat实例,而无需拷贝多份安装文件。如下

    set CATALINA_BASE=D:\soft\apache-tomcat-8.5\tomcat-8081
    set CATALINA_HOME=D:\soft\apache-tomcat-8.5\apache-tomcat-8.5.57
    %CATALINA_HOME%\bin\startup.bat
    
            // Will always be non-null
            String userDir = System.getProperty("user.dir");
    
            // Home first
            String home = System.getProperty(Constants.CATALINA_HOME_PROP);
            File homeFile = null;
    
            if (home != null) {
                File f = new File(home);
                try {
                    homeFile = f.getCanonicalFile();
                } catch (IOException ioe) {
                    homeFile = f.getAbsoluteFile();
                }
            }
    
            if (homeFile == null) {
                // First fall-back. See if current directory is a bin directory
                // in a normal Tomcat install
                File bootstrapJar = new File(userDir, "bootstrap.jar");
    
                if (bootstrapJar.exists()) {
                    File f = new File(userDir, "..");
                    try {
                        homeFile = f.getCanonicalFile();
                    } catch (IOException ioe) {
                        homeFile = f.getAbsoluteFile();
                    }
                }
            }
    
            if (homeFile == null) {
                // Second fall-back. Use current directory
                File f = new File(userDir);
                try {
                    homeFile = f.getCanonicalFile();
                } catch (IOException ioe) {
                    homeFile = f.getAbsoluteFile();
                }
            }
    
            catalinaHomeFile = homeFile;
            System.setProperty(
                    Constants.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
    
            // Then base
            String base = System.getProperty(Constants.CATALINA_BASE_PROP);
            if (base == null) {
                catalinaBaseFile = catalinaHomeFile;
            } else {
                File baseFile = new File(base);
                try {
                    baseFile = baseFile.getCanonicalFile();
                } catch (IOException ioe) {
                    baseFile = baseFile.getAbsoluteFile();
                }
                catalinaBaseFile = baseFile;
            }
            System.setProperty(
                    Constants.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
        }
    

    3.2 初始化类加载器

    作为一个web容器,我们会有以下需求

    • 不同工程间相同包的共享
    • 不同工程间不同版本包的隔离
    • tomcat内部jar和应用jar隔离
      那么这些功能是怎么实现的呢?答案是classloader。tomcat在启动的时候,实现了classloader的实例化。
        private void initClassLoaders() {
            try {
                commonLoader = createClassLoader("common", null);
                if (commonLoader == null) {
                    // no config file, default to this loader - we might be in a 'single' env.
                    commonLoader = this.getClass().getClassLoader();
                }
                catalinaLoader = createClassLoader("server", commonLoader);
                sharedLoader = createClassLoader("shared", commonLoader);
            } catch (Throwable t) {
                handleThrowable(t);
                log.error("Class loader creation threw exception", t);
                System.exit(1);
            }
        }
    

    关于tomcat classloader如何做到jar隔离与共享,我们后续再说。

    3.3 load

    load主要做了两件事,1:解析配置文件并实例化组件,2:组件的初始化。在这个过程中,会创建socket,开启8080端口监听。大致流程图如下:

    load 流程.png
    load的过程即为,通过反射调用org.apache.catalina.startup.Catalina.load()方法的过程。

    3.3.1 解析配置文件并实例化组件

    • 创建文件解析规则
      文件解析的时候用的是SAX解析,SAX是基于事件回调的形式解析xml。调用入口为org.apache.catalina.startup.Catalina.load()
            //创建配置文件解析规则
            Digester digester = createStartDigester();
    

    篇幅所限,仅仅列出了部分解析规则的代码

          //server实例化规则
           digester.addObjectCreate("Server",
                                     "org.apache.catalina.core.StandardServer",
                                     "className");
            digester.addSetProperties("Server");
            digester.addSetNext("Server",
                                "setServer",
                                "org.apache.catalina.Server");
            //service实例化规则
            digester.addObjectCreate("Server/Service",
                                     "org.apache.catalina.core.StandardService",
                                     "className");
            digester.addSetProperties("Server/Service");
            digester.addSetNext("Server/Service",
                                "addService",
                                "org.apache.catalina.Service");
    
    • 获取配置文件输入流
     InputSource inputSource = null;
            InputStream inputStream = null;
            File file = null;
            try {
                try {
                    file = configFile();
                    inputStream = new FileInputStream(file);
                    inputSource = new InputSource(file.toURI().toURL().toString());
    

    读取的文件目录为:%CATALINA_HOME%\conf\server.xml

    protected String configFile = "conf/server.xml";
        protected File configFile() {
    
            File file = new File(configFile);
            if (!file.isAbsolute()) {
                file = new File(Bootstrap.getCatalinaBase(), configFile);
            }
            return file;
    
        }
    
    • 解析配置文件实例化组件
      配置文件解析的过程中,会根据上面定义好的解析规则,实例化当前Catalina对象中的属性,即各种组件,如Server、service、engine、connector等。
                    inputSource.setByteStream(inputStream);
                    digester.push(this);
                    //解析并实例化
                    digester.parse(inputSource);
    

    3.3.2 组件初始化

    解析出来的组件,还要经历初始化的过程。初始化时候会依次初始化Server、Service、Engine、Connector、ProtocolHandler、AbstractEndpoint。最终创建socket,开启8080端口监听。值得注意的是Server、Engine、Connector这些接口的实现都继承自LifecycleBase,LifecycleBase中的init方法为模板方法,在init方法中定义了骨架实现,需要子类单独实现initInternal

      //模板方法,骨架实现
     @Override
        public final synchronized void init() throws LifecycleException {
            if (!state.equals(LifecycleState.NEW)) {
                invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
            }
    
            try {
                setStateInternal(LifecycleState.INITIALIZING, null, false);
                initInternal();
                setStateInternal(LifecycleState.INITIALIZED, null, false);
            } catch (Throwable t) {
                handleSubClassException(t, "lifecycleBase.initFail", toString());
            }
        }
      // 留给子类实现的方法
     protected abstract void initInternal() throws LifecycleException;
    

    有了这个前提后,我们来梳理下初始化究竟经历了哪些过程。

    • 初始化server。org.apache.catalina.startup.Catalina.load
     //getServer返回的对象在解析xml后已经实例化
     getServer().init();
    
    • 初始化service。org.apache.catalina.core.StandardServer.initInternal()
           // Initialize our defined Services
            for (Service service : services) {
                //初始化service
                service.init();
            }
    
    • 初始化engine。org.apache.catalina.core.StandardService.initInternal()
          if (engine != null) {
                //初始化engine
                engine.init();
            }
    
    • 初始化connector。org.apache.catalina.core.StandardService.initInternal()
        synchronized (connectorsLock) {
                for (Connector connector : connectors) {
                    try {
                        //初始化connector
                        connector.init();
                    } catch (Exception e) {
                        String message = sm.getString(
                                "standardService.connector.initFailed", connector);
                        log.error(message, e);
    
                        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
                            throw new LifecycleException(message);
                    }
                }
            }
    
    • 初始化ProtocolHandler及CoyoteAdapter。其中CoyoteAdapter为封装Request、Response,并将请求发送给相应servlet的关键。代码路径为org.apache.catalina.connector.initInternal()
      设置CoyoteAdapter
            // 设置CoyoteAdapter
            adapter = new CoyoteAdapter(this);
            protocolHandler.setAdapter(adapter);
    

    初始化protocolHandler

            //初始化protocolHandler
            protocolHandler.init();
    

    其中protocolHandler为实例化Connector的时候创建的,默认为org.apache.coyote.http11.Http11NioProtocol

     public Connector(String protocol) {
            setProtocol(protocol);
            // Instantiate protocol handler
            ProtocolHandler p = null;
            try {
                Class<?> clazz = Class.forName(protocolHandlerClassName);
                p = (ProtocolHandler) clazz.getConstructor().newInstance();
            } catch (Exception e) {
                log.error(sm.getString(
                        "coyoteConnector.protocolHandlerInstantiationFailed"), e);
            } finally {
                this.protocolHandler = p;
            }
    
            if (Globals.STRICT_SERVLET_COMPLIANCE) {
                uriCharset = StandardCharsets.ISO_8859_1;
            } else {
                uriCharset = StandardCharsets.UTF_8;
            }
    
            // Default for Connector depends on this (deprecated) system property
            if (Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "false"))) {
                encodedSolidusHandling = EncodedSolidusHandling.DECODE;
            }
        }
    
    • AbstractEndpoint 初始化。org.apache.coyote.AbstractProtocol.init()。
        //endpoint同样是在构造函数中初始化好了
        endpoint.init();
    
     public Http11NioProtocol() {
            super(new NioEndpoint());
        }
    
    • AbstractEndpoint初始化。在这里就会创建socket,监听8080端口。代码入口为:org.apache.tomcat.util.net.AbstractEndpoint.init()
    if (bindOnInit) {
                bind();
                bindState = BindState.BOUND_ON_INIT;
            }
    

    创建socket。值得注意的是,这里的serverSock是作为实例变量保存在NioEndpoint类中的

                serverSock = ServerSocketChannel.open();
                socketProperties.setProperties(serverSock.socket());
                //socket地址
                InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
                //绑定端口
                serverSock.socket().bind(addr,getAcceptCount());
    

    到此为止,我们终于看到了熟悉的8080端口监听。好像也仅仅是创建socket,那么tomcat又是如何处理socket读写的呢?别急,容老夫慢慢道来....

    3.4 start

    前面的load过程仅仅是为start过程做准备,start的过程考虑的事情比较多,也比较复杂。但是大致可以分为以下几个步骤,1:启动server 2:启动service 3:启动container 4:启动connector。其中核心步骤为步骤3,以及步骤4。包括部署war,启动应用context,实例化并初始DispatcherServlet,开启ContextLoaderListener。启动Http11NioProtocol,启动NioEndPoint,创建Acceptor,创建Poller

    start大致流程.png
    下面来简单介绍下上诉过程。

    3.4.1 启动server

    server的start入口为org.apache.catalina.startup.Catalina.start()

           // Start the new server
            try {
                getServer().start();
            } catch (LifecycleException e) {
                log.fatal(sm.getString("catalina.serverStartFail"), e);
                try {
                    getServer().destroy();
                } catch (LifecycleException e1) {
                    log.debug("destroy() failed for failed Server ", e1);
                }
                return;
            }
    

    server的默认实现为StandardServer。与init的流程一样,同样存在一个从LifecycleBase继承而来的模板方法start,同样要重写startInternal()方法。

    3.4.2 启动service

    启动service的入口在StandServer重写的startInternal方法中,如下:

    protected void startInternal() throws LifecycleException {
    
            fireLifecycleEvent(CONFIGURE_START_EVENT, null);
            setState(LifecycleState.STARTING);
    
            globalNamingResources.start();
    
            // Start our defined Services
            synchronized (servicesLock) {
                for (Service service : services) {
                    service.start();
                }
            }
        }
    

    我们这里主要关注的为StandService.start()。

    protected void startInternal() throws LifecycleException {
    
            if(log.isInfoEnabled())
                log.info(sm.getString("standardService.start.name", this.name));
            setState(LifecycleState.STARTING);
    
            // Start our defined Container first
            //部署war
            if (engine != null) {
                synchronized (engine) {
                    engine.start();
                }
            }
    
            synchronized (executors) {
                for (Executor executor: executors) {
                    executor.start();
                }
            }
    
            mapperListener.start();
    
            // Start our defined Connectors second
            synchronized (connectorsLock) {
                for (Connector connector: connectors) {
                    try {
                        // If it has already failed, don't try and start it
                        if (connector.getState() != LifecycleState.FAILED) {
                            connector.start();
                        }
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "standardService.connector.startFailed",
                                connector), e);
                    }
                }
            }
        }
    

    从代码可以很清晰的看到Service的启动分为以下步骤:启动engine(container),启动mapperListener,启动Connector。这里也是启动的关键所在。

    3.4.3 启动engine(container)

    启动engine的过程相对来说比较复杂。主要涉及到如下步骤:部署war包,创建context、为context赋值,实例化servlet,开启ServletContextListener。正是在这个步骤里,我们的应用war包被解压,context被实例化,spring mvc得以启动

    3.4.3.1 部署war包

    部署war包是通过提交一个单独的线程的方式进行的。线程为org.apache.catalina.core.ContainerBase.StartChild。最终会调用HostConfig.lifecycleEvent方法来触发部署war包。

          //这里的child是load 解析xml的时候初始化的StandardHost
           for (Container child : children) {
                results.add(startStopExecutor.submit(new StartChild(child)));
            }
    

    由于中间过程太多,我们只贴出关键代码

       protected void deployApps() {
    
            File appBase = host.getAppBaseFile();
            File configBase = host.getConfigBaseFile();
            String[] filteredAppPaths = filterAppPaths(appBase.list());
            // Deploy XML descriptors from configBase
            deployDescriptors(configBase, configBase.list());
            // Deploy WARs
            deployWARs(appBase, filteredAppPaths);
            // Deploy expanded folders
            deployDirectories(appBase, filteredAppPaths);
    
        }
    

    3.4.3.2 创建context

    war包被解压后,当然要创建context了。创建context又分为两部:实例化ContextName作为创建context的参数,创建context实例

    • 实例化ContextName
      这里的ContextName会作为context的path和name属性赋值给context,所以这里有必要提一下ContextName的创建过程。
    ContextName cn = new ContextName(file, true);
    
    public ContextName(String name, boolean stripFileExtension) {
    
            String tmp1 = name;
    
            // Convert Context names and display names to base names
    
            // Strip off any leading "/"
            if (tmp1.startsWith("/")) {
                tmp1 = tmp1.substring(1);
            }
    
            // Replace any remaining /
            tmp1 = tmp1.replaceAll("/", FWD_SLASH_REPLACEMENT);
    
            // Insert the ROOT name if required
            if (tmp1.startsWith(VERSION_MARKER) || "".equals(tmp1)) {
                tmp1 = ROOT_NAME + tmp1;
            }
    
            // Remove any file extensions
            if (stripFileExtension &&
                    (tmp1.toLowerCase(Locale.ENGLISH).endsWith(".war") ||
                            tmp1.toLowerCase(Locale.ENGLISH).endsWith(".xml"))) {
                tmp1 = tmp1.substring(0, tmp1.length() -4);
            }
    
            baseName = tmp1;
    
            String tmp2;
            // Extract version number
            int versionIndex = baseName.indexOf(VERSION_MARKER);
            if (versionIndex > -1) {
                version = baseName.substring(versionIndex + 2);
                tmp2 = baseName.substring(0, versionIndex);
            } else {
                version = "";
                tmp2 = baseName;
            }
    
            if (ROOT_NAME.equals(tmp2)) {
                path = "";
            } else {
                path = "/" + tmp2.replaceAll(FWD_SLASH_REPLACEMENT, "/");
            }
    
            if (versionIndex > -1) {
                this.name = path + VERSION_MARKER + version;
            } else {
                this.name = path;
            }
        }
    

    可以看到,这里ContextName的name默认为war包名称去掉.war后缀,path属性与name属性相同

    • 实例化Context
      实例化context的过程为通过反射调用org.apache.catalina.core.StandardContext的无参构造方法的过程
      context = (Context) Class.forName(contextClass).getConstructor().newInstance();
    

    这里的contextClass默认为org.apache.catalina.core.StandardContex

    3.4.3.3 为context赋值

    为context赋值的过程其实包括了实例化servlet的过程。

                context.setName(cn.getName());
                context.setPath(cn.getPath());
                context.setWebappVersion(cn.getVersion());
                context.setDocBase(cn.getBaseName() + ".war");
                host.addChild(context);
    

    这里会将context作为host的子元素添加进去,后续触发context的启动,而context启动时会实例化servlet。

     private void addChildInternal(Container child) {
    
            if( log.isDebugEnabled() )
                log.debug("Add child " + child + " " + this);
            synchronized(children) {
                if (children.get(child.getName()) != null)
                    throw new IllegalArgumentException("addChild:  Child name '" +
                                                       child.getName() +
                                                       "' is not unique");
                child.setParent(this);  // May throw IAE
                children.put(child.getName(), child);
            }
    
            // Start child
            // Don't do this inside sync block - start can be a slow process and
            // locking the children object can cause problems elsewhere
            try {
                if ((getState().isAvailable() ||
                        LifecycleState.STARTING_PREP.equals(getState())) &&
                        startChildren) {
                  //触发StandContext启动
                    child.start();
                }
            } catch (LifecycleException e) {
                log.error("ContainerBase.addChild: start: ", e);
                throw new IllegalStateException("ContainerBase.addChild: start: " + e);
            } finally {
                fireContainerEvent(ADD_CHILD_EVENT, child);
            }
        }
    
                    // Notify our interested LifecycleListeners
                    fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
    

    这里会触发ContextConfig.lifecycleEvent

      //启动
     configureStart();
    //读取xml,并设置属性
    webConfig();‘
    //将web.xml的属性设置到context中
    configureContext(webXml);
    

    主要设置属性的入口在org.apache.catalina.startup.ContextConfig.configureContext()方法中。这里有很多属性的设置过程,我们重点关注下Servlet的设置过程。

     for (ServletDef servlet : webxml.getServlets().values()) {
                Wrapper wrapper = context.createWrapper();
                // Description is ignored
                // Display name is ignored
                // Icons are ignored
    
                // jsp-file gets passed to the JSP Servlet as an init-param
    
                if (servlet.getLoadOnStartup() != null) {
                    wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
                }
                if (servlet.getEnabled() != null) {
                    wrapper.setEnabled(servlet.getEnabled().booleanValue());
                }
                wrapper.setName(servlet.getServletName());
                Map<String,String> params = servlet.getParameterMap();
                for (Entry<String, String> entry : params.entrySet()) {
                    wrapper.addInitParameter(entry.getKey(), entry.getValue());
                }
                wrapper.setRunAs(servlet.getRunAs());
                Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
                for (SecurityRoleRef roleRef : roleRefs) {
                    wrapper.addSecurityReference(
                            roleRef.getName(), roleRef.getLink());
                }
                wrapper.setServletClass(servlet.getServletClass());
                MultipartDef multipartdef = servlet.getMultipartDef();
                if (multipartdef != null) {
                    long maxFileSize = -1;
                    long maxRequestSize = -1;
                    int fileSizeThreshold = 0;
    
                    if(null != multipartdef.getMaxFileSize()) {
                        maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
                    }
                    if(null != multipartdef.getMaxRequestSize()) {
                        maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
                    }
                    if(null != multipartdef.getFileSizeThreshold()) {
                        fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
                    }
    
                    wrapper.setMultipartConfigElement(new MultipartConfigElement(
                            multipartdef.getLocation(),
                            maxFileSize,
                            maxRequestSize,
                            fileSizeThreshold));
                }
                if (servlet.getAsyncSupported() != null) {
                    wrapper.setAsyncSupported(
                            servlet.getAsyncSupported().booleanValue());
                }
                wrapper.setOverridable(servlet.isOverridable());
                context.addChild(wrapper);
            }
    

    Wrapper 的默认实现为StandardWrapper

      wrapper = new StandardWrapper();
    

    可以看到这里把servlet包装为StandardWrapper,并且将servlet的各种属性赋值给StandardWrapper。

    3.4.4 启动Connector

    启动Connector的过程相对来说分为如下步骤:启动Http11NioProtocol,启动NioEndPoint,创建Acceptor&Poller。

    3.4.4.1 启动Http11NioProtocol

    protocolHandler.start();
    

    protocolHandler的默认实现为org.apache.coyote.http11.Http11NioProtocol,其同样是在解析load的时候生成的。

     public Connector(String protocol) {
            setProtocol(protocol);
            // Instantiate protocol handler
            ProtocolHandler p = null;
            try {
                Class<?> clazz = Class.forName(protocolHandlerClassName);
                p = (ProtocolHandler) clazz.getConstructor().newInstance();
            } catch (Exception e) {
                log.error(sm.getString(
                        "coyoteConnector.protocolHandlerInstantiationFailed"), e);
            } finally {
                this.protocolHandler = p;
            }
    

    3.4.4.2 启动NioEndPoint

     public void start() throws Exception {
            if (getLog().isInfoEnabled()) {
                getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
            }
    
            endpoint.start();
    
            // Start timeout thread
            asyncTimeout = new AsyncTimeout();
            Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
            int priority = endpoint.getThreadPriority();
            if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
                priority = Thread.NORM_PRIORITY;
            }
            timeoutThread.setPriority(priority);
            timeoutThread.setDaemon(true);
            timeoutThread.start();
        }
    

    启动NioEndpoint的过程主要为调用NioEndpoint.startInternal

    public void startInternal() throws Exception {
    
            if (!running) {
                running = true;
                paused = false;
    
                processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getProcessorCache());
                eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                                socketProperties.getEventCache());
                nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getBufferPool());
    
                // Create worker collection
                if ( getExecutor() == null ) {
                    createExecutor();
                }
    
                initializeConnectionLatch();
    
                // Start poller threads
                pollers = new Poller[getPollerThreadCount()];
                for (int i=0; i<pollers.length; i++) {
                    pollers[i] = new Poller();
                    Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                    pollerThread.setPriority(threadPriority);
                    pollerThread.setDaemon(true);
                    pollerThread.start();
                }
    
                startAcceptorThreads();
            }
        }
    

    可以看到NioEndpoint的创建过程主要做了两件事,创建Poller和创建Acceptor

    3.4.4.3 创建Acceptor&Poller

    Acceptor:负责处理连接的线程。Poller:负责轮询是否可读写,如果可读写,则将socket转给工作线程读写。这里实现了IO的多路复用

    • 创建Acceptor
       protected final void startAcceptorThreads() {
            int count = getAcceptorThreadCount();
            acceptors = new Acceptor[count];
    
            for (int i = 0; i < count; i++) {
                acceptors[i] = createAcceptor();
                String threadName = getName() + "-Acceptor-" + i;
                acceptors[i].setThreadName(threadName);
                Thread t = new Thread(acceptors[i], threadName);
                t.setPriority(getAcceptorThreadPriority());
                t.setDaemon(getDaemon());
                t.start();
            }
        }
    

    还记得我们在load里开启的8080端口监听吗,load里我们只开启了8080端口监听,然后就什么都没做。这里的Acceptor线程就是为了接收从8080端口过来的请求,并将其绑定到Poller中

            //接收tcp连接
            socket = serverSock.accept();
    
      //绑定到Poller
     if (!setSocketOptions(socket)) {
                                closeSocket(socket);
                            }
    
            socket.configureBlocking(false);
                Socket sock = socket.socket();
                socketProperties.setProperties(sock);
    
                NioChannel channel = nioChannels.pop();
                if (channel == null) {
                    SocketBufferHandler bufhandler = new SocketBufferHandler(
                            socketProperties.getAppReadBufSize(),
                            socketProperties.getAppWriteBufSize(),
                            socketProperties.getDirectBuffer());
                    if (isSSLEnabled()) {
                        channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                    } else {
                        channel = new NioChannel(socket, bufhandler);
                    }
                } else {
                    channel.setIOChannel(socket);
                    channel.reset();
                }
                getPoller0().register(channel);
    
    • 创建Poller
      Poller会轮询PollerEvent队列,直到读写事件,则将事件转给工作线程。
    //是否有事件
     hasEvents = events();
    

    当事件可用的时候,将事件转给工作线程

    while (iterator != null && iterator.hasNext()) {
                       SelectionKey sk = iterator.next();
                       NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
                       // Attachment may be null if another thread has called
                       // cancelledKey()
                       if (attachment == null) {
                           iterator.remove();
                       } else {
                           iterator.remove();
                           processKey(sk, attachment);
                       }
                   }//while
    
                              //可读
                             if (sk.isReadable()) {
                                    if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
                                        closeSocket = true;
                                    }
                                }
                              //可写
                                if (!closeSocket && sk.isWritable()) {
                                    if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
                                        closeSocket = true;
                                    }
                                }
    

    关于工作线程如何处理处理读写事件,我们放在后面分析。到此为止,我们说完了tomcat的大致启动,流程。那么tomcat启动完成后,当有请求进来的时候,是如何处理的呢?

    4. 请求处理

    上面我们花了很大的篇幅来说tomcat的启动流程,启动的目的当然是为了对外提供服务了。那么思考下面两个问题:
    1. tomcat如何读取tcp数据包解析为Reuqest
    2. tomcat如何根据请求路径找到context的
    先给请求处理的流程图

    tomcat启动后请求处理流程.png
    从上面的流程图中,我们可以知道业务处理大致分为如下过程
    1. 连接请求被Acceptor监听,Acceptor将连接后的Socket绑定到Poller。
    2. Poller轮询是否有可用读写事件,如果有可用读写事件,则将事件提交给SocketProcessor处理。
    3. SocketProcessor将得到的读写状态的socket传递给ConnectionHandler处理。
    4. ConnectionHandler进行部分处理后将请求传递至Http11Processor。
    5. Http11Processor进行部分数据的转换后将请求传递给CoyoteAdapter
    6. CoyoteAdapter会构建HttpServletRequest/HttpSevletResponse的子类,org.apache.catalina.connector.Request/org.apache.catalina.connector.Response。并进行数据包的解析。
    7. 解析完成后将请求传递给container中的Pipeline进行处理,这里会以责任链的形式进行请求传递。请求会按照container中的层级关系Engine->Host->Context->Wapper的形式传递。
    8. 请求最终到达StandardWrapperValve。StandardWrapperValve是对servlet的包装,这里会触发我们自己的web.xml配置的拦截器链调用,然后再调用对应servlet.service方法进行处理。
      下面我们简单看下几个关键的过程

    4.1 工作线程处理

    我们之前在启动的流程中分析Connector启动的一项重要工作就是创建Accptor和Poller。这里的Poller为NioEndPoint的内部类

      @Override
            public void run() {
                // Loop until destroy() is called
                while (true) {
    
                    boolean hasEvents = false;
    
                    try {
                        if (!close) {
                            hasEvents = events();
                            if (wakeupCounter.getAndSet(-1) > 0) {
                                //if we are here, means we have other stuff to do
                                //do a non blocking select
                                keyCount = selector.selectNow();
                            } else {
                                keyCount = selector.select(selectorTimeout);
                            }
                            wakeupCounter.set(0);
                        }
                        if (close) {
                            events();
                            timeout(0, false);
                            try {
                                selector.close();
                            } catch (IOException ioe) {
                                log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
                            }
                            break;
                        }
                    } catch (Throwable x) {
                        ExceptionUtils.handleThrowable(x);
                        log.error("",x);
                        continue;
                    }
                    //either we timed out or we woke up, process events first
                    if ( keyCount == 0 ) hasEvents = (hasEvents | events());
    
                    Iterator<SelectionKey> iterator =
                        keyCount > 0 ? selector.selectedKeys().iterator() : null;
                    // Walk through the collection of ready keys and dispatch
                    // any active event.
                    while (iterator != null && iterator.hasNext()) {
                        SelectionKey sk = iterator.next();
                        NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
                        // Attachment may be null if another thread has called
                        // cancelledKey()
                        if (attachment == null) {
                            iterator.remove();
                        } else {
                            iterator.remove();
                            processKey(sk, attachment);
                        }
                    }//while
    
                    //process timeouts
                    timeout(keyCount,hasEvents);
                }//while
    
                getStopLatch().countDown();
            }
    

    正如注释所写,Poller会一直轮询,直到容器销毁。如果有可用事件,则调用processKey处理

                                    if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
                                        closeSocket = true;
                                    }
                                }
                                if (!closeSocket && sk.isWritable()) {
                                    if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
                                        closeSocket = true;
                                    }
                                }
    
      SocketProcessorBase<S> sc = processorCache.pop();
                if (sc == null) {
                    sc = createSocketProcessor(socketWrapper, event);
                } else {
                    sc.reset(socketWrapper, event);
                }
                Executor executor = getExecutor();
                if (dispatch && executor != null) {
                    executor.execute(sc);
                } else {
                    sc.run();
                }
    

    正如我们所看到的,最终的请求是提交到了SocketProcessorBase这个抽象类中了,此时的实现为SocketProcessor,其同样是NioEndPoint的内部类。在这里我们可以看到NioEndPonit是一个专门处理底层网络读写的组件,内部包含Acceptor、Poller、SocketProcessor、ConnectionHandler。Acceptor用于接收请求,Poller用于轮询请求,SocketProcessor负责将请求中转给ConnectionHandler处理。

    4.2 请求流转

    请求提交给SocketProcessor,会经过SocketProcessor→ConnectionHandler→Http11Processor→CoyoteAdapter的过程流转。具体实现如下

    • SocketProcessor线程处理
       if (event == null) {
                            state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
                        } else {
                            state = getHandler().process(socketWrapper, event);
                        }
    
    • ConnectionHandler处理
        // Associate the processor with the connection
                    connections.put(socket, processor);
    
                    SocketState state = SocketState.CLOSED;
                    do {
                        state = processor.process(wrapper, status);
    
                        if (state == SocketState.UPGRADING) {
    
    • Http11Processor 处理
    } else if (status == SocketEvent.OPEN_WRITE) {
                    // Extra write event likely after async, ignore
                    state = SocketState.LONG;
                } else if (status == SocketEvent.OPEN_READ) {
                    state = service(socketWrapper);
                }
    
       if (getErrorState().isIoAllowed()) {
                    try {
                        rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
                        getAdapter().service(request, response);
                        // Handle when the response was committed before a serious
                        // error occurred.  Throwing a ServletException should both
                        // set the status to 500 and set the errorException.
                        // If we fail here, then the response is likely already
                        // committed, so we can't try and set headers.
                        if(keepAlive && !getErrorState().isError() && !isAsync() &&
                                statusDropsConnection(response.getStatus())) {
                            setErrorState(ErrorState.CLOSE_CLEAN, null);
                        }
                    }
    

    4.3 CoyoteAdapter处理

    上面的步骤中,我们最终会调用Adapter.service方法进行请求的处理,这个Adapter同样是在load的过程中,具体为Connector初始化的时候设置的

            @Override
        protected void initInternal() throws LifecycleException {
    
            super.initInternal();
    
            // Initialize adapter
            adapter = new CoyoteAdapter(this);
            protocolHandler.setAdapter(adapter);
    
    

    CoyoteAdapter处理的过程又分为如下几个步骤

    • 构造HttpServletRequest和HttpServletResponse的子类,org.apache.catalina.connector.Request&org.apache.catalina.connector.Response
    • 解析tcp数据包,填充request/response
    • 找到相应的context
    • 调用Pipeline进行链式处理

    4.3.1 构造HttpServletRequest/HttpServletResponse

           Request request = (Request) req.getNote(ADAPTER_NOTES);
            Response response = (Response) res.getNote(ADAPTER_NOTES);
    
            if (request == null) {
                // Create objects
                request = connector.createRequest();
                request.setCoyoteRequest(req);
                response = connector.createResponse();
                response.setCoyoteResponse(res);
    
                // Link objects
                request.setResponse(response);
                response.setRequest(request);
    
                // Set as notes
                req.setNote(ADAPTER_NOTES, request);
                res.setNote(ADAPTER_NOTES, response);
    
                // Set query string encoding
                req.getParameters().setQueryStringCharset(connector.getURICharset());
            }
    
        public Request createRequest() {
    
            Request request = new Request();
            request.setConnector(this);
            return (request);
    
        }
    
        public Response createResponse() {
    
            Response response = new Response();
            response.setConnector(this);
            return (response);
    
        }
    

    这里的构造出来的Request/Response 均为HttpServletRequest/HttpServletResponse的子类,并未对我们熟悉的请求参数进行赋值

    4.3.2 解析tcp数据包,填充request/response

    上面得到的Request/Response 关键数据为空,这里做的就是解析数据包,填充request/response。

               // Parse and set Catalina and configuration specific
                // request parameters
                postParseSuccess = postParseRequest(req, request, res, response);
    

    篇幅所限,仅列出部分...

     if (pathParamEnd >= 0) {
                    if (charset != null) {
                        pv = new String(uriBC.getBuffer(), start + pathParamStart,
                                    pathParamEnd - pathParamStart, charset);
                    }
                    // Extract path param from decoded request URI
                    byte[] buf = uriBC.getBuffer();
                    for (int i = 0; i < end - start - pathParamEnd; i++) {
                        buf[start + semicolon + i]
                            = buf[start + i + pathParamEnd];
                    }
                    uriBC.setBytes(buf, start,
                            end - start - pathParamEnd + semicolon);
                } else {
                    if (charset != null) {
                        pv = new String(uriBC.getBuffer(), start + pathParamStart,
                                    (end - start) - pathParamStart, charset);
                    }
                    uriBC.setEnd(start + semicolon);
                }
    

    4.3.3 查找context

    还记得我们在start的过程中说的部署war的线程吗,启动Engine的时候,会同时触发部署war包,部署war的一个关键过程就是初始化context并启动。有了这个前提,我们来看查找context的过程

     while (mapRequired) {
                // 查找context的过程
                connector.getService().getMapper().map(serverName, decodedURI,
                        version, request.getMappingData());
    
             ContextList contextList = mappedHost.contextList;
            MappedContext[] contexts = contextList.contexts;
            int pos = find(contexts, uri);
            if (pos == -1) {
                return;
            }
    
            int lastSlash = -1;
            int uriEnd = uri.getEnd();
            int length = -1;
            boolean found = false;
            MappedContext context = null;
            while (pos >= 0) {
                context = contexts[pos];
                if (uri.startsWith(context.name)) {
                    length = context.name.length();
                    if (uri.getLength() == length) {
                        found = true;
                        break;
                    } else if (uri.startsWithIgnoreCase("/", length)) {
                        found = true;
                        break;
                    }
                }
                if (lastSlash == -1) {
                    lastSlash = nthSlash(uri, contextList.nesting + 1);
                } else {
                    lastSlash = lastSlash(uri);
                }
                uri.setEnd(lastSlash);
                pos = find(contexts, uri);
            }
            uri.setEnd(uriEnd);
    
            if (!found) {
                if (contexts[0].name.equals("")) {
                    context = contexts[0];
                } else {
                    context = null;
                }
            }
            if (context == null) {
                return;
            }
    
            mappingData.contextPath.setString(context.name);
    
            ContextVersion contextVersion = null;
            ContextVersion[] contextVersions = context.versions;
            final int versionCount = contextVersions.length;
            if (versionCount > 1) {
                Context[] contextObjects = new Context[contextVersions.length];
                for (int i = 0; i < contextObjects.length; i++) {
                    contextObjects[i] = contextVersions[i].object;
                }
                mappingData.contexts = contextObjects;
                if (version != null) {
                    contextVersion = exactFind(contextVersions, version);
                }
            }
            if (contextVersion == null) {
                // Return the latest version
                // The versions array is known to contain at least one element
                contextVersion = contextVersions[versionCount - 1];
            }
            mappingData.context = contextVersion.object;
            mappingData.contextSlashCount = contextVersion.slashCount;
    

    查找的过程其实很简单,就是根据uri和context.name做匹配,如果命中,则就会将当前请求转发到该context。值得注意的是context的name和path默认都是war包的名字。

    4.3.4 调用Pipeline进行链式处理

    container内的组件都会有对应的Pipeline,业务处理的时候会调用Pipeline以责任链的形式进行业务处理

     connector.getService().getContainer().getPipeline().getFirst().invoke(
                            request, response);
    

    请求会按照StandardEngineValve→StandardHostValve→StandardContextValve→StandardWrapperValve的流程处理,值得注意的是StandardWrapper是对servlet的包装,这里同样会获取servlet实例。

     if (!unavailable) {
                    servlet = wrapper.allocate();
                }
    

    历经千辛万苦终于到我们熟悉的servlet了,下面的内容相信对每个web人员都不陌生。过滤器链的处理

    if ((servlet != null) && (filterChain != null)) {
                    // Swallow output if needed
                    if (context.getSwallowOutput()) {
                        try {
                            SystemLogHandler.startCapture();
                            if (request.isAsyncDispatching()) {
                                request.getAsyncContextInternal().doInternalDispatch();
                            } else {
                                filterChain.doFilter(request.getRequest(),
                                        response.getResponse());
                            }
                        } finally {
                            String log = SystemLogHandler.stopCapture();
                            if (log != null && log.length() > 0) {
                                context.getLogger().info(log);
                            }
                        }
                    } else {
                        if (request.isAsyncDispatching()) {
                            request.getAsyncContextInternal().doInternalDispatch();
                        } else {
                            filterChain.doFilter
                                (request.getRequest(), response.getResponse());
                        }
                    }
    
     private void internalDoFilter(ServletRequest request,
                                      ServletResponse response)
            throws IOException, ServletException {
    //省略很多代码
    //过滤器的处理
     filter.doFilter(request, response, this);
    //省略很多代码
     //servlet.service(request, response);
    

    是不是还是熟悉的味道,调用filter.doFilter&servlet.service,这样请求就转到我们的DispatcherServlet中了,后续的流程就是spring mvc的处理逻辑了。剩下的就是应用处理,tomcat内的socket回写数据了。到此为止,我们简单分析了tomcat的处理流程。


    5 总结

    知道大家平时工作的时候有没有想过为什么要这么做,实现的原理是什么。常说servlet的生命周期,http请求的过程,但是这些问题到底是如何实现的,为什么tomcat部署我们的应用后我们就能访问其提供的api了呢?带着这些问题,我们简单分析了tomcat的启动流程,以及请求的处理。可以看到,内容还是比较多的。篇幅所限,有些地方我们也是点到即止,没做更深层的刨析。

    希望看了这篇文章的朋友能有些感悟吧。由于能力有限,本篇文章,难免有错漏和不足的地方,欢迎批评指正。我们后续的文章见......

    相关文章

      网友评论

        本文标题:tomcat源码浅析-从一次http请求谈起

        本文链接:https://www.haomeiwen.com/subject/wtuqrktx.html