通過前面文章的學習,我們已經了解了連接器,四大容器是如何配合工作的,在源碼中提供的示例也都是“一個連接器”+“一個頂層容器”的結構。并且啟動方式是分別啟動連接器和容器,類似下面代碼
connector.setContainer(engine);
try {connector.initialize();((Lifecycle) connector).start();((Lifecycle) engine).start();// make the application wait until we press a key.System.in.read();((Lifecycle) engine).stop();
} catch (Exception e) {e.printStackTrace();
}
連接器要運行起來需要執行兩個方法 initialize() 與 start(),容器要運行起來只需執行一個方法 start()。
之前的設計存在兩個問題:
1.Tomcat中不應該僅有支持HTTP協議的連接器,還應該有支持HTTPS等協議的連接器,所以連接器有多種類型。多個連接器可以關聯同一個容器,不同的連接器將請求統一處理成容器需要的同一種對象即可,這種多對一的結構該如何設計?
2.之前章節的程序架構中,缺少一種關閉Tomcat的機制,僅僅是通過?System.in.read(); 來阻塞程序運行,還需要手動在控制臺輸入東西才能走關閉流程。
Tomcat設計了兩個組件來解決上面兩個問題:服務器組件(Server),服務組件(Service)。
Service組件
先看第一個問題,Tomcat提供了Service組件來將連接器和容器包裝起來,并向外提供 initialize() 與 start() 兩個方法。他們之間的關系如下圖所示
Service組件的標準實現類為StandardService,在StandardService的?initialize()方法中,調用了所有連接器的??initialize() 方法,代碼如下
// 連接器數組
private Connector[] connectors = new Connector[0];public void initialize() throws LifecycleException {if (initialized) throw new LifecycleException(sm.getString("standardService.initialize.initialized"));initialized = true;// Initialize our defined Connectorssynchronized (connectors) {for (Connector connector : connectors) {connector.initialize();}}
}
StandardService的 start()方法中,調用了容器的start() 方法和所有連接器的 start() 方法,代碼如下
private Connector[] connectors = new Connector[0];
private Container container = null;public void start() throws LifecycleException {// Validate and update our current component stateif (started) {throw new LifecycleException(sm.getString("standardService.start.started"));}// 通知事件監聽器lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);lifecycle.fireLifecycleEvent(START_EVENT, null);started = true;// 先啟動容器if (container != null) {synchronized (container) {if (container instanceof Lifecycle) {((Lifecycle) container).start();}}}// 再啟動連接器synchronized (connectors) {for (Connector connector : connectors) {if (connector instanceof Lifecycle) {((Lifecycle) connector).start();}}}// 通知事件監聽器lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);}
所以呢,Service組件的作用就是將連接器與容器的?initialize() 與 start() 兩個方法的入口收束了一下。
Server組件
Tomcat中是支持多個Service組件實例的,如果存在多個Service組件實例的話,那么他們的?initialize() 與 start() 兩個方法就又散開了,又要各啟動各的,為了解決這個問題,Tomcat引入Server組件,將Service組件的?initialize() 與 start() 兩個方法 再次收束一下,他們的結構如下圖所示
Server組件的標準實現類為StandardServer,StandardServer的 initialize() 方法調用了所有Service組件的?initialize() 方法
private Service[] services = new Service[0];public void initialize() throws LifecycleException {if (initialized) throw new LifecycleException(sm.getString("standardServer.initialize.initialized"));initialized = true;// 初始化所有Service組件for (int i = 0; i < services.length; i++) {services[i].initialize();}
}
StandardServer的 start() 方法調用了所有Service組件的?start() 方法
private Service[] services = new Service[0];public void start() throws LifecycleException {// 防止重復指定start() 方法if (started) {throw new LifecycleException(sm.getString("standardServer.start.started"));}// 通知事件監聽器lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);lifecycle.fireLifecycleEvent(START_EVENT, null);started = true;// 啟動所有Servicessynchronized (services) {for (Service service : services) {if (service instanceof Lifecycle) {((Lifecycle) service).start();}}}// 通知事件監聽器lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);}
Server組件就是Tomcat的頂層組件了,上面提到的initialize() 和 start() 方法是啟動Tomcat需要的兩個方法,這兩個方法執行完后,整個Tomcat服務就可以開始工作了。
什么情況下會用到多個Service組件實例呢?網上搜的內容看的云里霧里,總結起來就是:我們平時候開發基本不會用到多Service實例,所以這塊的內容可以不求甚解😂。
接下來是如何關閉Tomcat服務
先來看關閉Tomcat服務需要調用的方法:StandardServer#stop(), 該方法通過調用所有Service組件的 stop() 方法進而層層調用各個組件和容器的 stop() 方法,將Tomcat服務正常關閉掉。
public void stop() throws LifecycleException {// Validate and update our current component stateif (!started) {throw new LifecycleException(sm.getString("standardServer.stop.notStarted"));}// 通知事件監聽器lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);lifecycle.fireLifecycleEvent(STOP_EVENT, null);started = false;// 停止所有Service組件for (Service service : services) {if (service instanceof Lifecycle) {((Lifecycle) service).stop();}}// 通知事件監聽器lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);}
如何觸發stop() 方法的執行呢?
在之前的啟動類中,start() 方法后會緊跟著 【System.in.read();】 來阻塞啟動線程,并且 【System.in.read(); 】后緊跟著 stop() 方法。這個邏輯編排是沒有問題的,主要就是這個 【System.in.read(); 】不像是個正常操作。StandardServer中提供了await() 方法來替代這個阻塞操作。
StandardServer的 await() 方法大致邏輯是這樣:這個方法會創建一個ServerSocket,阻塞監聽某個端口(默認8005),如果該ServerSocket收到Socket連接,并且接收到的消息是提前定義好的“關閉Tomcat”(shutdown)的指令時,該方法會結束阻塞并返回,否則會繼續阻塞等待下一個請求。
也就是說 await() 通過一個TCP消息來達到結束阻塞的目的,比之前通過控制臺輸入字符來結束阻塞 高大上了很多。
await方法的代碼如下
// 關閉Tomcat的指令
private String shutdown = "SHUTDOWN";public void await() {// 創建一個server socket去阻塞等待ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));} catch (IOException e) {System.err.println("StandardServer.await: create[" + port + "]: " + e);e.printStackTrace();System.exit(1);}// 循環等待鏈接并驗證是不是shutdown命令while (true) {// 等待下一個鏈接Socket socket = null;InputStream stream = null;try {socket = serverSocket.accept();socket.setSoTimeout(10 * 1000); // Ten secondsstream = socket.getInputStream();} catch (AccessControlException ace) {System.err.println("StandardServer.accept security exception: " + ace.getMessage());continue;} catch (IOException e) {System.err.println("StandardServer.await: accept: " + e);e.printStackTrace();System.exit(1);}// Read a set of characters from the socketStringBuffer command = new StringBuffer();int expected = 1024; // Cut off to avoid DoS attackwhile (expected < shutdown.length()) {if (random == null) {random = new Random(System.currentTimeMillis());}expected += (random.nextInt() % 1024);}while (expected > 0) {int ch = -1;try {ch = stream.read();} catch (IOException e) {System.err.println("StandardServer.await: read: " + e);e.printStackTrace();ch = -1;}if (ch < 32) // Control character or EOF terminates loopbreak;command.append((char) ch);expected--;}// 關閉該socket連接try {socket.close();} catch (IOException e) {;}// 判斷收到的指令是不是shutdown指令,如果是則結束監聽,否則繼續阻塞監聽boolean match = command.toString().equals(shutdown);if (match) {break;} else {System.err.println("StandardServer.await: Invalid command '" + command.toString() + "' received");}} // while end// 收到了shutdown命令,關閉 server socket 并返回try {serverSocket.close();} catch (IOException e) {;}}
await() 方法只是實現了一個阻塞邏輯,并提供了一個結束阻塞的方法。那么接下來只要將 await() 方法放在 start() 和 stop() 兩個方法的中間即可。start() 方法執行后,Tomcat啟動成功;接著await() 方法執行,啟動線程進入阻塞狀態;等到 await() 方法中的server socket收到關閉指令后,await() 方法結束阻塞并返回;接著就執行到stop() 方法,Tomcat就能正常關閉掉了。
這幾個方法的邏輯編排如下圖所示
下面是本章內容的啟動類Bootstrap,與發送關閉Tomcat命令的工具類Stopper。
package ex14.pyrmont.startup;import ex14.pyrmont.core.SimpleContextConfig;
import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.loader.WebappLoader;public final class Bootstrap {public static void main(String[] args) {System.setProperty("catalina.base", System.getProperty("user.dir"));Connector connector = new HttpConnector();Wrapper wrapper1 = new StandardWrapper();wrapper1.setName("Primitive");wrapper1.setServletClass("PrimitiveServlet");Wrapper wrapper2 = new StandardWrapper();wrapper2.setName("Modern");wrapper2.setServletClass("ModernServlet");Context context = new StandardContext();// StandardContext's start method adds a default mappercontext.setPath("/app1");context.setDocBase("app1");context.addChild(wrapper1);context.addChild(wrapper2);LifecycleListener listener = new SimpleContextConfig();((Lifecycle) context).addLifecycleListener(listener);Host host = new StandardHost();host.addChild(context);host.setName("localhost");host.setAppBase("webapps");Loader loader = new WebappLoader();context.setLoader(loader);// context.addServletMapping(pattern, name);context.addServletMapping("/Primitive", "Primitive");context.addServletMapping("/Modern", "Modern");Engine engine = new StandardEngine();engine.addChild(host);engine.setDefaultHost("localhost");Service service = new StandardService();service.setName("Stand-alone Service");Server server = new StandardServer();server.addService(service);service.addConnector(connector);//StandardService class's setContainer will call all its connector's setContainer methodservice.setContainer(engine);// Start the new serverif (server instanceof Lifecycle) {try {server.initialize();((Lifecycle) server).start();server.await();// the program waits until the await method returns,// i.e. until a shutdown command is received.}catch (LifecycleException e) {e.printStackTrace(System.out);}}// Shut down the serverif (server instanceof Lifecycle) {try {((Lifecycle) server).stop();}catch (LifecycleException e) {e.printStackTrace(System.out);}}}
}
package ex14.pyrmont.startup;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;public class Stopper {public static void main(String[] args) {// the following code is taken from the Stop method of// the org.apache.catalina.startup.Catalina classint port = 8005;try {Socket socket = new Socket("127.0.0.1", port);OutputStream stream = socket.getOutputStream();String shutdown = "SHUTDOWN";for (int i = 0; i < shutdown.length(); i++)stream.write(shutdown.charAt(i));stream.flush();stream.close();socket.close();System.out.println("The server was successfully shut down.");} catch (IOException e) {System.out.println("Error. The server has not been started.");}}
}
OK,這一章的內容就到這里。本章主要講解了Server與Service兩個組件,Server組件是Tomcat的頂層組件,它提供了啟停Tomcat的方法。下一章來看一個更萬無一失的關閉Tomcat的方案:關閉鉤子(ShutdownHook)。