【0】README
0.1)本文部分文字描述转自 “how tomcat works”,旨在学习 “tomcat(14)服务器组件和服务组件” 的基础知识;
0.2)for complete source code ,please visit https://github.com/pacosonTang/HowTomcatWorks/tree/master/chapter14
【1】服务器组件(org.apache.catalina.Server)
1)org.apache.catalina.Server接口的实例表示 Catalina的整个servlet引擎,囊括了所有的组件。服务器组件使用了一种优雅的方法来启动/关闭 整个系统,不需要再对连接器和容器分别启动/ 关闭;
2)启动/关闭机制的工作原理:当启动服务器组件时,它会启动其中所有的组件,然后它就无限期地等待关闭命令。如果想要关闭系统,可以向指定端口发送一条关闭命令。服务器组件接收到关闭命令后,就会关闭其中所有的组件;
3)服务组件:服务器组件引入了服务组件来包含其他组件,如一个容器组件和一个或多个连接器组件;(干货——注意区分服务器组件和服务组件,以下代码摘自某测试用例,StandardService表服务组件,而StandardServer表服务器组件)
Service service = new StandardService(); service.setName("Stand-alone Service"); Server server = new StandardServer(); server.addService(service);
4)服务器组件Server接口定义
package org.apache.catalina; import org.apache.catalina.deploy.NamingResources; public interface Server { public String getInfo(); public NamingResources getGlobalNamingResources(); public void setGlobalNamingResources (NamingResources globalNamingResources); public int getPort(); public void setPort(int port); public String getShutdown(); public void setShutdown(String shutdown); public void addService(Service service); public void await(); public Service findService(String name); public Service[] findServices(); public void removeService(Service service); public void initialize() throws LifecycleException; }
对以上代码的分析(Analysis):
A1)shutdown属性:保存了必须发送给Server 实例用来关闭整个系统的关闭命令;
A2)port属性:定义了服务器组件会从哪个端口获取关闭命令;
A3)addService() 和 removeService()方法:为服务器组件添加或移除服务组件;
A4)findService()方法:返回添加到该服务器组件中的所有服务组件;
A5)initialize()方法:包含在系统启动前要执行的一些代码;
【2】StandardServer类
1)intro:一个服务器组件可以有0个或多个服务组件;(干货——一个服务器组件可以有0个或多个服务组件)
2)StandardServer类有4个与生命周期相关的方法,分别是initialize()方法,start()方法,stop()方法 和 await()方法;
【2.1】initialize()方法
1)intro:用于初始化添加到其中的服务器组件;
public void initialize() throws LifecycleException { //org.apache.catalina.StandardServer.initialize(). if (initialized) throw new LifecycleException ( sm.getString("standardServer.initialize.initialized")); initialized = true; // Initialize our defined Services for (int i = 0; i < services.length; i++) { services[i].initialize(); } }
【2.2】start() 方法
1)intro:该方法用于启动服务器组件,即启动所有的服务组件,逐个启动所有组件,如连接器组件和servlet容器;
public void start() throws LifecycleException { //org.apache.catalina.StandardServer.start(). // Validate and update our current component state if (started) throw new LifecycleException (sm.getString("standardServer.start.started")); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; // Start our defined Services synchronized (services) { for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).start(); } } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }
【2.3】stop()方法
1)intro:用于关闭服务器组件;(调用stop方法会关闭所有的服务组件,并reset started,这样,才能再次启动服务器组件;参见start()方法)
public void stop() throws LifecycleException { //org.apache.catalina.StandardServer.stop(). // Validate and update our current component state if (!started) throw new LifecycleException (sm.getString("standardServer.stop.notStarted")); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null); lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; // Stop our defined Services for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).stop(); } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null); }
【2.4】await()方法
1)intro:该方法负责等待关闭整个tomcat 部署的命令;
2)await()方法:调用该方法后,会一直阻塞,直到它从 8085 端口(也可以是其他端口)上接收到关闭命令(accept()方法);当await()方法返回时,会运行stop方法来关闭其下的所有子组件;
public void await() { //org.apache.catalina.StandardServer.await(). // Set up a server socket to wait on 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); } // Loop waiting for a connection and a valid command while (true) { // Wait for the next connection Socket socket = null; InputStream stream = null; try { socket = serverSocket.accept(); //highlight line. socket.setSoTimeout(10 * 1000); // Ten seconds stream = 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 socket StringBuffer command = new StringBuffer(); int expected = 1024; // Cut off to avoid DoS attack while (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 loop break; command.append((char) ch); expected--; } // Close the socket now that we are done with it try { socket.close(); } catch (IOException e) { ; } // Match against our command string boolean match = command.toString().equals(shutdown); //highlight line. if (match) { break; } else System.err.println("StandardServer.await: Invalid command '" + command.toString() + "' received"); } // Close the server socket and return try { serverSocket.close(); } catch (IOException e) { ; } }
对以上代码的调用过程分析(steps):
step1)创建一个ServerSocket对象,监听8085端口,并在while循环中调用它的accept()方法;
step2)当在指定端口上接收到消息时,accept()方法才会返回;
step3)将接收到的消息与关闭命令的字符串相比较,相同的话就跳出while循环,关闭 ServerSocket,否则会再次循环,继续等待消息;
【3】Service接口(服务组件接口——org.apache.catalina.core.Service)
1)intro:一个服务组件可以有一个servlet容器 和 多个连接器实例;(干货——一个服务器组件可以有0个或多个服务组件,而一个服务组件可以有一个servlet容器
和 多个连接器实例)
2)可以自由地把连接器添加到服务组件中,所有的连接器都会与这个servlet容器相关联;
package org.apache.catalina; public interface Service { public Container getContainer(); public void setContainer(Container container); public String getInfo(); public String getName(); public void setName(String name); public Server getServer(); public void setServer(Server server); public void addConnector(Connector connector); public Connector[] findConnectors(); public void removeConnector(Connector connector); public void initialize() throws LifecycleException; }
【4】StandardService类(org.apache.catalina.core.StandardService)
1)StandardService.initialize()方法:用于初始化添加到其中的所有连接器;
2)StandardService.start()方法:启动连接器和所有servlet容器;
【4.1】connectors 和 container
1)StandardService实例中有两种组件:连接器和servlet容器;其中servlet容器只有一个,而连接器则可以有多个;(干货——StandardService实例中有两种组件:连接器和servlet容器)
2)多个连接器使tomcat 可以为多种不同的请求协议提供服务;如,一个连接器处理HTTP请求,而另一个连接器处理HTTPS请求;(干货——多个连接器使tomcat
可以为多种不同的请求协议提供服务)
3)变量connectors 和 container:StandardService类使用 container 变量来指向一个Container接口的实例,而数组connectors 来保存所有连接器的引用;(干货——变量connectors
和 container的用途)
private Connector connectors[] = new Connector[0]; // defined in StandardService.
4)setContainer()方法:需要调用setContainer()方法 将servlet容器与服务组件相关联;
public void setContainer(Container container) { // org.apache.catalina.core.StandardService.setContainer(). Container oldContainer = this.container; if ((oldContainer != null) && (oldContainer instanceof Engine)) ((Engine) oldContainer).setService(null); this.container = container; if ((this.container != null) && (this.container instanceof Engine)) ((Engine) this.container).setService(this); if (started && (this.container != null) && (this.container instanceof Lifecycle)) { try { ((Lifecycle) this.container).start(); } catch (LifecycleException e) { ; } } synchronized (connectors) { for (int i = 0; i < connectors.length; i++) connectors[i].setContainer(this.container); } if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) { try { ((Lifecycle) oldContainer).stop(); } catch (LifecycleException e) { ; } } // Report this property change to interested listeners support.firePropertyChange("container", oldContainer, this.container); }
对以上代码的补充描述(description):与服务组件相关联的servlet容器的实例将被传给每个连接器对象的 setContainer()方法,这样在服务组件中就可以形成每个连接器和 servlet容器间的关联关系;
5)addConnector()方法和removeConnector()方法:前者将连接器添加到服务组件中, 后者将某个连接器移除;
(补充:addConnector()方法会初始化并启动添加到其中的连接器,参见下面的start()方法——highlight line.)
public void addConnector(Connector connector) { //org.apache.catalina.core.StandardService.addConnector(). synchronized (connectors) { connector.setContainer(this.container); connector.setService(this); Connector results[] = new Connector[connectors.length + 1]; System.arraycopy(connectors, 0, results, 0, connectors.length); results[connectors.length] = connector; connectors = results; if (initialized) { try { connector.initialize(); //highlight line. } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (connector instanceof Lifecycle)) { try { ((Lifecycle) connector).start();//highlight line. } catch (LifecycleException e) { ; } } // Report this property change to interested listeners support.firePropertyChange("connector", null, connector); } } public void removeConnector(Connector connector) { //org.apache.catalina.core.StandardService.removeConnector(). synchronized (connectors) { int j = -1; for (int i = 0; i < connectors.length; i++) { if (connector == connectors[i]) { j = i; break; } } if (j < 0) return; if (started && (connectors[j] instanceof Lifecycle)) { try { ((Lifecycle) connectors[j]).stop(); } catch (LifecycleException e) { ; } } connectors[j].setContainer(null); connector.setService(null); int k = 0; Connector results[] = new Connector[connectors.length - 1]; for (int i = 0; i < connectors.length; i++) { if (i != j) results[k++] = connectors[i]; } connectors = results; // Report this property change to interested listeners support.firePropertyChange("connector", connector, null); } }
【4.2】与生命周期有关的方法(start() + stop() + initialize())
1)intro: 与生命周期有关的方法包括Lifecycle.start() + Lifecycle.stop()+ initialize()方法;其中initialize()方法会调用该服务组件中所有连接器的 initialize()方法;
2)org.apache.catalina.core.StandardService.initialize()源码如下:
public void initialize() throws LifecycleException { //org.apache.catalina.core.StandardService.initialize(). if (initialized) throw new LifecycleException ( sm.getString("standardService.initialize.initialized")); initialized = true; // Initialize our defined Connectors synchronized (connectors) { for (int i = 0; i < connectors.length; i++) { connectors[i].initialize(); //highlight line. } } } public void initialize() throws LifecycleException { //org.apache.catalina.connector.HttpConnector.initialize(). if (initialized) throw new LifecycleException ( sm.getString("httpConnector.alreadyInitialized")); this.initialized=true; Exception eRethrow = null; // Establish a server socket on the specified port try { serverSocket = open(); } catch (IOException ioe) { log("httpConnector, io problem: ", ioe); eRethrow = ioe; } catch (KeyStoreException kse) { log("httpConnector, keystore problem: ", kse); eRethrow = kse; } catch (NoSuchAlgorithmException nsae) { log("httpConnector, keystore algorithm problem: ", nsae); eRethrow = nsae; } catch (CertificateException ce) { log("httpConnector, certificate problem: ", ce); eRethrow = ce; } catch (UnrecoverableKeyException uke) { log("httpConnector, unrecoverable key: ", uke); eRethrow = uke; } catch (KeyManagementException kme) { log("httpConnector, key management problem: ", kme); eRethrow = kme; } if ( eRethrow != null ) throw new LifecycleException(threadName + ".open", eRethrow); }
3)org.apache.catalina.core.StandardService.start()源码如下:该方法负责启动被添加到 该服务组件中的连接器和servlet容器;
public void start() throws LifecycleException { // org.apache.catalina.core.StandardService.start() // Validate and update our current component state if (started) { throw new LifecycleException (sm.getString("standardService.start.started")); } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); System.out.println (sm.getString("standardService.start.name", this.name)); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; // Start our defined Container first if (container != null) { synchronized (container) { if (container instanceof Lifecycle) { ((Lifecycle) container).start(); //highlight line. } } } // Start our defined Connectors second synchronized (connectors) { for (int i = 0; i < connectors.length; i++) { if (connectors[i] instanceof Lifecycle) ((Lifecycle) connectors[i]).start(); //highlight line. } } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }
4)org.apache.catalina.core.StandardService.stop()源码如下:用于关闭与该服务组件相关联的servlet容器和所有连接器;
public void stop() throws LifecycleException { // org.apache.catalina.core.StandardService.stop() // Validate and update our current component state if (!started) { throw new LifecycleException (sm.getString("standardService.stop.notStarted")); } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null); lifecycle.fireLifecycleEvent(STOP_EVENT, null); System.out.println (sm.getString("standardService.stop.name", this.name)); started = false; // Stop our defined Connectors first synchronized (connectors) { for (int i = 0; i < connectors.length; i++) { if (connectors[i] instanceof Lifecycle) ((Lifecycle) connectors[i]).stop();//highlight line. } } // Stop our defined Container second if (container != null) { synchronized (container) { if (container instanceof Lifecycle) { ((Lifecycle) container).stop(); //highlight line. } } } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null); }
【5】应用程序
1)intro:本应用程序旨在说明如何使用服务器组件和服务组件,特别是在 StandardServer类中如何利用 启动和关闭机制;
2)共有3个类:Bootstrap类+Stopper类+SimpleContextConfig类(它其实是作为 LifecycleListener的实例)
2.1)Bootstrap类源码
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 mapper context.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 method service.setContainer(engine); // Start the new server if (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 server if (server instanceof Lifecycle) { try { ((Lifecycle) server).stop(); } catch (LifecycleException e) { e.printStackTrace(System.out); } } } }
对以上代码的调用过程分析(steps):
step1)该方法会创建一个连接器,两个Wrapper实例,一个Context容器,一个Host容器和一个Engine容器;
step2)将两个Wrapper 实例添加到 Context容器中, 将Context容器添加到 Host容器中,再将Host容器添加到Engine容器中;(干货——到此,tomcat的4种容器创建完毕)
step3)但是,这并没有将连接器和最顶层的容器Engine相关联;
step4)相反,main()方法创建了一个Service对象,设置其名称,再创建一个 Server对象,将Service实例添加到 Server实例中:
Service service = new StandardService(); service.setName("Stand-alone Service"); Server server = new StandardServer(); server.addService(service);step5)main()方法将连接器和Engine容器添加到 Service实例中;
service.addConnector(connector); service.setContainer(engine);step6)main()方法调用 Server.intialize() 和 Server.start()方法,初始化连接器,并启动连接器和servlet容器;
step7)接下来,main()方法调用Server.await()方法,进入循环等待,监听8085端口等待关闭命令;(干货——参见上述【2.4】await()方法)
Attention)此时连接器已经处于运行状态,等待另一个端口8080上的 http 请求;(监听8085和8080端口分别处于不同的线程)
// Start the new server if (server instanceof Lifecycle) { try { server.initialize(); // step6 ((Lifecycle) server).start(); // step6 server.await(); // step7 // the program waits until the await method returns, // i.e. until a shutdown command is received. } catch (LifecycleException e) { e.printStackTrace(System.out); } }step8)除非收到了正确的关闭命令,否则await()方法是不会返回的。当 await()方法返回时,main()方法调用 Server.stop()方法,实际上关闭其所有组件;
// Shut down the server if (server instanceof Lifecycle) { try { ((Lifecycle) server).stop(); } catch (LifecycleException e) { e.printStackTrace(System.out); } }
2.2)Stopper类 源码
2.2.1)intro:Stopper类提供了更加优雅的方式来关闭Catalina服务器,也保证了所有的生命周期组件中的stop() 方法都能够调用;
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 class int 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."); } } }
对以上代码的分析(Analysis): Stopper类的main()方法会创建一个 Socket对象,然后将正确 的关闭命令 “SHOTDOWN”字符串发送到 端口8085。Catalina 服务器在接收到关闭命令后,就会执行相应的关闭操作;
Attention)本文还是习惯性地总结了上述测试用例的调用流程图
A1)HttpConnector.start()方法的调用过程如下(下面的图借用自 tomcat(11) 中章节【1】的4.1小节的图):
A2)StandardsEngine.start()方法的调用过程如下(下面的第二张图借用自tomcat(13)中的章节【10】 ,两张图可以结合起来看):
(补充,需要了解 HttpConnector.initialize()
和 start() 方法 以及 StandardContext.start()方法的具体调用过程,参见 tomcat(supplement)HttpConnector.initialize() 和 start() 方法 以及 StandardContext.start()方法的分析)
【6】执行结果
1)console info
E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src>java -cp .;lib/servlet.jar;lib/catalina_4_1_24.jar;lib/catalina-5.5.4.jar;lib/naming-common. jar;lib/commons-collections.jar;lib/naming-resources.jar;lib/commons-digester.jar;lib/catalina.jar;E:\bench-cluster\cloud-data-preprocess\HowTomcatWok s\weroot com.tomcat.chapter14.startup.Bootstrap HttpConnector Opening server socket on all host IP addresses Starting service Stand-alone Service Apache Tomcat/4.1.24 WebappLoader[/app1]: Deploying class repositories to work directory E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src\work\_\localhost\app1 WebappLoader[/app1]: Deploy class files /WEB-INF/classes to E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src\webapps\app1\WEB-INF\classes StandardManager[/app1]: Seeding random number generator class java.security.SecureRandom StandardManager[/app1]: Seeding of random number generator has been completed StandardManager[/app1]: IOException while loading persisted sessions: java.io.EOFException java.io.EOFException at java.io.ObjectInputStream$PeekInputStream.readFully(Unknown Source) at java.io.ObjectInputStream$BlockDataInputStream.readShort(Unknown Source) at java.io.ObjectInputStream.readStreamHeader(Unknown Source) at java.io.ObjectInputStream.<init>(Unknown Source) at org.apache.catalina.util.CustomObjectInputStream.<init>(CustomObjectInputStream.java:103) at org.apache.catalina.session.StandardManager.load(StandardManager.java:408) at org.apache.catalina.session.StandardManager.start(StandardManager.java:655) at org.apache.catalina.core.StandardContext.start(StandardContext.java:3570) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardHost.start(StandardHost.java:738) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:347) at org.apache.catalina.core.StandardService.start(StandardService.java:497) at org.apache.catalina.core.StandardServer.start(StandardServer.java:2190) at com.tomcat.chapter14.startup.Bootstrap.main(Bootstrap.java:77) StandardManager[/app1]: Exception loading sessions from persistent storage java.io.EOFException at java.io.ObjectInputStream$PeekInputStream.readFully(Unknown Source) at java.io.ObjectInputStream$BlockDataInputStream.readShort(Unknown Source) at java.io.ObjectInputStream.readStreamHeader(Unknown Source) at java.io.ObjectInputStream.<init>(Unknown Source) at org.apache.catalina.util.CustomObjectInputStream.<init>(CustomObjectInputStream.java:103) at org.apache.catalina.session.StandardManager.load(StandardManager.java:408) at org.apache.catalina.session.StandardManager.start(StandardManager.java:655) at org.apache.catalina.core.StandardContext.start(StandardContext.java:3570) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardHost.start(StandardHost.java:738) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:347) at org.apache.catalina.core.StandardService.start(StandardService.java:497) at org.apache.catalina.core.StandardServer.start(StandardServer.java:2190) at com.tomcat.chapter14.startup.Bootstrap.main(Bootstrap.java:77) HttpConnector[8080] Starting background thread ModernServlet -- init StandardHost[localhost]: MAPPING configuration error for request URI /favicon.ico
2)执行效果