浅读Tomcat源码(二)---启动过程

上一篇我们讲了Tomcat源码包的下载配置以及Tomcat组件的基本介绍,这一篇我们着重来讲述Tomcat的完整启动过程。


众所周知,Tomcat的启动和停止是有startup.sh/bat和shutdown.sh/bat来控制的,这里sh是linux的shellScript脚本,其运行的模式是调用catalina.sh,并传入参数startup或shutdown,具体就不做详述了,而catalina.sh最终会调用java类org.apache.catalina.startup.BootStrap的start或stop方法,我们来看下Bootstrap的start方法:

  /**
     * Start the Catalina daemon.
     */
    public void start()
        throws Exception {
        if( catalinaDaemon==null ) init();

        Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
        method.invoke(catalinaDaemon, (Object [])null);

    }

这里我们可以看到bootstrap的start方法其实是用反射形式调用真正的启动类的start方法,这里的catalinaDamon其实是同一个包下的Catalina类,其start方法如下:

 /**
     * Start a new server instance.
     */
    public void start() {

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

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        long t1 = System.nanoTime();

        // Start the new server
        try {
            getServer().start();
        } catch (LifecycleException e) {
            log.error("Catalina.start: ", e);
        }

        long t2 = System.nanoTime();
        if(log.isInfoEnabled())
            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");

        try {
            // 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);
                }
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            // This will fail on JDK 1.2. Ignoring, as Tomcat can run
            // fine without the shutdown hook.
        }

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

    }

代码很长,我们可以看到他使用了getServer,获取到了对应的server组件,server组件的初始化是在init的过程中,对应到本类的load方法,其中核心的代码块是:

// Create and execute our Digester
        Digester digester = createStartDigester();
// some code
     digester.push(this);
     digester.parse(inputSource); 	

我们可以看到这里使用Digester去初始化一些组件,Digester的实际作用是解析server.xml,用的是SAX包解析,这里本篇先不做详述。


总而言之在解析完server.xml之后,catalina中的server类算是初始化了,然后就调用了server类的startup方法,来看server接口的实现类org.apache.catalina.core.StandardServer:

@Override
    protected void startInternal() throws LifecycleException {

        fireLifecycleEvent(CONFIGURE_START_EVENT, null);
        setState(LifecycleState.STARTING);

        globalNamingResources.start();
        
        // Start our defined Services
        synchronized (services) {
            for (int i = 0; i < services.length; i++) {
                services[i].start();
            }
        }
    }
我们可以看到server类的start就是将server.xml中解析出来的service全部启动,但是要注意一点的是,server类还有个非常大的作用,在上述catalina的启动中我们很清楚看到了其调用了await方法:

/**
     * Await and shutdown.
     */
    public void await() {

        getServer().await();

    }


而server类的await方法是这样的:

/**
     * Wait until a proper shutdown command is received, then return.
     * This keeps the main thread alive - the thread pool listening for http 
     * connections is daemon threads.
     */
    @Override
    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
        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;
                    try {
                        socket = serverSocket.accept();
                        socket.setSoTimeout(10 * 1000);  // Ten seconds
                        stream = socket.getInputStream();
                    } 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;
                        }
                        if (ch < 32)  // Control character or EOF terminates loop
                            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
                }
            }
        }
    }

这段代码曾经将我带入了一个彻底的误区,以为这就是Tomcat接收请求的serverSocket的启动,但是事实完全不是这样子,当我看到port的默认值的时候:

private int port = 8005;

以及在server.xml的配置的时候
<Server port="8005" shutdown="SHUTDOWN">

我才明白过来,众所周知Tomcat启动时候默认占用了三个端口:8080、8005、8009,8080和8009分别是HTTP和AJP协议的服务端口,那么8005是干嘛的呢,其实8005是tomcat停止监听线程的服务端口,看上面await方法就可以看出,这个serverSocket一直在监听shutdown指令,如果接受到了则开始停止过程。


这也说明了上面在catalina的start过程中为何要把awiat方法放在最后,因为我们看到这个停止监听是运行在当前线程(即启动线程)下的,必须确保所有需要开启的服务都在新线程中运行起来再进行await过程。


