tomcat源码分析三:tomcat启动与停止

总体概述

tomcat类有三种工作模式,当tomcat作为“独立的Servlet容器“时,tomcat的启动与停止是通过执行bin目录下的脚本完成的。脚本的入口方法是Bootstrap的main方法。具体的启动与停止都是通过BootStrap反射调用Catalina的实例来实现的。Catalina是tomcat生命期的一个开始类,BootStrap是对Catalina类所封装,提供了main()方法,方便调用。下面我们来研究一下tomcat的启动与停止过程。

tomcat运行可以分为以下三个阶段
1. 启动过程:启动相应的Server,Service,Connector等组件
2. 响应过程:主线程等待接收停止tomcat信号,其它线程接收并请求处理。
3. 结束过程:接收到停止信号后,关闭tomcat

总体分析

先看入口类Bootstrap的定义

//几个重要属性

private static Bootstrap daemon = null;
//Catalina类的实例
private Object catalinaDaemon = null;
//三个重要的ClassLoader
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;


public static void main(String args[]) {

        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
        // When running as a service the call to stop will be on a new
        // thread so make sure the correct class loader is used to prevent
        // a range of class not found exceptions。
        Thread。currentThread().setContextClassLoader(daemon。catalinaLoader);
    }

        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon。.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null==daemon。getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist。");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }

main() 方法中,首先判定daemon 是否为null,每次通过脚本执行main()函数时,一定会是一个新的线程,所以这里的deamon一定为空,daemon为空时,调用init()方法,该方法的主要功能初始始化相关的ClassLoader,创建catalinaDaemon,即``Catalina实例 ,并且给catalinaDaemon设置相应的parentClassLoader,最后将新建的BootStrap设置给deamon
而后处理具体命令及参数,看到主要的几个调用的方法是

daemon.setAwait(true);  //设置await
daemon.load(args);      //加载配置
daemon.start();         //启动服务
daemon.stop();          // 停止服务
daemon.stopServer(args);   //停止server

上面这五个函数的实现分别分通过反射调用catalinaDaemonsetAwait,load,start,stop,stopServer方法。这五个方法就是tomcat启动与停止的核心方法。这五个方法完成了tomcat的启动,主线程等待,tomcat停止的功能,stop的作用是停止服务,而stopServer的作用是发送停止命令。

首先来看Catalina类的属性的定义

 protected boolean await = false;
 protected Server server = null;  //Catalina中有一个属性为Server,所有对Server的操作都是针对此,而server是通过解析xml文件得到的。

设置await

与Await相关的方法有两个。await是一个简单变量,主线程会根据该变量确定是否等待。下面会再提到。

    public void setAwait(boolean b) {
        await = b;
    }

    public boolean isAwait() {
        return await;
    }

加载配置

public void load() {
        initDirs();
        // Create and execute our Digester
        Digester digester = createStartDigester();

        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        try {
                // 下面省略了查找配置文件的详细代码,可以下载具体的源码查看。主要流程查找 "conf/server.xml" 如果没有找到,就查找"server-embed.。xml"文件

            if (inputStream == null || inputSource == null) {
                return;
            }

            try {
                inputSource.setByteStream(inputStream);
                digester.push(this);
                digester.parse(inputSource);
            }  catch (Exception e) {
                return;
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }

        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap。getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap。getCatalinaBaseFile());

        // Stream redirection
        initStreams();

        // Start the new server
        try {
            getServer().init();
        } catch (LifecycleException e) {
        }
    }

上面的主要功能是根据配置文件如/conf/server.xml,加载tomcat,可以分为几步

  1. 初始化Digester :配置一个解析/conf/server.xml的解析器
  2. 查找配置文件(/conf/server.xml 或者server-embed.xml)
  3. 解析配置文件
  4. 初始化server(对Server设置相当的属性,此下详细介绍)
  5. 调用serverinit()方法: 修改生命周期状态,调用serviceinit()方法

    第1步是构建了一个解析配置文件的解析器(与配置文件相对应),第2步在代码中进行了省略,第3步是采用该解析器解析相应的配置文件。下面来看下文件是如何解析的。

    解析文件

    解析文件是通过Digester完成的。下面是对Digester的简单介绍,来自于百度百科,并且做了一定的修改

    Digester本来是Jakarta Struts中的一个工具,用于处理struts-config。xml配置文件。如今Digester随着Struts的发展以及其的公用性而被提到commons中独自立项,是apache的一个组件 apache commons-digester.jar,通过它可以很方便的从xml文件生成java对象。你不用再象以前通过jdom或者Xerces去读取一个document对象。Digester由”事件”驱动,通过调用预定义的规则操作对象栈,将XML文件转换为Java对象。

    附一份默认的/conf/server.xml的配置,去掉了注释

    <?xml version="1.0" encoding="UTF-8"?>
    <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 name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <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> 

