浅读tomcat架构设计和tomcat启动过程(1)
一图甚千言,这张图真的是耽搁我太多时间了:
下面的tomcat架构设计代码分析,和这张图息息相关.
使用maven搭建本次的环境,贴出pom.xml完整内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>groupId</groupId>
<artifactId>JavaWebDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency> <dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.0.14</version>
</dependency>
</dependencies> <properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties> </project>
至此,环境已经准备就绪,就可以愉快看代码了.
tomcat的Server是由org.apache.catalina.startup.Catalina来管理的,Catalina是tomcat的管理类可以通过反射加载查看代码:
Catalina类中有很多方法,他们具有不同的含义,其中
public void load() {...
public void start() {...
public void stop() {...
这些方法用于管理tomcat的生命周期,其中load方法:
load方法重要前半部分:
public void load() {
long t1 = System.nanoTime();
this.initDirs();
this.initNaming();
Digester digester = this.createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
File file = null; try {
file = this.configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
load方法重要后半部分:
this.getServer().setCatalina(this);
this.getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
this.getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
this.initStreams(); try {
this.getServer().init();
其中load方法是根据创建conf/server.xml文件来创建Server,并调用Server的init方法进行初始化
start和stop方法暂定,下面会讲到.这三个方法都会按照容器结构逐层调用相应的方法.
不过tomcat的入口main方法并不在Catalina类里,而是在org.apache.catalina.startup.Bootstrap类中,这样做的好处是tomcat管理类和入口类实现分离
Bootstrap是tomcat的入口,正常情况下启动tomcat就是调用Bootstrap类的main方法,代码如下:
public static void main(String[] args) {
if (daemon == null) {
//新建一个bootstrap
Bootstrap bootstrap = new Bootstrap(); try {
//初始化
bootstrap.init();
} catch (Throwable var3) {
handleThrowable(var3);
var3.printStackTrace();
return;
}
//赋值给daemon
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
} try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
} if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
} System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable var4) {
Throwable t = var4;
if (var4 instanceof InvocationTargetException && var4.getCause() != null) {
t = var4.getCause();
} handleThrowable(t);
t.printStackTrace();
System.exit(1);
} }
Bootstrap类中的main方法只干两件事情,(1):新建一个bootstrap,并执行init方法初始化,初始化后赋值给daemon,然后处理main方法传入进来的命令,来判断执行对应的方法,比如传入start,执行start方法,如果传入错误的命令,直接告警command不存在.
在main方法中,daemon调用了好几个方法,当main方法传入的命令是start的时候,会自动调用setAwait(true),load()和start()方法:
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
跟进start方法:
org.apache.catalina.startup.Bootstrap#182:
public void start() throws Exception {
if (this.catalinaDaemon == null) {
this.init();
} Method method = this.catalinaDaemon.getClass().getMethod("start", (Class[])null);
method.invoke(this.catalinaDaemon, (Object[])null);
}
首先会判断你的catalinaDaemon是否为空,不为空,再用反射调用start方法,然后实例化类.
所以上面的daemon.start相当于:(Catalina)catalinaDaemon.start()
同理,其他的方法如setAwait(true)和load方法也是通过反射调用,这里不在展示代码.
从前面的分析,我们知道tomcat入口类会调用tomcat管理类的start,load,setAwait方法:
tomcat入口类Bootstarp和tomcat管理类Catalina是相辅相成的:
Catalina的启动主要是调用setAwait,load和start方法来完成的,setAwait方法用于设置Server启动完成后是否进入等待状态的标志,如果为true就进入,否则讲究不进入
load方法会自动加载server.xml配置文件,创建并初始化Server [getServer.init()]
start方法用于启动服务器
下面一个个看下这些方法:
首先来看setAwait方法:
org.apache.startup.Catalina:
public void setAwait(boolean b) {
this.await = b;
}
这个方法就是设置await的属性值,await属性会在start方法中的服务器启动完成之后,使用它来判断是否进入等待状态:
查看load方法,文章开头已经讲了load方法,load方法中会根据conf/server.xml创建Server对象,并调用server的init方法来初始化
Catalina的start方法查看:
public void start() {
if (this.getServer() == null) {
this.load();
} if (this.getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
} else {
long t1 = System.nanoTime(); try {
//调用Server的start方法启动服务器
this.getServer().start();
} catch (LifecycleException var7) {
log.fatal(sm.getString("catalina.serverStartFail"), var7); try {
this.getServer().destroy();
} catch (LifecycleException var6) {
log.debug("destroy() failed for failed Server ", var6);
} return;
} long t2 = System.nanoTime();
if (log.isInfoEnabled()) {
log.info("Server startup in " + (t2 - t1) / 1000000L + " ms");
} if (this.useShutdownHook) {
if (this.shutdownHook == null) {
this.shutdownHook = new Catalina.CatalinaShutdownHook();
} Runtime.getRuntime().addShutdownHook(this.shutdownHook);
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager)logManager).setUseShutdownHook(false);
}
}
//判断等待状态
if (this.await) {
this.await();
this.stop();
} }
}
start方法主要调用了server的start方法启动服务器,并根据等待状态判断是否让程序进行等待状态
这里首先判断getServer是否存在,如果不存在就启动server的load方法进行初始化Server.然后调用Server的start方法来启动服务器,注册判断await属性.在tomcat入口类Bootstrap类中,设置await为true,所以需要进入等待状态,跟进逻辑判断的await方法,静态调试进入:
org.apache.catalina.core.StandServer:
发现await方法内部会执行一个while循环,这样程序就会停到awit方法,当await方法里的while循环退出时,就会执行stop方法,从而关闭服务器.
通过上面的学习,我们简单梳理了tomcat的入口类Bootstrap类和tomcat的管理类Catalina
继续学习往下突进:
Server接口的默认实现是org.apache.catalina.core.StandardServer,可通过反射加载进入查看代码:
public final class StandardServer extends LifecycleMBeanBase implements Server {
StandardServer类继承自LifecycleMBeanBase类,跟进LifecycleMBeanBase类:
org.apache.catalina.util.LifecycleMBeanBase:
LifecycleMBeanBase类又继承自LifecycleBase:
org.apache.catalina.util.LifecycleBase:
查看LifecycleBase类的init和start方法:
org.apache.catalina.util.LifecycleBase:
public final synchronized void init() throws LifecycleException {
if (!this.state.equals(LifecycleState.NEW)) {
this.invalidTransition("before_init");
} this.setStateInternal(LifecycleState.INITIALIZING, (Object)null, false); try {
this.initInternal();
} catch (Throwable var2) {
ExceptionUtils.handleThrowable(var2);
this.setStateInternal(LifecycleState.FAILED, (Object)null, false);
throw new LifecycleException(sm.getString("lifecycleBase.initFail", new Object[]{this.toString()}), var2);
} this.setStateInternal(LifecycleState.INITIALIZED, (Object)null, false);
}
发现init方法会调用initInternal方法:
initInternal是一个模块方法,需要其子类去实现此方法.
LifecycleBase类start方法:
截取部分:
public final synchronized void start() throws LifecycleException {
if (!LifecycleState.STARTING_PREP.equals(this.state) && !LifecycleState.STARTING.equals(this.state) && !LifecycleState.STARTED.equals(this.state)) {
if (this.state.equals(LifecycleState.NEW)) {
this.init();
} else if (this.state.equals(LifecycleState.FAILED)) {
this.stop();
} else if (!this.state.equals(LifecycleState.INITIALIZED) && !this.state.equals(LifecycleState.STOPPED)) {
this.invalidTransition("before_start");
} this.setStateInternal(LifecycleState.STARTING_PREP, (Object)null, false); try {
this.startInternal();
} catch (Throwable var2) {
ExceptionUtils.handleThrowable(var2);
this.setStateInternal(LifecycleState.FAILED, (Object)null, false);
throw new LifecycleException(sm.getString("lifecycleBase.startFail", new Object[]{this.toString()}), var2);
}
会调用this.startInternal();方法:
startInternal方法也是模块方法,需要其子类去具体实现方法:
我们的StandardServer类和LifecycleMBeanBase类都是继承自LifecycleBase,都是LifecycleBase的子类,都可以去实现方法的.
回到StandardServer类,查看initInternal方法实现:
org.apache.catalina.core.StandardServer:
protected void initInternal() throws LifecycleException {
super.initInternal();
this.onameStringCache = this.register(new StringCache(), "type=StringCache");
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
this.onameMBeanFactory = this.register(factory, "type=MBeanFactory");
this.globalNamingResources.init();
if (this.getCatalina() != null) {
..................
查看startInternal方法具体实现:
protected void startInternal() throws LifecycleException {
this.fireLifecycleEvent("configure_start", (Object)null);
this.setState(LifecycleState.STARTING);
this.globalNamingResources.start();
synchronized(this.servicesLock) {
for(int i = 0; i < this.services.length; ++i) {
this.services[i].start();
} }
}
除了startInternal和initInternal方法,StandardServer中还实现了await方法,Catalina中就是调用它让服务器进入等待状态的:
public void await() {
if (this.port != -2) {
if (this.port == -1) {
try {
this.awaitThread = Thread.currentThread(); while(!this.stopAwait) {
try {
Thread.sleep(10000L);
} catch (InterruptedException var64) {
}
}
} finally {
this.awaitThread = null;
} } else {
try {
this.awaitSocket = new ServerSocket(this.port, 1, InetAddress.getByName(this.address));
} catch (IOException var67) {
log.error("StandardServer.await: create[" + this.address + ":" + this.port + "]: ", var67);
return;
} boolean var32 = false; ServerSocket serverSocket;
try {
var32 = true;
this.awaitThread = Thread.currentThread(); while(true) {
if (this.stopAwait) {
var32 = false;
break;
} serverSocket = this.awaitSocket;
if (serverSocket == null) {
var32 = false;
break;
} Socket socket = null;
StringBuilder command = new StringBuilder(); label603: {
label602: {
try {
label618: {
long acceptStartTime = System.currentTimeMillis(); InputStream stream;
try {
socket = serverSocket.accept();
socket.setSoTimeout(10000);
stream = socket.getInputStream();
} catch (SocketTimeoutException var69) {
log.warn(sm.getString("standardServer.accept.timeout", new Object[]{System.currentTimeMillis() - acceptStartTime}), var69);
continue;
} catch (AccessControlException var70) {
log.warn("StandardServer.accept security exception: " + var70.getMessage(), var70);
continue;
} catch (IOException var71) {
if (this.stopAwait) {
break label602;
} log.error("StandardServer.await: accept: ", var71);
break label618;
} int expected;
for(expected = 1024; expected < this.shutdown.length(); expected += this.random.nextInt() % 1024) {
if (this.random == null) {
this.random = new Random();
}
} while(true) {
if (expected <= 0) {
break label603;
} boolean var8 = true; int ch;
try {
ch = stream.read();
} catch (IOException var66) {
log.warn("StandardServer.await: read: ", var66);
ch = -1;
} if (ch < 32) {
break label603;
} command.append((char)ch);
--expected;
}
}
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (IOException var63) {
} } var32 = false;
break;
} var32 = false;
break;
} boolean match = command.toString().equals(this.shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
var32 = false;
break;
} log.warn("StandardServer.await: Invalid command '" + command.toString() + "' received");
}
} finally {
if (var32) {
ServerSocket serverSocket = this.awaitSocket;
this.awaitThread = null;
this.awaitSocket = null;
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException var62) {
}
} }
} serverSocket = this.awaitSocket;
this.awaitThread = null;
this.awaitSocket = null;
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException var65) {
}
} }
}
}
StandardServer类中的await实现代码很长,他大概率的处理逻辑是这样的:
首先判断port端口号,port=多少就进入哪个逻辑判断:
port为-1就进入一个while循环:
代码中没有break语句,只有在调用stop的时候,当stopAwait为true,才会退出循环
当port为其他值得时候,走else语句:
创建ServerSocket对象,跟进代码,发现是个绑定操作,绑定地址和端口:
往下看代码,等待接收消息,它会把等待接收消息的数据存储到StringBuilder command中:
StringBuilder command = new StringBuilder(); label603: {
label602: {
try {
label618: {
long acceptStartTime = System.currentTimeMillis(); InputStream stream;
try {
socket = serverSocket.accept();
socket.setSoTimeout(10000);
stream = socket.getInputStream();
} catch (SocketTimeoutException var69) {
log.warn(sm.getString("standardServer.accept.timeout", new Object[]{System.currentTimeMillis() - acceptStartTime}), var69);
continue;
} catch (AccessControlException var70) {
log.warn("StandardServer.accept security exception: " + var70.getMessage(), var70);
continue;
} catch (IOException var71) {
if (this.stopAwait) {
break label602;
} log.error("StandardServer.await: accept: ", var71);
break label618;
} int expected;
for(expected = 1024; expected < this.shutdown.length(); expected += this.random.nextInt() % 1024) {
if (this.random == null) {
this.random = new Random();
}
} while(true) {
if (expected <= 0) {
break label603;
} boolean var8 = true; int ch;
try {
ch = stream.read();
} catch (IOException var66) {
log.warn("StandardServer.await: read: ", var66);
ch = -1;
} if (ch < 32) {
break label603;
} command.append((char)ch);
--expected;
}
继续往下看代码,就是把监听接收到的命令和shutdown匹配,如果匹配上,就break退出循环:
//检查在指定端口接收到的命令是否和shutdown匹配
boolean match = command.toString().equals(this.shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
var32 = false;
break;
}
这里的shutdown和port对应的是conf/server.xml文件中的:
这时程会在8005端口监听shutdown命令,如果接收到就关闭tomcat. 对接收到的数据,tomcat也是有要求的:
int ch;
try {
ch = stream.read();
} catch (IOException var66) {
log.warn("StandardServer.await: read: ", var66);
ch = -1;
} if (ch < 32) {
break label603;
}
接收到的数据ascii<32,会自动截断掉摒弃.
现在已经讲完了Server的启动过程,以及上面讲的tomcat管理类和tomcat入口类,继续冲
Service的启动过程:
Service的默认实现类是:org.apache.catalina.core.StandardService
StandardService和StandardServer一样,都继承自LifecycleMBeanBase,而LifecycleMBeanBase继承自LifecycleBase:
所以StandardService也是会调用initInternal和startInternal方法:
来看下这两个方法:
先看initInternal方法:
protected void initInternal() throws LifecycleException {
super.initInternal();
if (this.container != null) {
this.container.init();
} Executor[] arr$ = this.findExecutors();
int len$ = arr$.length; int len$;
for(len$ = 0; len$ < len$; ++len$) {
Executor executor = arr$[len$];
if (executor instanceof JmxEnabled) {
((JmxEnabled)executor).setDomain(this.getDomain());
} executor.init();
} this.mapperListener.init();
synchronized(this.connectorsLock) {
Connector[] arr$ = this.connectors;
len$ = arr$.length; for(int i$ = 0; i$ < len$; ++i$) {
Connector connector = arr$[i$]; try {
connector.init();
} catch (Exception var9) {
String message = sm.getString("standardService.connector.initFailed", new Object[]{connector});
log.error(message, var9);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new LifecycleException(message);
}
}
} }
}
可以发现StandardService类的initInternl方法先调用父类型的initInternl方法,然后开始调用this.container.init(); executor.init();connector.init();
完整代码:
protected void startInternal() throws LifecycleException {
if (log.isInfoEnabled()) {
log.info(sm.getString("standardService.start.name", new Object[]{this.name}));
} this.setState(LifecycleState.STARTING);
if (this.container != null) {
synchronized(this.container) {
this.container.start();
}
} synchronized(this.executors) {
Iterator i$ = this.executors.iterator(); while(true) {
if (!i$.hasNext()) {
break;
} Executor executor = (Executor)i$.next();
executor.start();
}
} this.mapperListener.start();
synchronized(this.connectorsLock) {
Connector[] arr$ = this.connectors;
int len$ = arr$.length; for(int i$ = 0; i$ < len$; ++i$) {
Connector connector = arr$[i$]; try {
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception var8) {
log.error(sm.getString("standardService.connector.startFailed", new Object[]{connector}), var8);
}
} }
}
可以发现StandardService类的startInternl方法,主要调用了: this.container.start(); executor.start();this.mapperListener.start();connector.start();
mapperListener是Mapper的监听器,可以监听container容器的变化,executors是用在connectors中管理线程的线程池
在server.xml配置文件中有参考用法,不过默认是注释掉的:
内容如下:
<Service name="Catalina"> <!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->
这样Connector就配置了一个tomcatThreadPool线程池,最多可以同时启动150个线程,最少4个可用线程
整个tomcat的启动流程如下:
tomcat 入口类:Bootstrap---->tomcat 管理类:Calalina----->Sever实现类:StandardServer ----->Service实现类:StandardSercice ----->MapperListencer----->Executor----->Connector
浅读tomcat架构设计和tomcat启动过程(1)的更多相关文章
- 浅读tomcat架构设计之tomcat生命周期(2)
浅读tomcat架构设计和tomcat启动过程(1) https://www.cnblogs.com/piaomiaohongchen/p/14977272.html tomcat通过org.apac ...
- 浅读tomcat架构设计之tomcat容器Container(3)
浅读tomcat架构设计和tomcat启动过程(1) https://www.cnblogs.com/piaomiaohongchen/p/14977272.html 浅读tomcat架构设计之tom ...
- Tomcat详解系列(2) - 理解Tomcat架构设计
Tomcat - 理解Tomcat架构设计 前文我们已经介绍了一个简单的Servlet容器是如何设计出来,我们就可以开始正式学习Tomcat了,在学习开始,我们有必要站在高点去看看Tomcat的架构设 ...
- SpringMVC 原理 - 设计原理、启动过程、请求处理详细解读
SpringMVC 原理 - 设计原理.启动过程.请求处理详细解读 目录 一. 设计原理 二. 启动过程 三. 请求处理 一. 设计原理 Servlet 规范 SpringMVC 是基于 Servle ...
- Tomcat架构解析(一)-----Tomcat总体架构
Tomcat是非常常用的应用服务器,了解Tomcat的总体架构以及实现细节,对于理解整个java web也是有非常大的帮助. 一.Server 1.最简单的服务器结构 最简单的服务器结构如图所示: ...
- tomcat 解析(五)-Tomcat的核心组成和启动过程
声明:源码版本为Tomcat 6.0.35 前面的文章中介绍了Tomcat的基本配置,每个配置项也基本上对应了Tomcat的组件结构,如果要用一张图来形象展现一下Tomcat组成的话,整个Tomcat ...
- Tomcat服务器顶层结构和启动过程【转】
号外:2016 最流行的是哪一种 Java 应用服务器呢? 通过从部署的 1240 个 JVM 中得到的数据,我们能够确定出现了 862 个容器供应商,或者说是占到了运行环境的 70% 左右.这些容器 ...
- Tomcat架构解析(五)-----Tomcat的类加载机制
类加载器就是根据类的全限定名(例如com.ty.xxx.xxx)来获取此类的二进制字节流的代码模块,从而程序可以自己去获取到相关的类. 一.java中的类加载器 1.类加载器类别 java中的类加 ...
- 浅读tomcat架构设计之Pipeline-Valve管道(4)
tomcat Container容器处理请求是使用Pipeline-Valve管道来处理的,后续写的tomcat内存马,和他紧密结合 Pipeline-Valve是责任链模式,责任链模式是指在一个请求 ...
随机推荐
- [笔记] 《c++ primer》书店程序 Chapter7
Sales_data.h 1 #ifndef SALES_DATA_H 2 #define SALES_DATA_H 3 4 #include "Version_test.h" 5 ...
- 二进制部署K8S-2集群部署
二进制部署K8S-2集群部署 感谢老男孩教育王导的公开视频,文档整理自https://www.yuque.com/duduniao/k8s. 因为在后期运行容器需要有大量的物理硬件资源使用的环境是用的 ...
- Docker——Jenkins + Git + Registry构建自动化持续集成环境(CI/CD)
前言 在互联网时代,对于每一家公司,软件开发和发布的重要性不言而喻,目前已经形成一套标准的流程,最重要的组成部分就是持续集成(CI)及持续部署.交付(CD). 本文基于Jenkins+Docker+G ...
- 云计算OpenStack核心组件---glance镜像服务(6)
一.glance介绍: Glance是Openstack项目中负责镜像管理的模块,其功能包括虚拟机镜像的查找.注册和检索等. Glance提供Restful API可以查询虚拟机镜像的metadata ...
- Go语言web开发---Beego基础
一.框架 框架:可复用的设计组件,它规定了应用的体系结构,明确了整个设计,协作各个组件之间的依赖关系,责任分配,和流程控制.通俗解释框架就是一堆代码的集合,为了提高软件的开发效率和质量,一般都会使用框 ...
- Python+Selenium - 下拉列表处理
下拉列表分两种:select下拉表和非select下拉表. 1.select下拉列表 如下图元素代码展示 可用Select类处理 from selenium.webdriver.support.sel ...
- The Superego 实验四 团队作业1:软件研发团队组建
项目 内容 课程班级博客链接 班级博客链接 这个作业要求链接 作业要求链接 团队名称 The Superego 团队的课程学习目标 (1)组建团队,建设团队文化,申请开通团队博客 (2)团队之间相互协 ...
- 将代码生成器带入TVM
将代码生成器带入TVM 为了使数据科学家不必担心开发新模型时的性能,硬件后端提供程序(例如Intel,NVIDIA,ARM等)可以提供诸如cuBLAS或cuDNN之类的内核库以及许多常用的深度学习内核 ...
- ITS智能交通监控系统技术解析
ITS智能交通监控系统技术解析 红灯,逆行,变 车辆抓拍和车速检测 非法停车和交通流量检测 交叉路口违法检测 发生碰撞的交叉口是智能交通管理. 机动执法 当你需要一个可以移动的系统时,会跟着你移动.移 ...
- 浪潮 ClusterEngineV4.0 任意命令执行
1.浪潮ClusterEngineV4.0 任意命令执行 影响版本 ClusterEngineV4.0 2.漏洞影响 远程代码执行 3.复现 fofa语句 title='TSCEV4.0' 抓包构造e ...