现在回过头来看service的start过程,我们看到org.apache.catalina.core.StandardService类

 @Override
    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
        if (container != null) {
            synchronized (container) {
                container.start();
            }
        }

        synchronized (executors) {
            for (Executor executor: executors) {
                executor.start();
            }
        }

        // Start our defined Connectors second
        synchronized (connectors) {
            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);
                }
            }
        }
    }

可以看到这个过程的启动有三件,其中目前我比较清楚的是头尾两件,一是容器container的启动,这里的container实则是engine容器;二是connector的启动,Connector是server.xml中注册的,我们先来看Connector的启动过程,看到org.apache.catalina.connector.Connector类:

/**
     * Begin processing requests via this Connector.
     *
     * @exception LifecycleException if a fatal startup error occurs
     */
    @Override
    protected void startInternal() throws LifecycleException {

        setState(LifecycleState.STARTING);

        try {
            protocolHandler.start();
        } catch (Exception e) {
            String errPrefix = "";
            if(this.service != null) {
                errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
            }

            throw new LifecycleException
                (errPrefix + " " + sm.getString
                 ("coyoteConnector.protocolHandlerStartFailed"), e);
        }

        mapperListener.start();
    }

这里启动了protocolHandler,根据Connector的实质作用的不同,Tomcat会给他分配不同的Handler,这里我们看到用于处理Http1.1的handler,org.apache.coyote.http11.Http11Protocol类,看到其对应的start,这里的start是在其父类AbstratcProtocolHandler中实现的:

   @Override
    public void start() throws Exception {
        if (getLog().isInfoEnabled())
            getLog().info(sm.getString("abstractProtocolHandler.start",
                    getName()));
        try {
            endpoint.start();
        } catch (Exception ex) {
            getLog().error(sm.getString("abstractProtocolHandler.startError",
                    getName()), ex);
            throw ex;
        }
    }

这里启动了endpoint,看到其子类JIOEndpoint:

 @Override
    public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            // Create worker collection
            if (getExecutor() == null) {
                createExecutor();
            }
            
            initializeConnectionLatch();

            // Start acceptor threads
            for (int i = 0; i < acceptorThreadCount; i++) {
                Thread acceptorThread = new Thread(new Acceptor(),
                        getName() + "-Acceptor-" + i);
                acceptorThread.setPriority(threadPriority);
                acceptorThread.setDaemon(getDaemon());
                acceptorThread.start();
            }
            
            // Start async timeout thread
            Thread timeoutThread = new Thread(new AsyncTimeout(),
                    getName() + "-AsyncTimeout");
            timeoutThread.setPriority(threadPriority);
            timeoutThread.setDaemon(true);
            timeoutThread.start();
        }
    }

在这里启动了新线程,新线程的操作定义在其内部类Acceptor类中:

/**
     * Server socket acceptor thread.
     */
    protected class Acceptor implements Runnable {


        /**
         * The background thread that listens for incoming TCP/IP connections and
         * hands them off to an appropriate processor.
         */
        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                try {
                    //if we have reached max connections, wait
                    awaitConnection();

                    Socket socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        socket = serverSocketFactory.acceptSocket(serverSocket);
                    } catch (IOException ioe) {
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    if (setSocketOptions(socket)) {
                        // Hand this socket off to an appropriate processor
                        if (!processSocket(socket)) {
                            // Close socket right away
                            try {
                                socket.close();
                            } catch (IOException e) {
                                // Ignore
                            }
                        } else {
                            countUpConnection();
                        }
                    } else {
                        // Close socket right away
                        try {
                            socket.close();
                        } catch (IOException e) {
                            // Ignore
                        }
                    }
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (NullPointerException npe) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), npe);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
                // The processor will recycle itself when it finishes
            }
        }
    }
看到这里,我们长舒了一口气,因为终于看到了处理http请求的监听在8080端口serverSocket运行起来,从代码看这里把接收到的socket通过processSocket方法进行处理,这在之后的请求处理的篇章再做详述,到此为止Tomcat自身类的组件算是启动完成了,但是对于我们开发人员添加的web应用的处理还尚未讲解,我们回到service的start方法中,看到这里container的启动:

  @Override
    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
        if (container != null) {
            synchronized (container) {
                container.start();
            }
        }
}