上面这个配置是默认的/conf/server.xml的配置,Digester通过该配置初始化server。从这个配置中可以看到,定义了一个端口号为8005,’shutdown’ 为’SHUTDOWN’的字符串,这两个是为停止tomcat服务的。 最外层的元素为Server,该Server有一个Service,Service有两个Connector,一个是http/1。1的,另外一个是Ajp/1。3的,与该Connector相关联的Engine的name为Catalina,其默认的Host为defaultHost。并且定义了一个默认的Host。这个里面,没有定义Context与Wrapper。
Digester的作用就是将这个文件转化为相应的类的定义。因此,在实际的tomcat组件中,该Server只有一个Service,这个Service有两个Connector及一个Engine。具体的可以参考tomcat源码分析一:总体简介(其它的同配置文件,不详细展开)

启动服务

先看代码

public void start() {

        if (getServer() == null) {
            load();
        }

        if (getServer() == null) {
            log。fatal("Cannot start server。 Server instance is not configured。");
            return;
        }
        // Start the new server
        try {
            getServer().start();
        } catch (LifecycleException e) {

            return;
        }
        // Register shutdown hook
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);

            // If JULI is being used, disable JULI's shutdown hook since
            // shutdown hooks run in parallel and log messages may be lost
            // if JULI's hook completes before the CatalinaShutdownHook()
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager)。setUseShutdownHook(
                        false);
            }
        }

        if (await) {
            await();
            stop();
        }
    }

