如果我问大家Tomcat的启动有哪些方式?关闭又有哪些方式?我想大多数人都会说出很多种方式,毕竟我们都天天和Tomcat打交道。对于Windows系统,我们可以双击startup.bat、双击tomcat.exe、Tomcatw.exe以及通过服务启动。Tomcat关闭我们可以通过执行shutdown.bat、关闭黑窗口、Tomcatw.exe或者服务中都有停止的按钮。当然,针对于前两种启动我们还可以使用暴力的方式关闭黑窗口。而针对于Linux系统,我们一般使用startup.sh开启,shutdown.bat关闭。然而,不知道大家想过没有?除了通过关闭黑窗口的暴力关闭之外,其他种方式是怎么关闭Tomcat服务的?毕竟我们重新执行一个bat或者sh文件,他和tomcat服务是两个完全独立的服务?他怎么就能关闭Tomcat服务呢?好吧,带着这些问题,我们开启tomat源码的探索之旅。
要想了解这两个文件为何物?我们需要看一下里面的代码,幸运的事,他们都是明文文件,查看起来更加容易。
setlocal rem Guess CATALINA_HOME if not defined set "CURRENT_DIR=%cd%" if not "%CATALINA_HOME%" == "" goto gotHome set "CATALINA_HOME=%CURRENT_DIR%" if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome cd .. set "CATALINA_HOME=%cd%" cd "%CURRENT_DIR%" :gotHome if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome echo The CATALINA_HOME environment variable is not defined correctly echo This environment variable is needed to run this program goto end :okHome set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" rem Check that target executable exists if exist "%EXECUTABLE%" goto okExec echo Cannot find "%EXECUTABLE%" echo This file is needed to run this program goto end :okExec rem Get remaining unshifted command line arguments and save them in the set CMD_LINE_ARGS= :setArgs if ""%1""=="""" goto doneSetArgs set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 shift goto setArgs :doneSetArgs call "%EXECUTABLE%" stop %CMD_LINE_ARGS% :end有没有被这一段代码唬住?其实不用担心,经过分析,我们知道这里最重要的一段代码是倒数第二行。我们通过替换变量,最终得到的命令是%CATALINA_HOME%\bin\catalina.bat stop %1。其实这里是通过传递stop参数,调用catalina.bat文件。
针对catalina.bat这个文件,我们主要查看如下代码:
%_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%其实翻译一下,大体意思如下(忽略了一些细节:
java -classpath ... org.apache.catalina.startup.Bootstrap stop
饶了半天,原来是执行了Boostrap类的main方法,并传递了stop参数。那我们就从Bootstrap的main方法入手,看看stop参数都执行了哪些操作?不过在此之前,我们先看一下Tomcat启动中Catalina启动最后都做了哪些事?
根据追踪路径org.apache.catalina.startup.Catalina#start->org.apache.catalina.startup.Catalina#await->org.apache.catalina. core.StandardServer#await,我们很快来到了StandardServer的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.等待直到收到正确的关闭命令,然后返回。这样可以使主线程保持活动状态-监听HTTP连接的线程池是守护程序线程。通过字面意思我们知道他应该是启动一个线程用于监听一个关闭事件,当接收到关闭命令的时候开始往下执行代码,最终结束Tomcat服务。现在我们看一下这个代码片段:
@Override public void await() { ... try { awaitSocket = new ServerSocket(port, 1, InetAddress.getByName(address)); } catch (IOException 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(); ... }这里我只截取了部分代码,其重要的代码为:
awaitSocket = new ServerSocket(port, 1, InetAddress.getByName(address));
ServerSocket serverSocket = awaitSocket;
socket = serverSocket.accept();
在上述代码片段中,第一行主要创建一个ServerSocket服务,端口号为8005,address为localhost,当然也可以通过配置文件配置。在上一讲中我给大家展示过server的配置文件,其中配置server的时候有两个属性port和shutdown,其中port便为此端口,如果这里没有配置,则采用默认的端口8005。address是通过初始值进行赋值。
<Server port="8005" shutdown="SHUTDOWN"> ... </Server>第二步便是将成员变量赋值给局部变量。
第三步为开启端口监听。当执行到这段代码,当前线程处于阻塞状态,等待客户端发送信号内容。
这里有个小问题:代码中为什么采用while(!stopAwait){}循环?
看到上面的 英文注释,想必大家也明白了。
源码看到这里,我们知道在Tomcat启动的时候,会开启一个守护线程,此线程主要监听8005端口,专门用于监听关闭事件。
根据上面的分析,我们知道是通过Bootstrap的main的stop参数实现Tomcat服务关闭的。接下来我们看看他是如何真正关闭Tomcat服务的。
我们根据Bootstrap的main的stop参数,一路跟随到org.apache.catalina.startup.Catalina#stopServer(java.lang.String[])。在这个方法中,我们主要做了如下几件事:
1)通过createStopDigester创建Digester对象。
protected Digester createStopDigester() { Digester digester = new Digester(); digester.setUseContextClassLoader(true); digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className"); digester.addSetProperties("Server"); digester.addSetNext("Server", "setServer", "org.apache.catalina.Server"); return digester; }这段代码是不是似曾相识,却有些陌生呢?没错,他是Tomcat启动中解析server.xml的处理器。不过相对于启动的Digester,这里只解析了Server的元素级属性信息,即创建Server对象,并赋值port属性和shutdown属性。
2)发送Socket请求
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) { } catch (IOException e) { }不知道大家看到这段代码是否迷糊?这里我先解释一下try-catch的一个不算新但是容易被忽略的语法:
try-catch有一种语法为如下代码:
try(创建资源对象){ ... }catch(Exception e){}也就是说try中可以加入代码块,他的作用域与try的大括号作用域相同。针对于创建资源对象中,可以有多行代码,按照java语法习惯必须用“;”结束,在这里定义的成员变量,如果实现了java.io.Closeable接口,在try-catch代码块执行完毕之后,会自动将其执行close方法关闭,免去了在finally中各种判空或者捕获异常进行资源的关闭。另外,这样可以在一定程度上避免漏掉关闭资源。
在这里,我们通过socket将SHUTDOWN命令"SHUTDOWN"字符串发送给localhost:8005的服务端。到此为止,客户端完成了关闭tomcat服务器的使命。当前进程也将自动关闭。
接线来,我们继续分析第三章节剩下的代码片段。
当服务端接收到请求连接之后,执行了socket.setSoTimeout(10 * 1000);。这段代码是从建立连接到发送数据的超时时间,这里也算上一种防止攻击的一种手段。什么意思呢?socket拒绝占坑不拉屎的行为。
代码的逻辑很简单,我就不说了。然而他是做什么用的呢?源码中给予的解释是Cut off to avoid DoS attack(切断以避免DoS攻击),怎么就可以防止Dos攻击了呢?我们带着问题往下看。
这段代码主要实现了将socket输入流转换为command字符串。在这里,我们需要注意两点:
1)ch < 32 || ch == 127,在ASCII的世界里,小于32为控制字符,等于127为结束标识符。所以遇到这两种类型的字符,提前终止输入流的读取。
2)当expected大于输入流的长度,会以输入流的长度作为command字符串(遇到控制字符或者结束标识符除外);当expected小于输入流的长度,此时会截取前expected长度的输入流。例如,expected为4,输入流为abcdefg,那么最终获取的字符串为abcd.
看到这里,是不是对于上面提到的Dos攻击有自己想法了?此时,如果没有上面的代码,客户端请求发送的命令长度为几十兆、几百兆,甚至好几十G,此时服务端也会全盘接受。轻点导致服务器性能下降,严重的有可能会是服务器由于内容溢出而宕机,想想后果是不是很严重。也许会有人说谁会这么无聊?答案就是黑客或者你的竞争对手。
这里处理的也很巧妙,如果socket流为"SHUTDOWN"字符串,结束当前while()循环,执行finally代码块,否则,继续执行while()循环,后果是又会执行到socket = serverSocket.accept();等待下一次关闭命令。
这段代码主要将成员变量置空,同时关闭serversocket链接,最终结束当前方法。由于此方法是守护线程,此线程一旦结束,其他线程也相继over。
到此,Tomcat服务算是彻底被关闭了。