上一篇我们说过,tomcat的容器有engine、host、context、wrapper四种,其中前两者都是在server.xml中解析出来的,而后两者则需要在运行时候再去读取,我们来看container的基本实现类org.apache.catalina.core.ContainerBase类的start过程:


/**
     * Start this component and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Start our subordinate components, if any
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();
        logger = null;
        getLogger();
        if ((logger != null) && (logger instanceof Lifecycle))
            ((Lifecycle) logger).start();
        if ((manager != null) && (manager instanceof Lifecycle))
            ((Lifecycle) manager).start();
        if ((cluster != null) && (cluster instanceof Lifecycle))
            ((Lifecycle) cluster).start();
        if ((realm != null) && (realm instanceof Lifecycle))
            ((Lifecycle) realm).start();
        if ((resources != null) && (resources instanceof Lifecycle))
            ((Lifecycle) resources).start();

        // Start our child containers, if any
        Container children[] = findChildren();
        for (int i = 0; i < children.length; i++) {
            children[i].start();
        }

        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle)
            ((Lifecycle) pipeline).start();


        setState(LifecycleState.STARTING);

        // Start our thread
        threadStart();

    }
可以看到所有的容器都有一个启动子组件的过程,但是重点在于刚开始父组件是如何拿到子组件的呢,其实我个人觉得这里的for循环只是保险起见,万一子组件已经被纳入了父组件中的操作,真正实现启动子组件的是下面的threadStart过程:

 /**
     * Start the background thread that will periodically check for
     * session timeouts.
     */
    protected void threadStart() {

        if (thread != null)
            return;
        if (backgroundProcessorDelay <= 0)
            return;

        threadDone = false;
        String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
        thread = new Thread(new ContainerBackgroundProcessor(), threadName);
        thread.setDaemon(true);
        thread.start();

    }

这个过程是个非常长的链式调用,大家自己去一个个ctrl点击进去,到最后会发现四个组件都有对应的初始化控制类,在startup包下的HostConfig、ContextConfig等,其实现了lifecycleEvent方法,我们专门来看一下Host组件对应的Config的过程:

 @Override
    public void lifecycleEvent(LifecycleEvent event) {

        if (event.getType().equals(Lifecycle.PERIODIC_EVENT))
            check();

        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
            }
        } catch (ClassCastException e) {
            log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.START_EVENT))
            start();
        else if (event.getType().equals(Lifecycle.STOP_EVENT))
            stop();

    }

这里我们看到,如果tomcat处于启动之初,会调用check方法,check则会调用deployApps方法:

**
     * Deploy applications for any directories or WAR files that are found
     * in our "application root" directory.
     */
    protected void deployApps() {

        File appBase = appBase();
        File configBase = configBase();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs, and loop if additional descriptors are found
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);
        
    }
这里就分流处理了,取得webapp文件夹的路径之后,会分别处理war、文件夹等多种web应用的发布,根据经验我们知道war的发布实质是tomcat将其解压成为文件夹应用,所以我们关键看下deployDirectories方法:

/**
     * Deploy directories.
     */
    protected void deployDirectories(File appBase, String[] files) {

        if (files == null)
            return;
        
        for (int i = 0; i < files.length; i++) {

            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            File dir = new File(appBase, files[i]);
            if (dir.isDirectory()) {
                ContextName cn = new ContextName(files[i]);

                if (isServiced(cn.getName()))
                    continue;

                deployDirectory(cn, dir, files[i]);
            }
        }
    }