start()主要完成了以下几个功能

  1. 调用server.start():改变当前的生命周期状态,并通过调用service.start()启动相应的Engine,Host,Connector等,进而可以开始接收及处理请求。后面找时间会详细展开。
  2. 如果useShutdownHook为真,配置服务停止时的钩子,这个主要是担心非正常关闭时tomcat时,相关服务可以得到正常关闭。
  3. 如果await为真时,调用await(),await()完成后,调用stop()方法,当tomcat作为独立的servlet容器使用时,因为调用上面叙述的setAwait()设置await变量为true。await()的主要功能是实现当前线程等待。当await()完成后,说明当前线程退出等待,即接收到了关闭tomcat的命令,执行stop()方法。

    等待

    //先看一个变量,这个变量用于标识是否要停止主线程等待时的标识,当为true时,就表示可以停止开闭tomcat了,这个值在调用 server.stop()时会被设置。
    private volatile boolean stopAwait = false;
    
    public void await() {
        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
        if( port == -2 ) {
            // undocumented yet - for embedding apps that are around, alive.
            return;
        }
        if( port==-1 ) {
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }
    
        // Set up a server socket to wait on
        // 起一个监听端口,这个端口号就是我们上面 server。xml配置文件中的端口号
        try {
            awaitSocket = new ServerSocket(port, 1,
                    InetAddress.getByName(address));
        } catch (IOException e) {
            log.error("StandardServer.await: create[" + address
                               + ":" + port
                               + "]: ", e);
            return;
        }
        try {
            awaitThread = Thread.currentThread();
    
            // Loop waiting for a connection and a valid command
            while (!stopAwait) {
                ServerSocket serverSocket = awaitSocket;
                if (serverSocket == null) {
                    break;
                }
    
                // Wait for the next connection
                Socket socket = null;
                StringBuilder command = new StringBuilder();
                try {
                    InputStream stream;
                    long acceptStartTime = System.currentTimeMillis();
                    try {
                        socket = serverSocket.accept();
                        socket.setSoTimeout(10 * 1000);  // Ten seconds
                        stream = socket.getInputStream();
                    } catch (SocketTimeoutException ste) {
                        // This should never happen but bug 56684 suggests that
                        // it does.
                        log.warn(sm.getString("standardServer.accept.timeout",
                                Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
                        continue;
                    } catch (AccessControlException ace) {
                        log.warn("StandardServer.accept security exception: "
                                + ace.getMessage(), ace);
                        continue;
                    } catch (IOException e) {
                        if (stopAwait) {
                            // Wait was aborted with socket.close()
                            break;
                        }
                        log.error("StandardServer.await: accept: ", e);
                        break;
                    }
    
                    // Read a set of characters from the socket
                    int expected = 1024; // Cut off to avoid DoS attack
                    while (expected < shutdown.length()) {
                        if (random == null)
                            random = new Random();
                        expected += (random.nextInt() % 1024);
                    }
                    while (expected > 0) {
                        int ch = -1;
                        try {
                            ch = stream.read();
                        } catch (IOException e) {
                            log.warn("StandardServer.await: read: ", e);
                            ch = -1;
                        }
                        // Control character or EOF (-1) terminates loop
                        if (ch < 32 || ch == 127) {
                            break;
                        }
                        command.append((char) ch);
                        expected--;
                    }
                } finally {
                    // Close the socket now that we are done with it
                    try {
                        if (socket != null) {
                            socket.close();
                        }
                    } catch (IOException e) {
                        // Ignore
                    }
                }
    
                // Match against our command string
                boolean match = command.toString().equals(shutdown);
                if (match) {
                    log.info(sm.getString("standardServer.shutdownViaPort"));
                    break;
                } else
                    log.warn("StandardServer.await: Invalid command '"
                            + command.toString() + "' received");
            }
        } finally {
            ServerSocket serverSocket = awaitSocket;
            awaitThread = null;
            awaitSocket = null;
    
            // Close the server socket and return
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
    }

    对这块儿代码理解需要先了解几个内容

    1. port 及 shutdown 分别是前文中提到的配置文件的端口号及shutdown 字符串,用于在哪个端口号监听停止命令以及停止命令是什么
    2. stopAwait 变量:这个初始化后为false,在执行server.stop() 后,会变为true。

    await()方法的主要目的是让当前线程等待,主要分三个部分

    1. 当port为-2时,表示tomcat是作为一个非独立的servlet容器使用的,不需要关心
    2. 当port 为-1 时, 会定时循环查看 stopAwait 变量是否是否为true,为true时,退出循环,否则,当前线程一直进行sleep。
    3. 其它情况时(port为正,当tomcat作为独立的servlet容器时),会监听相应的端口,当接收到输入时,取得相应的输入,并匹配输入与 目标的shutdown 命令是否一致,如果一致,则退出,不一致,则根据stopAwait值确定继续等待还是退出。同时这里考虑了在该端口上进行dos攻击的情况。

此时,tomcat已经完成启动,当前线程是非deamon的,会启动若干deamon线程,那些线程用于接收并处理来自客户端的请求,当当前的非deamon退出时,deamon线程自动退出,进而tomcat关闭

当从await()退出后,执行stop()方法,关闭tomcat。

停止服务

不上源码了,比较简单,主要分为两步

9.调用server.stop()方法
10. 在server.stop()方法内部,调用service.stop()stopAwait() (设置stopAwait变量)的方法,进而让主线程退出,并且执行一些退出操作。

停止server

 public void stopServer(String[] arguments) {
        Server s = getServer();
        if (s == null) {
            // Create and execute our Digester
            Digester digester = createStopDigester();
            File file = configFile();
            try (FileInputStream fis = new FileInputStream(file)) {
                InputSource is =
                    new InputSource(file。toURI()。toURL()。toString());
                is.setByteStream(fis);
                digester.push(this);
                digester.parse(is);
            } catch (Exception e) {
                log.error("Catalina。stop: ", e);
                System.exit(1);
            }
        } else {
            // Server object already present。 Must be running as a service
            try {
                s.stop();
            } catch (LifecycleException e) {
            }
            return;
        }

        // Stop the existing server
        s = getServer();
        if (s.getPort()>0) {
            try (Socket socket = new Socket(s.getAddress(), s.getPort());
                    OutputStream stream = socket.getOutputStream()) {
                String shutdown = s.getShutdown();
                for (int i = 0; i < shutdown.length(); i++) {
                    stream.write(shutdown.charAt(i));
                }
                stream.flush();
            } catch (ConnectException ce) {
                System.exit(1);
            } catch (IOException e) {
                System.exit(1);
            }
        } else {
            System.exit(1);
        }
    }

主要分两种情况,这两种情况与await()方法中的端口号为-1及端口号大于0相对应。
1。 当此时的server不为null时,直接调用stop()进行关闭,因为每次执行main方法时,都是一个新的线程,都是新建的一个deamon,而server是在load的方法中建立的,所以在通过main方法执行时,此处一定为null,同时在代码的注释也说明了,此时的tomcat是作为一个服务运行的,意思是不是作为一个独立的servlet容器。
2。 当些时server为null时(作为一个独立的servlet容器时),根据conf/server.xml()新建一个server(实际上只是获得了端口号与停止命令),如果端口号大于0时,向相应的端口中发送shutdown命令。,主线程退出等待,关闭tomcat。

总结

当tomcat作为一个独立的servlet容器运行时,通过调用BootStrapmain()方法开启与停止tomcat服务。tomcat启动时,会调用server的变量await为true,基于此变量,当前线程被serverSocket.accept()block住。此时,tomcat完成启动,启动了一些deamon线程,这些线和用于接收并处理请求。直到当前启动线程接收到shutdown命令时,退出await()`,即退出等待,关闭tomcat。

  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

曲雨齐

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值