Tomcat 7 服务器关闭原理

在默认的配置下启动完之后会看到后台实际上总共有 6 个线程在运行。即 1 个用户线程,剩下 5 个为守护线程(下图中的 Daemon Thread )。

如果对什么叫守护线程的概念比较陌生,这里再重复一下:

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。这种线程并不属于程序中不可或缺的部分,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon(true) 方法来实现。

Tomcat 的关闭正是利用了这个原理,即只要将那唯一的一个用户线程关闭,则整个应用就关闭了。

要研究这个用户线程怎么被关闭的得先从这个线程从何产生说起。在前面分析 Tomcat 的启动时我们是从org.apache.catalina.startup.Bootstrap类的 main 方法作为入口,该类的 453 到 456 行是 Tomcat 启动时会执行的代码:

前面的文章里分析了 daemon.load 和 daemon.start 方法,这里请注意

daemon.setAwait(true);

这句,它的作用是通过反射调用 

org.apache.catalina.startup.Catalina

类的 setAwait(true) 方法,最终将 Catalina 类的实例变量 await 设值为 true 。

Catalina 类的 setAwait 方法代码:

/**
 * 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 方法,看下这个方法的代码: 

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 方法: 

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

    getServer().await();

}

该方法就一句话,意思是调用org.apache.catalina.core.StandardServer类的 await 方法: 

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连接的线程池是守护线程”。

熟悉 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 了,原理?看懂了上面的文章,这个应该不难。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值