专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Tomcat 7 服务器关闭原理

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

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

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

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

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

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

63_2.png前面的文章里分析了 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 了,原理?看懂了上面的文章,这个应该不难。

文章永久链接:https://tech.souyunku.com/35911

未经允许不得转载:搜云库技术团队 » Tomcat 7 服务器关闭原理

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们