Tomcat 7 服務器關閉原理

以前的幾篇文章講了 Tomcat 的啓動過程,在默認的配置下啓動完以後會看到後臺實際上總共有 6 個線程在運行。即 1 個用戶線程,剩下 5 個爲守護線程(下圖中的 Daemon Thread )。 apache

若是對什麼叫守護線程的概念比較陌生,這裏再重複一下:

所謂守護線程,是指在程序運行的時候在後臺提供一種通用服務的線程,好比垃圾回收線程。這種線程並不屬於程序中不可或缺的部分,當全部的非守護線程結束時,程序也就終止了,同時會殺死進程中的全部守護線程。反過來講,只要任何非守護線程還在運行,程序就不會終止。編程

用戶線程和守護線程二者幾乎沒有區別,惟一的不一樣之處就在於虛擬機的離開:若是用戶線程已經所有退出運行了,只剩下守護線程存在了,虛擬機也就退出了。由於沒有了被守護者,守護線程也就沒有工做可作了,也就沒有繼續運行程序的必要了。將線程轉換爲守護線程能夠經過調用 Thread 對象的 setDaemon(true) 方法來實現。瀏覽器

Tomcat 的關閉正是利用了這個原理,即只要將那惟一的一個用戶線程關閉,則整個應用就關閉了。tomcat

要研究這個用戶線程怎麼被關閉的得先從這個線程從何產生提及。在前面分析 Tomcat 的啓動時咱們是從org.apache.catalina.startup.Bootstrap類的 main 方法做爲入口,該類的 453 到 456 行是 Tomcat 啓動時會執行的代碼: bash

前面的文章裏分析了 daemon.load 和 daemon.start 方法,這裏請注意 daemon.setAwait(true); 這句,它的做用是經過反射調用 org.apache.catalina.startup.Catalina類的 setAwait(true) 方法,最終將 Catalina 類的實例變量 await 設值爲 true 。

Catalina 類的 setAwait 方法代碼:app

/**
     * Set flag.
     */
    public void setAwait(boolean await)
        throws Exception {

        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Boolean.TYPE;
        Object paramValues[] = new Object[1];
        paramValues[0] = Boolean.valueOf(await);
        Method method =
            catalinaDaemon.getClass().getMethod("setAwait", paramTypes);
        method.invoke(catalinaDaemon, paramValues);

    }
複製代碼

如前文分析,Tomcat 啓動時會調用org.apache.catalina.startup.Catalina類的 start 方法,看下這個方法的代碼:dom

1	    /**
     2	     * Start a new server instance.
     3	     */
     4	    public void start() {
     5	
     6	        if (getServer() == null) {
     7	            load();
     8	        }
     9	
    10	        if (getServer() == null) {
    11	            log.fatal("Cannot start server. Server instance is not configured.");
    12	            return;
    13	        }
    14	
    15	        long t1 = System.nanoTime();
    16	
    17	        // Start the new server
    18	        try {
    19	            getServer().start();
    20	        } catch (LifecycleException e) {
    21	            log.fatal(sm.getString("catalina.serverStartFail"), e);
    22	            try {
    23	                getServer().destroy();
    24	            } catch (LifecycleException e1) {
    25	                log.debug("destroy() failed for failed Server ", e1);
    26	            }
    27	            return;
    28	        }
    29	
    30	        long t2 = System.nanoTime();
    31	        if(log.isInfoEnabled()) {
    32	            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
    33	        }
    34	
    35	        // Register shutdown hook
    36	        if (useShutdownHook) {
    37	            if (shutdownHook == null) {
    38	                shutdownHook = new CatalinaShutdownHook();
    39	            }
    40	            Runtime.getRuntime().addShutdownHook(shutdownHook);
    41	
    42	            // If JULI is being used, disable JULI's shutdown hook since
    43	            // shutdown hooks run in parallel and log messages may be lost
    44	            // if JULI's hook completes before the CatalinaShutdownHook()
    45	            LogManager logManager = LogManager.getLogManager();
    46	            if (logManager instanceof ClassLoaderLogManager) {
    47	                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
    48	                        false);
    49	            }
    50	        }
    51	
    52	        if (await) {
    53	            await();
    54	            stop();
    55	        }
    56	    }
複製代碼

前文分析啓動時發現經過第 19 行 getServer().start() 的此次方法調用,Tomcat 接下來會一步步啓動全部在配置文件中配置的組件。後面的代碼沒有分析,這裏請關注最後第 52 到 55 行,上面說到已經將 Catalina 類的實例變量 await 設值爲 true,因此這裏將會執行 Catalina 類的 await 方法:socket

/**
     * Await and shutdown.
     */
    public void await() {
    
        getServer().await();
        
    }
複製代碼

該方法就一句話,意思是調用org.apache.catalina.core.StandardServer類的 await 方法:ide