获取所有文件夹,并进入deployDirectory:

 /**
     * @param cn
     * @param dir
     * @param file
     */
    protected void deployDirectory(ContextName cn, File dir, String file) {
        
        if (deploymentExists(cn.getName()))
            return;

        DeployedApplication deployedApp = new DeployedApplication(cn.getName());

        // Deploy the application in this directory
        if( log.isInfoEnabled() ) 
            log.info(sm.getString("hostConfig.deployDir", file));
        try {
            Context context = null;
            File xml = new File(dir, Constants.ApplicationContextXml);
            File xmlCopy = null;
            if (deployXML && xml.exists()) {
                synchronized (digester) {
                    try {
                        context = (Context) digester.parse(xml);
                        if (context == null) {
                            log.error(sm.getString(
                                    "hostConfig.deployDescriptor.error",
                                    xml));
                            return;
                        }
                    } finally {
                        digester.reset();
                    }
                }
                if (copyXML) {
                    xmlCopy = new File(configBase(), file + ".xml");
                    InputStream is = null;
                    OutputStream os = null;
                    try {
                        is = new FileInputStream(xml);
                        os = new FileOutputStream(xmlCopy);
                        IOTools.flow(is, os);
                        // Don't catch IOE - let the outer try/catch handle it
                    } finally {
                        try {
                            if (is != null) is.close();
                        } catch (IOException e){
                            // Ignore
                        }
                        try {
                            if (os != null) os.close();
                        } catch (IOException e){
                            // Ignore
                        }
                    }
                    context.setConfigFile(xmlCopy.toURI().toURL());
                } else {
                    context.setConfigFile(xml.toURI().toURL());
                }
            } else {
                context = (Context) Class.forName(contextClass).newInstance();
            }

            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener =
                (LifecycleListener) clazz.newInstance();
            context.addLifecycleListener(listener);

            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            context.setDocBase(file);
            host.addChild(context);
            deployedApp.redeployResources.put(dir.getAbsolutePath(),
                    Long.valueOf(dir.lastModified()));
            if (xmlCopy != null) {
                deployedApp.redeployResources.put(
                        xmlCopy.getAbsolutePath(),
                        Long.valueOf(xmlCopy.lastModified()));
            }
            addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployDir.error", file), t);
        }

        deployed.put(cn.getName(), deployedApp);
    }

当初我看到这段代码真的是一身爽快,因为终于找到容器的初始化方式了,我们看到这里初始化了StandardContext类对象,初始化完了以后将web应用的根路径等信息全部填入context,并让context去初始化wrapper的内容,中间过程我们省去了,大家自己看,我们直接找到ContextConfig解析过程的核心代码:

protected void parseWebXml(InputSource source, WebXml dest,
            boolean fragment) {
        
        if (source == null) return;


        XmlErrorHandler handler = new XmlErrorHandler();


        // Web digesters and rulesets are shared between contexts but are not
        // thread safe. Whilst there should only be one thread at a time
        // processing a config, play safe and sync.
        Digester digester;
        if (fragment) {
            digester = webFragmentDigester;
        } else {
            digester = webDigester;
        }
        
        synchronized(digester) {
            
            digester.push(dest);
            digester.setErrorHandler(handler);
            
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("contextConfig.applicationStart",
                        source.getSystemId()));
            }


            try {
                digester.parse(source);


                if (handler.getWarnings().size() > 0 ||
                        handler.getErrors().size() > 0) {
                    ok = false;
                    handler.logFindings(log, source.getSystemId());
                }
            } catch (SAXParseException e) {
                log.error(sm.getString("contextConfig.applicationParse",
                        source.getSystemId()), e);
                log.error(sm.getString("contextConfig.applicationPosition",
                                 "" + e.getLineNumber(),
                                 "" + e.getColumnNumber()));
                ok = false;
            } catch (Exception e) {
                log.error(sm.getString("contextConfig.applicationParse",
                        source.getSystemId()), e);
                ok = false;
            } finally {
                digester.reset();
                if (fragment) {
                    webFragmentRuleSet.recycle();
                } else {
                    webRuleSet.recycle();
                }
            }
        }
    }

这里还是用degister,只不过这里的degister是用来解析web.xml的,解析完了以后,将每个Servlet类的信息装载在一个Wrapper中,到此为止容器的基本启动算是完成了。


最后还记得connector的start过程中有一个mappingListner的init吗?

 @Override
    public void startInternal() throws LifecycleException {

        setState(LifecycleState.STARTING);

        // Find any components that have already been initialized since the
        // MBean listener won't be notified as those components will have
        // already registered their MBeans
        findDefaultHost();
        
        Engine engine = (Engine) connector.getService().getContainer();
        addListeners(engine);
        
        Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
                // Registering the host will register the context and wrappers
                registerHost(host);
            }
        }
    }

这个过程就是从connector对应的service中取出容器engine,并注册其子容器,依次注册,之后在请求的处理过程中去调动注册的容器,我们下一篇再讲述。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值