1	    /**
     2	     * Wait until a proper shutdown command is received, then return.
     3	     * This keeps the main thread alive - the thread pool listening for http 
     4	     * connections is daemon threads.
     5	     */
     6	    @Override
     7	    public void await() {
     8	        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
     9	        if( port == -2 ) {
    10	            // undocumented yet - for embedding apps that are around, alive.
    11	            return;
    12	        }
    13	        if( port==-1 ) {
    14	            try {
    15	                awaitThread = Thread.currentThread();
    16	                while(!stopAwait) {
    17	                    try {
    18	                        Thread.sleep( 10000 );
    19	                    } catch( InterruptedException ex ) {
    20	                        // continue and check the flag
    21	                    }
    22	                }
    23	            } finally {
    24	                awaitThread = null;
    25	            }
    26	            return;
    27	        }
    28	
    29	        // Set up a server socket to wait on
    30	        try {
    31	            awaitSocket = new ServerSocket(port, 1,
    32	                    InetAddress.getByName(address));
    33	        } catch (IOException e) {
    34	            log.error("StandardServer.await: create[" + address
    35	                               + ":" + port
    36	                               + "]: ", e);
    37	            return;
    38	        }
    39	
    40	        try {
    41	            awaitThread = Thread.currentThread();
    42	
    43	            // Loop waiting for a connection and a valid command
    44	            while (!stopAwait) {
    45	                ServerSocket serverSocket = awaitSocket;
    46	                if (serverSocket == null) {
    47	                    break;
    48	                }
    49	    
    50	                // Wait for the next connection
    51	                Socket socket = null;
    52	                StringBuilder command = new StringBuilder();
    53	                try {
    54	                    InputStream stream;
    55	                    try {
    56	                        socket = serverSocket.accept();
    57	                        socket.setSoTimeout(10 * 1000);  // Ten seconds
    58	                        stream = socket.getInputStream();
    59	                    } catch (AccessControlException ace) {
    60	                        log.warn("StandardServer.accept security exception: "
    61	                                + ace.getMessage(), ace);
    62	                        continue;
    63	                    } catch (IOException e) {
    64	                        if (stopAwait) {
    65	                            // Wait was aborted with socket.close()
    66	                            break;
    67	                        }
    68	                        log.error("StandardServer.await: accept: ", e);
    69	                        break;
    70	                    }
    71	
    72	                    // Read a set of characters from the socket
    73	                    int expected = 1024; // Cut off to avoid DoS attack
    74	                    while (expected < shutdown.length()) {
    75	                        if (random == null)
    76	                            random = new Random();
    77	                        expected += (random.nextInt() % 1024);
    78	                    }
    79	                    while (expected > 0) {
    80	                        int ch = -1;
    81	                        try {
    82	                            ch = stream.read();
    83	                        } catch (IOException e) {
    84	                            log.warn("StandardServer.await: read: ", e);
    85	                            ch = -1;
    86	                        }
    87	                        if (ch < 32)  // Control character or EOF terminates loop
    88	                            break;
    89	                        command.append((char) ch);
    90	                        expected--;
    91	                    }
    92	                } finally {
    93	                    // Close the socket now that we are done with it
    94	                    try {
    95	                        if (socket != null) {
    96	                            socket.close();
    97	                        }
    98	                    } catch (IOException e) {
    99	                        // Ignore
   100	                    }
   101	                }
   102	
   103	                // Match against our command string
   104	                boolean match = command.toString().equals(shutdown);
   105	                if (match) {
   106	                    log.info(sm.getString("standardServer.shutdownViaPort"));
   107	                    break;
   108	                } else
   109	                    log.warn("StandardServer.await: Invalid command '"
   110	                            + command.toString() + "' received");
   111	            }
   112	        } finally {
   113	            ServerSocket serverSocket = awaitSocket;
   114	            awaitThread = null;
   115	            awaitSocket = null;
   116	
   117	            // Close the server socket and return
   118	            if (serverSocket != null) {
   119	                try {
   120	                    serverSocket.close();
   121	                } catch (IOException e) {
   122	                    // Ignore
   123	                }
   124	            }
   125	        }
   126	    }
複製代碼

這段代碼就不一一分析,整體做用如方法前的註釋所說,即「一直等待到接收到一個正確的關閉命令後該方法將會返回。這樣會使主線程一直存活——監聽http鏈接的線程池是守護線程」。oop

熟悉 Java 的 Socket 編程的話對這段代碼就很容易理解,就是默認地址(地址值由實例變量 address 定義,默認爲localhost)的默認的端口(端口值由實例變量 port 定義,默認爲8005)上監聽 Socket 鏈接,當發現監聽到的鏈接的輸入流中的內容與默認配置的值匹配(該值默認爲字符串SHUTDOWN)則跳出循環,該方法返回(第 103 到 107 行)。不然該方法會一直循環執行下去。 通常來講該用戶主線程會阻塞(第 56 行)直到有訪問localhost:8005的鏈接出現。 正由於如此纔出現開頭看見的主線程一直 Running 的狀況,而由於這個線程一直 Running ,其它守護線程也會一直存在。

說完這個線程的產生,接下來看看這個線程的關閉,按照上面的分析,這個線程提供了一個關閉機制,即只要訪問localhost:8005,而且發送一個內容爲SHUTDOWN的字符串,就能夠關閉它了。

Tomcat 正是這麼作的,通常來講關閉 Tomcat 經過執行 shutdown.bat 或 shutdown.sh 腳本,關於這段腳本可參照分析啓動腳本那篇文章,機制相似,最終會執行org.apache.catalina.startup.Bootstrap類的 main 方法,並傳入入參"stop",看下本文第 2 張圖片中org.apache.catalina.startup.Bootstrap類的第 458 行,接着將調用org.apache.catalina.startup.Catalina類 stopServer 方法:

1	    public void stopServer(String[] arguments) {
     2	
     3	        if (arguments != null) {
     4	            arguments(arguments);
     5	        }
     6	
     7	        Server s = getServer();
     8	        if( s == null ) {
     9	            // Create and execute our Digester
    10	            Digester digester = createStopDigester();
    11	            digester.setClassLoader(Thread.currentThread().getContextClassLoader());
    12	            File file = configFile();
    13	            FileInputStream fis = null;
    14	            try {
    15	                InputSource is =
    16	                    new InputSource(file.toURI().toURL().toString());
    17	                fis = new FileInputStream(file);
    18	                is.setByteStream(fis);
    19	                digester.push(this);
    20	                digester.parse(is);
    21	            } catch (Exception e) {
    22	                log.error("Catalina.stop: ", e);
    23	                System.exit(1);
    24	            } finally {
    25	                if (fis != null) {
    26	                    try {
    27	                        fis.close();
    28	                    } catch (IOException e) {
    29	                        // Ignore
    30	                    }
    31	                }
    32	            }
    33	        } else {
    34	            // Server object already present. Must be running as a service
    35	            try {
    36	                s.stop();
    37	            } catch (LifecycleException e) {
    38	                log.error("Catalina.stop: ", e);
    39	            }
    40	            return;
    41	        }
    42	
    43	        // Stop the existing server
    44	        s = getServer();
    45	        if (s.getPort()>0) {
    46	            Socket socket = null;
    47	            OutputStream stream = null;
    48	            try {
    49	                socket = new Socket(s.getAddress(), s.getPort());
    50	                stream = socket.getOutputStream();
    51	                String shutdown = s.getShutdown();
    52	                for (int i = 0; i < shutdown.length(); i++) {
    53	                    stream.write(shutdown.charAt(i));
    54	                }
    55	                stream.flush();
    56	            } catch (ConnectException ce) {
    57	                log.error(sm.getString("catalina.stopServer.connectException",
    58	                                       s.getAddress(),
    59	                                       String.valueOf(s.getPort())));
    60	                log.error("Catalina.stop: ", ce);
    61	                System.exit(1);
    62	            } catch (IOException e) {
    63	                log.error("Catalina.stop: ", e);
    64	                System.exit(1);
    65	            } finally {
    66	                if (stream != null) {
    67	                    try {
    68	                        stream.close();
    69	                    } catch (IOException e) {
    70	                        // Ignore
    71	                    }
    72	                }
    73	                if (socket != null) {
    74	                    try {
    75	                        socket.close();
    76	                    } catch (IOException e) {
    77	                        // Ignore
    78	                    }
    79	                }
    80	            }
    81	        } else {
    82	            log.error(sm.getString("catalina.stopServer"));
    83	            System.exit(1);
    84	        }
    85	    }
複製代碼

第 8 到 41 行是讀取配置文件,可參照前面分析 Digester 的文章,再也不贅述。從第 49 行開始,即向localhost:8005發起一個 Socket 鏈接,並寫入SHUTDOWN字符串。 這樣將會關閉 Tomcat 中的那惟一的一個用戶線程,接着全部守護線程將會退出(由 JVM 保證),以後整個應用關閉。

以上分析 Tomcat 的默認關閉機制,但這是經過運行腳原本關閉,我以爲這樣比較麻煩,那麼能不能經過一種在線訪問的方式關閉 Tomcat 呢?固然能夠,比較暴力的玩法是直接改org.apache.catalina.core.StandardServer的源碼第 500 行,將

boolean match = command.toString().equals(shutdown);
複製代碼

改爲

boolean match = command.toString().equals(「GET /SHUTDOWN HTTP/1.1」);  
複製代碼

或者修改 server.xml 文件,找到 Server 節點,將原來的

<Server port="8005" shutdown="SHUTDOWN">  
複製代碼

改爲

<Server port="8005" shutdown="GET /SHUTDOWN HTTP/1.1">  
複製代碼

這樣直接在瀏覽器中輸入http://localhost:8005/SHUTDOWN就能夠關閉 Tomcat 了,原理?看懂了上面的文章,這個應該不難。

相關文章
相關標籤/搜索