【笔记】JavaWeb - Tomcat

在这里插入图片描述

💡 如何搭建tomcat的调试环境,以跟踪/分析源码的执行,可以参考: https://blog.csdn.net/LawssssCat/article/details/103452278

文章目录

历史

  1. Tomcat 最初由 Sun公司的软件架构师 James Duncan Davidson 开发,名为 “JavaWebServer”
  2. 1999年,该项目与 apache软件基金会旗下的 JServ 项目合并,并发布第一个版本(3.x),即是现在的Tomcat,该版本实现了 servlet2.2 和 jsp1.1规范
  3. 2001年,Tomcat 发布了4.0版本,作为里程碑式的版本,Tomcat进行了架构重构,并实现了 Servlet2.3 和 jsp1.2 规范
  4. 目前(2019年),Tomcat 更新到 9.0.x版本。主流版本7.x和8.x

💡 提示

常见web服务器

名字维护者支持规范费用
weblogicoracle公司支持所有JavaEE规范收费
webSphereIBM公司支持所有JavaEE收费
JBOSSJBOSS公司支持所有JavaEE规范收费
TomcatApache组织仅支持少量JavaEE规范 servlet/jsp免费、开源

概念

术语 Terminology

# 术语: 上下文 context

Context - 一个context就是一个web应用。(In a nutshell, a Context is a web application.)

# 目录结构
  • /bin - 脚本目录
    Startup, shutdown, and other scripts. The *.sh files (for Unix systems) are functional duplicates of the *.bat files (for Windows systems). Since the Win32 command-line lacks certain functionality, there are some additional files in here.
  • /conf - 配置目录
    Configuration files and related DTDs. The most important file in here is server.xml. It is the main configuration file for the container.
  • /logs - 日志目录
    Log files are here by default.
  • /webapps - 我们的web应用存放目录
    This is where your webapps go.

在这里插入图片描述

# 变量:CATALINA_HOME、CATALINA_BASE
  • CATALINA_HOME : Tomcat 的安装位置(借此找到lib
    (Represents the root of your Tomcat installation, for example /home/tomcat/apache-tomcat-9.0.10 or C:\Program Files\apache-tomcat-9.0.10.)
  • CATALINA_BASE: Tomcat 实例的运行时配置根目录。 (借此找到conf、webapps
    (Represents the root of a runtime configuration of a specific Tomcat instance. If you want to have multiple Tomcat instances on one machine, use the CATALINA_BASE property.)

💡 提示

At minimum, CATALINA_BASE must contain:

  • conf/server.xml
  • conf/web.xml

整体架构

Tomcat 本质上就是一款 Servlet 容器,其中 Catalina(Servlet 容器)组件是核心,其他模块都是为Catalina提供支持的。

比如:

  • Coyote 模块提供链接通信
  • Jasper 模块提供JSP引擎
  • Naming 提供JNDI服务(命名服务)
  • JULI 提供日志服务

平面图

# 业务解耦(解释:为啥要弄Servlet容器,而不直接使用Servlet实例)

希望将调用逻辑和业务逻辑分开(即希望单独编写、测试业务逻辑和业务逻辑,而不需要每添加一个业务都要写一次调用逻辑)

在这里插入图片描述
在这里插入图片描述
解耦后的工作流程:

定位、加载、调用Servlet

在这里插入图片描述

# 抽象方案: Connector、Container

Tomcat 要实现两个核心功能:

  1. 处理Socket连接,负责网络字节流与Request和Response对象的转化
  2. 加载和管理Servlet,以及具体处理Request请求

为此,Tomcat设计了两个(抽象的)核心组件:

  1. 连接器(Connector): 对外交流
  2. 容器(Container): 内部处理
Tomcat(Server )
Service(nane='Catalina')
request(internal)
response(internal)
request 'http://shop.myhost.com:8080/shop/detail?id=99861'
response '200 OK {id=99861,price=56,...}'
Container(Engine name='Catalina')
Host 'shop.myhost.com'
Context '/pay'
Context '/shop'
Wrapper(Servlet) '/detail'
Host 'play.myhost.com'
Context '/music
Context '/game
Context '/movie
Connectors(Coyote)
HTTP1.1 protocol connector(port:8080)
HTTP1.1 protocol connector(port:8081)
....
client
# 具体方案: 连接器(Connector) Coyote

Coyote 是 Connector 的具体实现。

Coyote 封装了网络通信(Socket 请求及响应处理),为Catalina容器提供了统一的接口,使Catalina容器与具体的请求协议及IO操作方式完全解耦:

  • Coyote 将 Socket 输入转换封装为 Request 对象,交由 Catalina容器进行处理
  • 处理完成后,Catalina容器通过Coyote提供的Response对象将结果写入输出流

在这里插入图片描述

💡 提示

Coyote 作为独立的模块,只负责具体协议和IO的相关操作,与Servlet规范实现没有直接关系,因此即便是Request对象和Response对象也并未实现Servlet规范对应的接口,而是在Catalina中将他们封装为ServletRequest和ServletResponse

IO模型与协议

在Coyote中,Tomcat支持的多种I/O模型和应用层协议:

IO模型 (至8.5/9.0版本起,Tomcat移除了对BIO的支持)

IO模型描述
NIO非阻塞I/O,采用Java NIO类库实现
NIO2异步I/O,采用JDK7最新的NIO2类库实现
APR采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库

💡 更多: https://blog.csdn.net/LawssssCat/article/details/103194519

应用层协议

应用层协议描述
HTTP/1.1这是大部分Web应用采用的访问协议
AJP用于和Web服务器继承(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3
HTTP/2HTTP 2.0 大幅度的提升了Web性能。下一代HTTP协议,自8.5以及9.0版本之后支持

在这里插入图片描述

💡 提示

在 8.0 之前,Tomcat默认采用BIO作为I/O方式,之后改为NIO。
无论是NIO、NIO2还是APR,在性能方面均优于以往的BIO。
如果采用APR,甚至可以达到Apache HTTP Server 的理想性能。

初始化代码:

// Connector.initInternal
    @SuppressWarnings("deprecation")
    @Override
    protected void initInternal() throws LifecycleException {

        super.initInternal();

        // Initialize adapter
        adapter = new CoyoteAdapter(this);
        protocolHandler.setAdapter(adapter);

        // Make sure parseBodyMethodsSet has a default
        if (null == parseBodyMethodsSet) {
            setParseBodyMethods(getParseBodyMethods());
        }

        if (protocolHandler.isAprRequired() && !AprLifecycleListener.isInstanceCreated()) {
            throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprListener",
                    getProtocolHandlerClassName()));
        }
        if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
            throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprLibrary",
                    getProtocolHandlerClassName()));
        }
        if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
                protocolHandler instanceof AbstractHttp11JsseProtocol) {
            AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
                    (AbstractHttp11JsseProtocol<?>) protocolHandler;
            if (jsseProtocolHandler.isSSLEnabled() &&
                    jsseProtocolHandler.getSslImplementationName() == null) {
                // OpenSSL is compatible with the JSSE configuration, so use it if APR is available
                jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
            }
        }

        try {
            protocolHandler.init();
        } catch (Exception e) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
        }
    }
组件
Connector
Http11NioProtocol
NioEndPoint TCP/IP
ConnectionHandler
register()
processSocket()
getHander(),process()
service()
CoyoteAdapter
Http11Processor
Poller
Acceptor
SocketProcessor
client
Container
EndPoint

EndPoint: Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的。

💡 提示

Http11NioProtocol 的 NioEndPoint 中

还有有 accepter、poller、socketprocessor

AbstractEndPoint

Tomcat 并没有 EndPoint接口,而是提供了一个抽象类AbstractEndPoint,里面又定义了两个内部类:

  • Acceptor: 监听Socket连接请求

  • SocketProcessor: 处理接收到的Socket请求。

    • ⚠️其实现了Runnable的Run方法,在Run方法里调用(下面要介绍的)组件Processor进行协议处理
    • 为了提高处理性能,SocketProcessor被提交到线程池来处理。而这个线程池叫执行器(Executor)

    在这里插入图片描述

在这里插入图片描述

Processor

Processor: Coyote 协议处理接口,如果说EndPoint是用来实现TCP/IP协议的,那么Processor就是用来实现HTTP协议的。

Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。

ProtocolHandler

ProtocolHandler: Coyote协议接口,通过Endpoint和Processor实现针对具体协议的处理能力。Tomcat按照协议和I/O提供了6个实现类:

AJP协议:

  1. AjpNioProtocol: 采用NIO的IO模型
  2. AjpAprProtocol: 采用APR的IO模型 (💡需要依赖于APR库)
  3. AjpNio2Protocol: 采用NIO2的IO模型

HTTP协议:

  1. Http11NioProtocol: 采用NIO的IO模型(如果服务器没有安装APR,则默认使用这个协议)
  2. Http11Nio2Protocol: 采用NIO2的IO模型
  3. Http11ArpProtocol: 采用APR的IO模型 (💡需要依赖于APR库)

我们在配置tomcat/conf/server.xml时,至少要指定具体的ProtocolHandler。当然,也可以通过协议名的方式指定,如: HTTP/1.1,在安装了APR时,使用Http11AprProtocol,否则使用Http11NioProtocol

// Connector.setProtocol
@Deprecated
public void setProtocol(String protocol) {

    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
            AprLifecycleListener.getUseAprConnector();

    if ("HTTP/1.1".equals(protocol) || protocol == null) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
        }
    } else if ("AJP/1.3".equals(protocol)) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
        }
    } else {
        setProtocolHandlerClassName(protocol);
    }
}

在这里插入图片描述

Adapter

Tomcat Request

由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类(org.apache.coyote.Request)来存放这些信息。

Servlet Request

ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。

适配

Tomcat的解决方案是引入CoyoteAdapter(适配器)来连接两种Request。连接器调用CoyoteAdapter的Service方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的Service方法。

# 具体方案: 容器(Container) Catalina

Tomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的Servlet容器的一个实现。

容器外部的划分(和其他组件的划分)

它们不是平行的关系,而是存在相互嵌套的:

在这里插入图片描述

💡 具体的嵌套关系是参考 ${tomcat_home}/conf/server.xml 配置文件得来的:

<!-- 💡 tomcat服务器(Server)。8005为管理界面端口 -->
<Server port="8005" shutdown="SHUTDOWN">
  <!-- 💡 一个名为Catalina的服务(Service)声明 -->
  <Service name="Catalina">
	<!-- 连接器(Connector Coyote实现) -->
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

	<!--
		这里所谓的容器(Container)是抽象概念,具体有不同层级的实现:
		1. Engine容器: 管理Host容器(💡大管家: 不干实事,就干下面的下属,保证它们正常工作)
		2. Host容器: 管理Context容器,自己处理站点(域名 Host)相关的事务
		3. Context容器: 管理Wrapper(Servlet)容器,自己处理自己管辖下的webapp的全局性事务
		4. Wrapper容器: 内部管理着(JavaWeb规范提供的)Servlet接口(💡Servlet直属管家: 上面任何与Servlet的对接都通过它)
	-->

	<!-- 💡 一个名为Catalina的引擎(Engine)容器 -->
    <!-- 💡 具体类上实现 Lifecycle 接口,提供了一种优雅的启动和关闭整个系统的方式 -->
    <Engine name="Catalina" defaultHost="localhost">
      <!-- 💡 一个站点(域名 host)容器,管理localhost域名下的请求与${tomcat_base}/webapps下的应用的上下文(Context)对接 -->
      <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">
        <!-- 💡 一个上下文(Context)容器, 主要管理请求与${tomcat_base}/webapps/test-tomcat下的Servlet(Wrapper)的对接 -->
        <Context docBase="F:\bin_java\apache-tomcat-8.5.83\wtpwebapps\test-tomcat" path="/test-tomcat" reloadable="true" source="org.eclipse.jst.jee.server:test-tomcat"/>
        <!-- 💡 具体Servlet(Wrapper)的配置就不在server.xml,而是在web.xml -->
      </Host>
    </Engine>
  </Service>
</Server>
容器内部的划分

Tomcat 设计了4种容器,分别是Engine、Host、Context、和Wrapper。这4种容器不是平行关系,而是父子关系。

Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性:

在这里插入图片描述

各个组件的含义:

容器描述
Engine管理多个虚拟机站点
Host代表一个虚拟机(站点),可以给Tomcat配置多个虚拟机地址。(对应域名
Context表示一个Web应用程序(对应Web应用
Wrapper表示一个Servlet(对应Servlet服务

在代码依赖上,表现如下:

在这里插入图片描述

启动流程

分析tomcat的服务线程启动,搞清楚启动后,下图的这些线程都是干嘛的:(以默认配置为例)

在这里插入图片描述

大体流程如下图:

在这里插入图片描述

💡 提示

跟踪源码,便可将上述启动流程走一遍。

跟踪源码的时候需要注意: 很多时候找到的组件是一个接口,如何根据接口找到具体实现,可以结合上述提供的类模型和零星提到的默认实现帮助查找。
(实在猜不到,就上源码debug呗)

# 打破双亲委派机制

参考 : https://blog.csdn.net/LawssssCat/article/details/108369694

# 对象创建过程 (反射、new)

在这里插入图片描述

Digester

tomcat 的对象初始化都是由 Digester 完成的

Digester 的任务是解析server.xml文件(SAX方式解析),边解析边创建容器各组件对象实例,同时把它们以“父子”关系相互关联起来。

(Digester的工作流程参考: https://blog.csdn.net/LawssssCat/article/details/103614268

💡 提示

所谓“父子”关系即包含关系

<father>
	<son1></son1>
	<son2></son2>
</father>

💡 提示

对象创建、关联的代码非常巧妙,结合SAX解析方式的特性,Tomcat设计了Rule对象,通过继承Rule对象并重写其相应的方法,使Rule方法在SAX解析的各个事件中发挥不同的功能

MBean 【未完】

大概是用来监控组件状态的

todo jmeter

过程中使用的参数

启动命令: javaw -Dcatalina.home=${workspace_loc}/${project_path}/tomcat/src -Dcatalina.base=${workspace_loc}/${project_path}/tomcat/src -Dfile.encoding=UTF-8 ... org.apache.catalina.startup.Bootstrap start

调用时机参数类型参数含义
Bootstrap static block-D“user.dir”项目目录,如 E:\temp1\learn-tomcat
Bootstrap static block-D“catalina.home”如 E:\temp1/\learn-tomcat/tomcat/src
Bootstrap static block-D“catalina.base”如 E:\temp1/\learn-tomcat/tomcat/src
bootStrap.init()
CatalinaProperties static block-D“catalina.config”catalina 配置
CatalinaProperties static block.properties${catalina.base}/conf/catalina.propertiescatalina 配置(若上述配置不存在)
CatalinaProperties static block/org/apache/catalina/startup/catalina.propertiescatalina 配置(若上述配置不存在)
CatalinaProperties getProperty function-D 使用“common.loader”CommonClassLoader 负责的目录
CatalinaProperties getProperty function-D 使用“server.loader”ServerClassLoader 负责的目录
CatalinaProperties getProperty function-D 使用“shared.loader”SharedClassLoader 负责的目录
catalinaDaemon = new Catalina (反射)
catalinaDaemon .setParentClassLoader (反射)参数传递sharedLoader
bootStrap.load()
catalinaDaemon .load (反射)
catalinaDaemon.initNaming-D设置 “catalina.useNaming” true
设置 “java.naming.factory.url.pkgs”
设置 “java.naming.factory.initial”
catalinaDaemon.createStartDigesterxml解析器
解析 conf/server.xml 或 server-embed.xml 配置文件
SAX解析过程中创建容器实例
Digester static block-D“org.apache.tomcat.util.digester.PROPERTY_SOURCE"
“org.apache.tomcat.util.digester.REPLACE_SYSTEM_PROPERTIES”
catalinaDaemon.digester.parse(inputSource);
(SAX解析xml)回调catalinaDaemon.digesterstartElement
catalinaDaemon.getServer().init();
new Connector-D“org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH”
# 对象初始化过程 (init)

在这里插入图片描述

getServer().init();

这里的对象大都是LifecycleBase,因此,都要走到下面这个方法:

@Override
public final synchronized void init() throws LifecycleException {
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }

    try {
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        initInternal();
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        handleSubClassException(t, "lifecycleBase.initFail", toString());
    }
}

其中最多需要关注的是 initInternal 里面的方法,这个方法是抽象的,因此不同实现的主要内容都写在这里了

事件方法参数类型参数描述
Lifecycle.BEFORE_INIT_EVENT-Djdbc.drivers
initStandardEngine.initInternal
ContainerBase.initInternal
创建线程池
startStopExecutor = new ThreadPoolExecutor(
initconnector.init();
initCoyoteAdapter static block-D“org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH”
protocolHandler.init() ⭐️

这里好像没做什么,就是注册了几个mbean

以默认的 Http11NioProtocol这个protocolHandler为例:

在这里插入图片描述

@Override
public void init() throws Exception {
    if (getLog().isInfoEnabled()) {
        getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
        logPortOffset();
    }

    if (oname == null) {
        // Component not pre-registered so register it
        oname = createObjectName();
        if (oname != null) {
            Registry.getRegistry(null, null).registerComponent(this, oname, null);
        }
    }

    if (this.domain != null) {
        ObjectName rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
        this.rgOname = rgOname;
        Registry.getRegistry(null, null).registerComponent(
                getHandler().getGlobal(), rgOname, null);
    }

    String endpointName = getName();
    endpoint.setName(endpointName.substring(1, endpointName.length()-1));
    endpoint.setDomain(domain);

    endpoint.init();
}

在这里插入图片描述

public void init() throws Exception {
    if (bindOnInit) {
        bindWithCleanup();
        bindState = BindState.BOUND_ON_INIT;
    }
    if (this.domain != null) {
        // Register endpoint (as ThreadPool - historical name)
        oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");
        Registry.getRegistry(null, null).registerComponent(this, oname, null);

        ObjectName socketPropertiesOname = new ObjectName(domain +
                ":type=SocketProperties,name=\"" + getName() + "\"");
        socketProperties.setObjectName(socketPropertiesOname);
        Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);

        for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
            registerJmx(sslHostConfig);
        }
    }
}
# 对象启动过程 (start) 【未完】

启动过程有以下任务:

  1. 开启nio连接监听
  2. 注册mbean

💡 提示

通过重写 LifecycleBase 的 startInternal();方法 在 start 中被调用

在这里插入图片描述

protected void startInternal() throws LifecycleException {

    fireLifecycleEvent(CONFIGURE_START_EVENT, null);
    setState(LifecycleState.STARTING);

    globalNamingResources.start(); // 💡 创建一些以“名字声明的资源”

    // Start our defined Services
    synchronized (servicesLock) {
        for (Service service : services) { // 💡 默认 [StandardService[Catalina]]
            service.start(); // 💡 start事件向下传递
        }
    }
}

在这里插入图片描述

Poller 创建(selector) ⭐️

在这里插入图片描述

poller 就是 nio 中的 Selector

public Poller() throws IOException {
    this.selector = Selector.open();
}

同时,poller还是个 Runnable

NioEndpoint 的 startInternal 中

// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();

startAcceptorThread();
startup.sh BootStrap Catalina Server Service Executor Engine Host Context Connector ProtocolHandler Http11NioProtocol (Abstract)EndPoint Acceptor Socket javaw -Dcatalina.home=${workspace_loc}/${project_path}/tomcat/src -Dcatalina.base=${workspace_loc}/${project_path}/tomcat/src -Dfile.encoding=UTF-8 ... org.apache.catalina.startup.Bootstrap start init load load digester创建server.xml上注册的实例: Server、listener、service、engine、... init init init init init init init init init bind serverSocket = ServerSocketChannel.open start start start start start start start start start start start startInternal startAcceptorThreads acceptor = createAcceptor new Acceptor new Thread(acceptor).start run serverSocket.accept startup.sh BootStrap Catalina Server Service Executor Engine Host Context Connector ProtocolHandler Http11NioProtocol (Abstract)EndPoint Acceptor Socket

Socket监听启动流程(http-nio-8080)

💡 前置知识:

  1. 起码要知道nio有哪些组件、api
    关于 nio 的 api 这里有介绍: https://lawsssscat.blog.csdn.net/article/details/103194519
  2. nio服务器的启动步骤
# ServerSocketChannel

看到 NioEndPoint.initServerSocket() 方法,这是开启 ServerSocketChannel 的地方,在EndPoint的init中被调用:(💡 这个EndPoint被Digester反射创建的)

在这里插入图片描述

// Separated out to make it easier for folks that extend NioEndpoint to
// implement custom [server]sockets
protected void initServerSocket() throws Exception {
    if (getUseInheritedChannel()) {
        // Retrieve the channel provided by the OS
        Channel ic = System.inheritedChannel();
        if (ic instanceof ServerSocketChannel) {
            serverSock = (ServerSocketChannel) ic;
        }
        if (serverSock == null) {
            throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
        }
    } else { // 💡 一般走这里
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket()); // 💡 bio 中的 ServerSocket
        InetSocketAddress addr = new InetSocketAddress(
	        getAddress(),  // 💡 默认 null
	        getPortWithOffset() // 💡 默认 8080
        );
        serverSock.socket().bind(addr,getAcceptCount());
    }
    // 💡 阻塞?
    serverSock.configureBlocking(true); //mimic APR behavior
}
# Poller 启动(selector)

nio 的 Selector 被 tomcat 封装成了 Poller 类,它也是一个 Runnable

public class Poller implements Runnable {

    private Selector selector;

它是在 start 阶段被构造

在这里插入图片描述

专门创建创建一个线程给它监听事件

public Poller() throws IOException {
	this.selector = Selector.open();
}

Poller 的 run 方法(Runnable实现)就展示了标准的 nio 服务端实现:

private volatile boolean close = false;
/**
 * The background thread that adds sockets to the Poller, checks the
 * poller for triggered events and hands the associated socket off to an
 * appropriate processor as events occur.
 */
@Override
public void run() {
    // Loop until destroy() is called
    while (true) {

        boolean hasEvents = false;

        try {
            if (!close) {
            	// 💡 这里注册监听的内容
                hasEvents = events();
                // 💡 NIO 中的 Selector.select() 方法调用
                if (wakeupCounter.getAndSet(-1) > 0) {
                    // If we are here, means we have other stuff to do
                    // Do a non blocking select
                    keyCount = selector.selectNow();
                } else {
                    keyCount = selector.select(selectorTimeout);
                }
                wakeupCounter.set(0);
            }
            if (close) {
				// 略。。。。
                break;
            }
            // Either we timed out or we woke up, process events first
            if (keyCount == 0) {
                hasEvents = (hasEvents | events());
            }
        } catch (Throwable x) {
            ExceptionUtils.handleThrowable(x);
            log.error(sm.getString("endpoint.nio.selectorLoopError"), x);
            continue;
        }

		// 💡 遍历监听到的事件,然后逐一处理
        Iterator<SelectionKey> iterator =
            keyCount > 0 ? selector.selectedKeys().iterator() : null;
        // Walk through the collection of ready keys and dispatch
        // any active event.
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            iterator.remove();

			// 略 。。。。

        }

        // Process timeouts
        timeout(keyCount,hasEvents);
    }

    getStopLatch().countDown();
}

请求处理流程 ⭐️

http-nio 处理请求 http://localhost:8080/test/ 的流程

implements
implements
extends
extends
implements
extends
S=NioChannel
extends
implements
implements
extends
1. startInternal()
2. run()
⭐️ while(true) {...}
3. processKey()
4. processSocket()
5. createSocketProcessor()
6. getExecutor()
7. executor.execute(..)
run()
8. doRun()
9. getHandler()
10. process()
11. process()
11. process()
12. service()
12. service()
13. setSocketWrapper
14. init
15. parseRequestLine
...
生成request、response
16. getAdapter().service(request, response)
implements
16. service(request, response)
17. connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)
Connector
ProtocolHandler ProtocolHandler
«interface»
ProtocolHandler
AbstractProtocol<S>
AbstractEndpoint<S> endpoint
Handler<S> handler
Adapter adapter
«interface»
Handler
SocketState process(SocketWrapperBase socketWrapper, SocketEvent status)
ConnectionHandler<S>
«interface»
Processor
SocketState process(SocketWrapperBase socketWrapper, SocketEvent status)
AbstractProcessorLight
SocketState service(SocketWrapperBase socketWrapper)
AbstractProcessor
Request request
Response response
Adapter adapter
Http11Processor
Http11InputBuffer inputBuffer
Http11OutputBuffer outputBuffer
void prepareRequest
setSocketWrapper(SocketWrapperBase socketWrapper)
Http11NioProtocol
«Abstract»
AbstractEndpoint
Handler<S> handler
init()
startInternal()
getHandler()
«interface»
Runnable
run()
NioEndpoint
NioEndpoint$Poller poller
Executor executor
init()
startInternal()
createSocketProcessor()
Poller
Selector selector
run()
processKey()
processSocket()
«abstract»
SocketProcessorBase<NioChannel>
run()
abstract doRun()
SocketProcessor
doRun()
Http11InputBuffer
⭐️ByteBuffer byteBuffer
init(SocketWrapperBase socketWrapper)
parseRequestLine(boolean keptAlive)
«interface»
Adapter
CoyoteAdapter
Engine
# Poller 监听(selector)

在启动流程的最后,我们启动了一个叫 Poller 的对象,它内部封装了 nio 的 Selector,很明显它就是用来接收请求,然后分发任务的

我们把断点打到 Poller 的 run 方法中

// 💡 start后,这个Runnable的run方法就会运行
@Override
public void run() {
    // Loop until destroy() is called
    while (true) { // 💡 无论是否有请求,都会不断的循环

        boolean hasEvents = false;

        try {
            if (!close) { // 💡 默认false,poller destroy后状态才会改变
                hasEvents = events(); // 💡 ⭐️ 设置事件(events)监听,如果设置了事件监听则返回true
                if (wakeupCounter.getAndSet(-1) > 0) {
                    // If we are here, means we have other stuff to do
                    // Do a non blocking select
                    keyCount = selector.selectNow();
                } else {
                	// 💡 一般来这里
                	// 💡 这是 nio 的 api! 如果有事件返回事件个数
                	// selectorTimeout 默认1s超时
                    keyCount = selector.select(selectorTimeout);
                }
                wakeupCounter.set(0);
            }
            if (close) {
                events();
                timeout(0, false);
                try {
                    selector.close();
                } catch (IOException ioe) {
                    log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
                }
                break;
            }
            // Either we timed out or we woke up, process events first
            if (keyCount == 0) {
                hasEvents = (hasEvents | events());
            }
        } catch (Throwable x) {
            ExceptionUtils.handleThrowable(x);
            log.error(sm.getString("endpoint.nio.selectorLoopError"), x);
            continue;
        }

		// 💡 ⭐️ nio的api,获取监听到的事件,准备遍历
        Iterator<SelectionKey> iterator =
            keyCount > 0 ? selector.selectedKeys().iterator() : null;
        // Walk through the collection of ready keys and dispatch
        // any active event.
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            iterator.remove();
            // 💡 通过它可以建立连接
            NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
            // Attachment may be null if another thread has called
            // cancelledKey()
            if (socketWrapper != null) {
                processKey(sk, socketWrapper);
            }
        }

        // Process timeouts
        timeout(keyCount,hasEvents);
    }

    getStopLatch().countDown();
}

private final SynchronizedQueue<PollerEvent> events =
                new SynchronizedQueue<>();
/**
 * Processes events in the event queue of the Poller.
 *
 * @return <code>true</code> if some events were processed,
 *   <code>false</code> if queue was empty
 */
public boolean events() {
    boolean result = false;

    PollerEvent pe = null;
    // 💡 遍历事件(events)队列
    for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
        result = true;
        NioSocketWrapper socketWrapper = pe.getSocketWrapper();
        SocketChannel sc = socketWrapper.getSocket().getIOChannel();
        int interestOps = pe.getInterestOps();
        if (sc == null) {
            log.warn(sm.getString("endpoint.nio.nullSocketChannel"));
            socketWrapper.close();
        } else if (interestOps == OP_REGISTER) {
            try {
                sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
            } catch (Exception x) {
                log.error(sm.getString("endpoint.nio.registerFail"), x);
            }
        } else {
            final SelectionKey key = sc.keyFor(getSelector());
            if (key == null) {
                // The key was cancelled (e.g. due to socket closure)
                // and removed from the selector while it was being
                // processed. Count down the connections at this point
                // since it won't have been counted down when the socket
                // closed.
                socketWrapper.close();
            } else {
                final NioSocketWrapper attachment = (NioSocketWrapper) key.attachment();
                if (attachment != null) {
                    // We are registering the key to start with, reset the fairness counter.
                    try {
                        int ops = key.interestOps() | interestOps;
                        attachment.interestOps(ops);
                        key.interestOps(ops);
                    } catch (CancelledKeyException ckx) {
                        cancelledKey(key, socketWrapper);
                    }
                } else {
                    cancelledKey(key, socketWrapper);
                }
            }
        }
        if (running && eventCache != null) {
            pe.reset();
            eventCache.push(pe);
        }
    }

    return result;
}


protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
    try {
    	// 💡 检查poller是否关闭
        if (close) {
            cancelledKey(sk, socketWrapper);
        } else if (sk.isValid()) { // 💡 (nio api)检查SelectionKey是否有效,是否调用了cancel方法
         	// 💡 (nio api) 读、写事件
            if (sk.isReadable() || sk.isWritable()) {
                if (socketWrapper.getSendfileData() != null) {
                    processSendfile(sk, socketWrapper, false);
                } else {
                	// 💡 unregist 将触发监听的事件去除监听,如:收到read事件,则移除read监听
                    unreg(sk, socketWrapper, sk.readyOps());
                    boolean closeSocket = false;
                    // Read goes before write
                    if (sk.isReadable()) {
                        if (socketWrapper.readOperation != null) {
                            if (!socketWrapper.readOperation.process()) {
                                closeSocket = true;
                            }
                        } else if (socketWrapper.readBlocking) {
                            synchronized (socketWrapper.readLock) {
                                socketWrapper.readBlocking = false;
                                socketWrapper.readLock.notify();
                            }
                        } else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
                            closeSocket = true;
                        }
                    }
                    if (!closeSocket && sk.isWritable()) {
                        if (socketWrapper.writeOperation != null) {
                            if (!socketWrapper.writeOperation.process()) {
                                closeSocket = true;
                            }
                        } else if (socketWrapper.writeBlocking) {
                            synchronized (socketWrapper.writeLock) {
                                socketWrapper.writeBlocking = false;
                                socketWrapper.writeLock.notify();
                            }
                        // 💡 处理 Socket
                        } else if (!processSocket(socketWrapper, SocketEvent.OPEN_WRITE, true)) {
                            closeSocket = true;
                        }
                    }
                    if (closeSocket) {
                        cancelledKey(sk, socketWrapper);
                    }
                }
            }
        } else {
            // Invalid key
            cancelledKey(sk, socketWrapper);
        }
    } catch (CancelledKeyException ckx) {
        cancelledKey(sk, socketWrapper);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("endpoint.nio.keyProcessingError"), t);
    }
}


// ---------------------------------------------- Request processing methods

/**
 * Process the given SocketWrapper with the given status. Used to trigger
 * processing as if the Poller (for those endpoints that have one)
 * selected the socket.
 *
 * @param socketWrapper The socket wrapper to process
 * @param event         The socket event to be processed
 * @param dispatch      Should the processing be performed on a new
 *                          container thread
 *
 * @return if processing was triggered successfully
 */
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
        SocketEvent event, boolean dispatch) {
    try {
        if (socketWrapper == null) {
            return false;
        }
        // 💡 这里是Runnable的实现,获取一个socket的处理方法
        SocketProcessorBase<S> sc = null;
        if (processorCache != null) {
            sc = processorCache.pop();
        }
        if (sc == null) {
            sc = createSocketProcessor(socketWrapper, event);
        } else {
            sc.reset(socketWrapper, event);
        }
        // 💡 调用线程池,执行方法
        Executor executor = getExecutor();
        if (dispatch && executor != null) {
            executor.execute(sc);
        } else {
            sc.run();
        }
    } catch (RejectedExecutionException ree) {
        getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
        return false;
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        // This means we got an OOM or similar creating a thread, or that
        // the pool and its queue are full
        getLog().error(sm.getString("endpoint.process.fail"), t);
        return false;
    }
    return true;
}
# SocketProcessor (Buffer)

💡 提示

接收到监听读事件后,(SocketProcessor的)任务就是将(socketWrapper中)数据读出,形成request、response,然后交给Adapter处理(CoyoteAdapter)

我们跟踪的是 http-nio 的处理,所以到 SocketProcessorBase 的对应实现的 run 方法上

在这里插入图片描述

@Override
public final void run() {
    synchronized (socketWrapper) {
        // It is possible that processing may be triggered for read and
        // write at the same time. The sync above makes sure that processing
        // does not occur in parallel. The test below ensures that if the
        // first event to be processed results in the socket being closed,
        // the subsequent events are not processed.
        if (socketWrapper.isClosed()) {
            return;
        }
        doRun();
    }
}


@Override
protected void doRun() {
    /*
     * Do not cache and re-use the value of socketWrapper.getSocket() in
     * this method. If the socket closes the value will be updated to
     * CLOSED_NIO_CHANNEL and the previous value potentially re-used for
     * a new connection. That can result in a stale cached value which
     * in turn can result in unintentionally closing currently active
     * connections.
     */
    Poller poller = NioEndpoint.this.poller;
    if (poller == null) {
        socketWrapper.close();
        return;
    }

    try {
    	// 💡 三次握手?
        int handshake = -1;
        try {
        	// 💡 如果握手完成,就设置为0
            if (socketWrapper.getSocket().isHandshakeComplete()) {
                // No TLS handshaking required. Let the handler
                // process this socket / event combination.
                handshake = 0;
            } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT ||
                    event == SocketEvent.ERROR) {
                // Unable to complete the TLS handshake. Treat it as
                // if the handshake failed.
                handshake = -1;
            } else {
                handshake = socketWrapper.getSocket().handshake(event == SocketEvent.OPEN_READ, event == SocketEvent.OPEN_WRITE);
                // The handshake process reads/writes from/to the
                // socket. status may therefore be OPEN_WRITE once
                // the handshake completes. However, the handshake
                // happens when the socket is opened so the status
                // must always be OPEN_READ after it completes. It
                // is OK to always set this as it is only used if
                // the handshake completes.
                event = SocketEvent.OPEN_READ;
            }
        } catch (IOException x) {
            handshake = -1;
            if (logHandshake.isDebugEnabled()) {
                logHandshake.debug(sm.getString("endpoint.err.handshake",
                        socketWrapper.getRemoteAddr(), Integer.toString(socketWrapper.getRemotePort())), x);
            }
        } catch (CancelledKeyException ckx) {
            handshake = -1;
        }

		// 💡
        if (handshake == 0) {
            SocketState state = SocketState.OPEN;
            // Process the request from this socket
            if (event == null) {
            	// 💡 getHandler 得到 org.apache.coyote.AbstractProtocol$ConnectionHandler@a0e257a
            	/*
            	⚠️ 这个getHandler方法是AbstractEndpoint的方法,handler也是AbstractEndpoint的私有属性
            	这个handler私人属性是在AbstractEndpoint的构造方法中被设置进去的
            	*/ 
                state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
            } else {
                state = getHandler().process(socketWrapper, event);
            }
            if (state == SocketState.CLOSED) {
                poller.cancelledKey(getSelectionKey(), socketWrapper);
            }
        } else if (handshake == -1 ) {
            getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
            poller.cancelledKey(getSelectionKey(), socketWrapper);
        } else if (handshake == SelectionKey.OP_READ){
            socketWrapper.registerReadInterest();
        } else if (handshake == SelectionKey.OP_WRITE){
            socketWrapper.registerWriteInterest();
        }
    } catch (CancelledKeyException cx) {
        poller.cancelledKey(getSelectionKey(), socketWrapper);
    } catch (VirtualMachineError vme) {
        ExceptionUtils.handleThrowable(vme);
    } catch (Throwable t) {
        log.error(sm.getString("endpoint.processing.fail"), t);
        poller.cancelledKey(getSelectionKey(), socketWrapper);
    } finally {
        socketWrapper = null;
        event = null;
        //return to cache
        if (running && processorCache != null) {
            processorCache.push(this);
        }
    }
}

然后就是 org.apache.coyote.AbstractProtocol$ConnectionHandler@a0e257a

在这里插入图片描述

其中:

  • ConnectionHandler 通过 Processor(如: Http11Processor) 对请求字节码进行处理,形成connector内部的request

    💡 从socketWrapper中,通过Buffer形式读出请求头(request head)的主要看:
    org.apache.coyote.AbstractProtocol<S>SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) 方法

  • 交给Adapter,将connector内部的request转化成servlet能认识的request: getAdapter().service(request, response);
    (💡 下面介绍这部分)

# Adapter、Mapper

Adapter

Adapter 负责将 connector 内部的 request、response 封装成 servlet 能认识的 request、response,然后调用“责任链”(Pipeline、Valve)执行我们的应用代码

Mapper

Mapper 负责根据请求的路径,确定责任链中 Host、Context、Wrapper 部分的具体实例

执行流程(示意)

// 💡 Adapter
adapter instanceof CoyoteAdapter 

adapter.service()
	↳ adapter.postParseRequest(coyote_req, servlet_req, coyote_resp, servlet_resp)
		/*
		负责将 org.apache.coyote.Request
			   org.apache.coyote.Response
		转换为 org.apache.catalina.connector.Request implements HttpServletRequest
			   org.apache.catalina.connector.Response implements HttpServletResponse
		*/
		↳ connector.getService().getMapper().map(serverName, uri, version, servlet_req.getMappingData())
			/*
			💡 Mapper
			负责构建责任链中 Host、Context、Wrapper 的部分:确定这些对象的具体实例
			e.g. http://localhost:8080/test/ --> index.jsp
				StandardEngine[Catalina]
				StandardHost[localhost] <-- Host
				StandardContext[/test] <-- Context
				StandardWrapper[jsp] <-- Wrapper
			💡 数据存在mappingData,后面servlet_req包含此mappingData并从中获取相关信息
			*/internalMap(host, uri, version, mappingData)internalMapWrapper(contextVersion, path, mappingData)
	↳ connector.getService().getContainer().getPipeline().getFirst().invoke(servlet_req, servlet_resp);
		/*
		调用“责任链”(Pipline、Valve)
		*/
# Pipeline、Valve

在这里插入图片描述
tomcat中,组件各司其职,功能之间一不小心就会出现耦合。为了使组件间保持松耦合,确保整体架构的可伸缩性和可拓展性,在tomcat中,每个Container组件采用责任链模式来完成具体的请求处理。

在tomcat中定义了Pipleline、Valve两个接口:

  • Pipleline: 负责构建责任链

  • Valve: 负责责任链上的每个组件的具体处理(不同组件有不同的接口实现)

    在这里插入图片描述

执行流程(示意)

/* 
e.g. 访问目标
http://localhost:8080/test/
↓
connector.getService().getMapper().map(serverName, decodedURI,version, request.getMappingData());
CoyoteAdapter.postParseRequest
Mapper.internalMapWrapper // Rule 4c -- Welcome resources processing
↓
http://localhost:8080/test/index.jsp
*/

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)// StandardEngine[Catalina]
StandardEngineValve
↓
host.getPipeline().getFirst().invoke(request, response);(AccessLogValve)(ErrorReportValve)// StandardEngine[Catalina].StandardHost[localhost]
StandardHostValveContext context = request.getContext();
context.getPipeline().getFirst().invoke(request, response);(NonLoginAuthenticator)// StandardEngine[Catalina].StandardHost[localhost].StandardContext[/test]
StandardContextValve
↓
wrapper.getPipeline().getFirst().invoke(request, response);// StandardEngine[Catalina].StandardHost[localhost].StandardContext[/test].StandardWrapper[jsp]
StandardWrapperValve// org.apache.jasper.servlet.JspServlet@1fd4ffaf
servlet = wrapper.allocate();// Create the filter chain for this request
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
↓
filterChain.doFilter(request.getRequest(), response.getResponse());
# 配置分析

💡 以具体配置 ${tomcat_home}/conf/server.xml 配置文件为例:

接收: http://shop.myhost.com:8080/shop/detail?id=99861


<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
                                          找到服务器开放接口
                                                 ↓
    <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
    <Engine name="Catalina" defaultHost="localhost">
      <!-- 站点 -->
      <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">
        <Context docBase="F:\apache-tomcat-8.5.83\wtpwebapps\test-tomcat" path="/test-tomcat" reloadable="true" source="org.eclipse.jst.jee.server:test-tomcat"/>
      </Host>
                    找到对应的域名
                         ↓
      <Host name="shop.myhost.com"  appBase="webapps" unpackWARs="true" autoDeploy="true">
        <Context docBase="F:\apache-tomcat-8.5.83\wtpwebapps\pay" path="/pay" reloadable="true" source="org.eclipse.jst.jee.server:pay"/>
                                   进入对应项目名的web.xml文件      找到对应的项目名
                                             ↓                          ↓
        <Context docBase="F:\apache-tomcat-8.5.83\wtpwebapps\shop" path="/shop" reloadable="true" source="org.eclipse.jst.jee.server:shop"/>
      </Host>
    </Engine>
  </Service>
</Server>

${tomcat_home}/wtpwebapps/shop/WEB-INF/web.xml (springmvc)

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <!-- 加载spring-persist.xml配置文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-persist.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <!-- 加载spring-mvc.xml配置文件 -->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

	💡 根据 url-pattern 定位到名为 dispatcherServlet 的 servlet 
	💡 后续就交给 psring mvc 处理了,然后把响应再返回回来给 Context
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

	....    

</web-app>
# 代码分析

<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> 创建的 Connector 为例

根据启动流程的分析,启动时:

  • 调用 org.apache.catalina.connector.Connector 的 start() 方法调用 在 org.apache.tomcat.util.net.AbstractEndpoint

💡 提示

根据前面对Connector的实现类Coyote的分析,我们可以在 org.apache.tomcat.util.net.AbstractEndpoint.processSocket 下打断点:

在这里插入图片描述

配置

Tomcat 服务器的配置主要集中于 tomcat/conf 下的 catalina.policy、catalina.properties、context.xml、server.xml、tomcat-users.xml、web.xml 文件

# server.xml

server.xml 是tomcat 服务器的核心配置文件,包含了Tomcat的 Servlet 容器(Catalina)的所有配置。

<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<!-- Note:  A "Server" is not itself a "Container", so you may not
     define subcomponents such as "Valves" at this level.
     Documentation at /docs/config/server.html
 -->
 <!-- 💡 
 默认: org.apache.catalina.core.StandardServer
 port: Tomcat 监听的关闭服务器的端口
 shutdown: 关闭服务器的指令字符串
  -->
<Server port="8005" shutdown="SHUTDOWN">
  <!-- 💡 用于以日志形式输出服务器、操作系统、JVM的版本信息 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!-- APR library loader. Documentation at /docs/apr.html -->
  <!-- 💡 用于加载(服务器启动)和销毁(服务器停止)APR。如果找不到APR库,则会输出日志,并不影响Tomcat启动 -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <!-- 💡 用于避免JRE内存泄漏问题 -->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <!-- 💡 用户加载(服务器启动)和销毁(服务器停止)全局命名服务 -->
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <!-- 💡 用于在Context停止时重建Executor池中的线程,以避免ThreadLocal相关的内存泄漏 -->
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
  -->
  <GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <!-- A "Service" is a collection of one or more "Connectors" that share
       a single "Container" Note:  A "Service" is not itself a "Container",
       so you may not define subcomponents such as "Valves" at this level.
       Documentation at /docs/config/service.html
   -->
   <!-- 💡 
   默认: org.apache.catalina.core.StandardService 
   一个Server服务器,可以包含多个Service服务。
   在Service中,可以内嵌的元素: Listener、Executor、Connector、Engine
   其中 : 
	   + Listener: 为Service添加生命周期监听器
	   + Executor: 配置Service共享线程池
	   + Connector: 配置Service包含的链接器
	   + Engine: 配置Service中链接器对应的Servlet容器引擎
   -->
  <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"/>
    -->
	<!--
	💡 默认情况下,Service并未添加共享线程池配置。
		+ 如果不配置共享线程池,那么Catalina各组件在用到线程池时会独立创建。
		+ 如果我们想添加一个线程池,可以在下添加如下配置:
			<Executor name="tomcatThreadPool"
			namePrefix="catalina‐exec‐"
			maxThreads="200"
			minSpareThreads="100"
			maxIdleTime="60000"
			maxQueueSize="Integer.MAX_VALUE"
			prestartminSpareThreads="false"
			threadPriority="5"
			className="org.apache.catalina.core.StandardThreadExecutor"/>
			💡 属性说明:
		 	+ name 线程池名称,用于 Connector 中指定。
		 	+ namePrefix 所创建的每个线程的名称前缀,一个单独的线程名称为 namePrefix+threadNumber。
		 	+ maxThreads 池中最大线程数。
		 	+ minSpareThreads 活跃线程数,也就是核心池线程数,这些线程不会被销毁,会一直存在。
		 	+ maxIdleTime 线程空闲时间,超过该时间后,空闲线程会被销毁,默认值为6000(1分钟),单位毫秒。
		 	+ maxQueueSize 在被执行前最大线程排队数目,默认为Int的最大值,也就是广义的无限。除非特殊情况,这个值不需要更改,否则会有请求不会被处理的情况发生。
		 	+ prestartminSpareThreads 启动线程池时是否启动 minSpareThreads 部分线程。默认值为false,即不启动。
		 	+ threadPriority 线程池中线程优先级,默认值为5,值从1到10。
		 	+ className 线程池实现类,未指定情况下,默认实现类为 org.apache.catalina.core.StandardThreadExecutor。 如果想使用自定义线程池首先需要实现 org.apache.catalina.Executor接口。
	-->
	<!--
	💡 可以通过jdk提供的工具: jconsole 查看tomcat本地进程信息(内存、线程)
	-->


    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
    -->
    <!-- 
    💡 Connector用于创建链接器实例。
    默认情况下,server.xml配置了两个链接器,一个支持HTTP协议,一个支持AJP协议。(这个版本好像没开)
    (因此,大多数情况下,我们并不需要新增链接器配置,只是根据需要对已有的链接器进行优化)
    -->
    <!--
    💡 属性说明
    1. port: 端口号,Connector用于创建服务端Socket并进行监听,以等待客户端请求链接。如果该属性设置为0,Tomcat将会随机选择一个可用的端口号给当前Connector使用。
    2. protocol: 当前Connector支持的访问协议。默认为HTTP/1.1,并采用自动切换机制选择一个基于JAVA NIO的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)
    3. connectionTimeOut: Connector接收链接后的等待超时时间,单位为毫秒。-1表示不超时。
    4. redirectPort: 当前Connector不支持SSL请求,接收到了一个请求,并且也security-constraint约束,需要SSL传输,Catalina自动将请求重定向到指定的端口。
    5. executor: 指定共享线程池的名字,也可以是通过maxThreads、minSpareThreads等属性配置内部线程池。
    6. URIEncoding: 用于指定编码URI的字符编码,Tomcat8.x版本默认的编码为UTF-8,Tomcat7.x版本默认为ISO-8859-1。
	-->
	<!--
	💡 完整示例:
		<Connector port="8080"
		protocol="HTTP/1.1"
		executor="tomcatThreadPool"
		maxThreads="1000"
		minSpareThreads="100"
		acceptCount="1000"
		maxConnections="1000"
		connectionTimeout="20000"
		compression="on"
		compressionMinSize="2048"
		disableUploadTimeout="true"
		redirectPort="8443"
		URIEncoding="UTF‐8" />
    -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- A "Connector" using the shared thread pool-->
    <!--
    <Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    -->
    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443
         This connector uses the NIO implementation. The default
         SSLImplementation will depend on the presence of the APR/native
         library and the useOpenSSL attribute of the AprLifecycleListener.
         Either JSSE or OpenSSL style configuration may be used regardless of
         the SSLImplementation selected. JSSE style configuration is used below.
    -->
    <!--
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
               maxThreads="150" SSLEnabled="true">
        <SSLHostConfig>
            <Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>
    -->
    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
         This connector uses the APR/native implementation which always uses
         OpenSSL for TLS.
         Either JSSE or OpenSSL style configuration may be used. OpenSSL style
         configuration is used below.
    -->
    <!--
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
               maxThreads="150" SSLEnabled="true" >
        <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
        <SSLHostConfig>
            <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                         certificateFile="conf/localhost-rsa-cert.pem"
                         certificateChainFile="conf/localhost-rsa-chain.pem"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>
    -->

    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <!--
    <Connector protocol="AJP/1.3"
               address="::1"
               port="8009"
               redirectPort="8443" />
    -->

    <!-- An Engine represents the entry point (within Catalina) that processes
         every request.  The Engine implementation for Tomcat stand alone
         analyzes the HTTP headers included with the request, and passes them
         on to the appropriate Host (virtual host).
         Documentation at /docs/config/engine.html -->

    <!-- You should set jvmRoute to support load-balancing via AJP ie :
    <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
    -->
    <!--
    💡 Engine 作为Servlet 引擎的顶级元素,内部可以嵌入: Cluster、Listener、Realm、
Valve和Host。
	1. name: 用于指定Engine的名称,默认Catalina。该名称会影响Tomcat的一部分文件的存储路径。(如临时文件、jsp编译文件)
	2. defaultHost: 默认使用的虚拟主机名称,当客户端请求指向的主机无效时,将交由默认的虚拟主机处理,默认为localhost
    -->
    <Engine name="Catalina" defaultHost="localhost">

      <!--For clustering, please take a look at documentation at:
          /docs/cluster-howto.html  (simple how to)
          /docs/config/cluster.html (reference documentation) -->
      <!--
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
      -->

      <!-- Use the LockOutRealm to prevent attempts to guess user passwords
           via a brute-force attack -->
      <!--
      💡
 	  如果在Engine下配置了Realm,那么此配置在当前Engine下的所有Host中共享。
	  同样,如果在Host中配置了Realm,则在当前Host下的所有Context中共享。
	  优先级: Context>Host>Engine
      -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <!-- This Realm uses the UserDatabase configured in the global JNDI
             resources under the key "UserDatabase".  Any edits
             that are performed against this UserDatabase are immediately
             available for use by the Realm.  -->
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

	  <!--
	  💡 Host元素用于配置一个虚拟机,它支持下面嵌入元素: Alias、Cluster、Listener、Valve、Realm、Context。
	  属性说明:
	  1. name: 当前Host通用的网络名称,必须与DNS服务器上注册的信息一致。Engine中包含的Host必须存在一个名称与Engine的defaultHost设置一致。
	  2. appBase: 当前Host应用基础目录,当前Host上部署的Web应用均在该目录下(可以是绝对末路,相对路径)。默认为webapps
	  3. unpackWARs: 设置为true,Host在启动时会将appBase目录下war包解压为目录。设置为false,Host将直接从war文件启动。
	  4. autoDeploy: 控制tomcat是否在运行时定期检查并自动部署新增或更改的web应用
	  -->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

		<!-- 
		💡 Context 用于配置一个Web应用
		属性描述:
		1. docBase: Web应用目录或者War包的部署路径。可以是绝对路径,也可以是相对于Host appBase的相对路径。
		2. path: Web应用的Context路径。如果我们Host名为localhost, 则该web应用访问的根路径为: http://localhost:8080/myApp。

		它支持的内嵌元素为:CookieProcessor, Loader, Manager,Realm,Resources,
WatchedResource,JarScanner,Valve。
		-->
		<Context docBase="myApp" path="/myApp">
		....
		</Context>

      </Host>
    </Engine>
  </Service>
</Server>
# web.xml

web.xml 是web应用的描述文件, 它支持的元素及属性来自于 Servlet 规范定义 。 在 Tomcat 中, Web 应用的描述信息包括 tomcat/conf/web.xml 中默认配置以及 Web 应用 WEB-INF/web.xml 下的定制配置

参考: http://xmlns.jcp.org/xml/ns/javaee/

context‐param (ServletContext 初始化参数)

我们可以通过 添加ServletContext 初始化参数,它配置了一个键值对,这样我们可以在应用程序中使用 javax.servlet.ServletContext.getInitParameter()方法获取参数。

<context‐param>
	<param‐name>contextConfigLocation</param‐name>
	<param‐value>classpath:applicationContext‐*.xml</param‐value>
	<description>Spring Config File Location</description>
</context‐param>
session‐config (会话配置)

用于配置Web应用会话,包括 超时时间、Cookie配置以及会话追踪模式。它将覆盖 server.xml 和 context.xml 中的配置。

<session‐config>
	<!-- 💡 会话超时时间,单位 分钟 -->
	<session‐timeout>30</session‐timeout>
	<!-- 💡 用于配置会话追踪Cookie -->
	<cookie‐config>
		<name>JESSIONID</name>
		<domain>www.itcast.cn</domain>
		<path>/</path>
		<comment>Session Cookie</comment>
		<!-- 💡 此cookie只能通过HTTP方式进行访问,JS无法读取或修改,此项可以增加网站访问的安全性。 -->
		<http‐only>true</http‐only>
		<!-- 💡 此cookie只能通过HTTPS连接传递到服务器,而HTTP连接则不会传递该信息。⚠️注意是从浏览器传递到服务器,服务器端的Cookie对象不受此项影响。 -->
		<secure>false</secure>
		<!-- 💡 cookie的生存期,以秒为单位,默认为‐1表示是会话Cookie,浏览器关闭时才会消失。 -->
		<max‐age>3600</max‐age>
	</cookie‐config>
	<!-- 💡 用于配置会话追踪模式,Servlet3.0版本中支持的追踪模式:COOKIE、URL、SSL
		1. COOKIE: 通过HTTP Cookie追踪会话是最常用的会话追踪机制, 而且Servlet规范也要求所有的Servlet规范都需要支持Cookie追踪。
		2. URL: URL重写是最基本的会话追踪机制。当客户端不支持Cookie时,可以采用URL重写的方式。当采用URL追踪模式时,请求路径需要包含会话标识信息,Servlet容器会根据路径中的会话标识设置请求的会话信息。如:http://www.myserver.com/user/index.html;jessionid=1234567890。
		3. SSL: 对于SSL请求, 通过SSL会话标识确定请求会话标识。
	 -->
	<tracking‐mode>COOKIE</tracking‐mode>
</session‐config>
servlet、servlet-mapping (Servlet配置)
<servlet>
	<servlet‐name>myServlet</servlet‐name>
	<servlet‐class>cn.itcast.web.MyServlet</servlet‐class>
	<init‐param>
		<param‐name>fileName</param‐name>
		<param‐value>init.conf</param‐value>
	</init‐param>
	<!-- 控制在Web应用启动时,Servlet的加载顺序。 值小于0,web应用启动时,不加载该servlet, 第一次访问时加载。 -->
	<load‐on‐startup>1</load‐on‐startup>
	<!--  若为false,表示Servlet不处理任何请求。 -->
	<enabled>true</enabled>
</servlet>
<servlet‐mapping>
	<servlet‐name>myServlet</servlet‐name>
	<url‐pattern>*.do</url‐pattern>
	<url‐pattern>/myservet/*</url‐pattern>
</servlet‐mapping>
servlet (文件上传配置)
<servlet>
	<servlet‐name>uploadServlet</servlet‐name>
	<servlet‐class>cn.itcast.web.UploadServlet</servlet‐class>
	<multipart‐config>
		<!-- 存放生成的文件地址 -->
		<location>C://path</location>
		<!-- 允许上传的文件最大值。默认值为‐1,表示没有限制。 -->
		<max‐file‐size>10485760</max‐file‐size>
		<!-- 针对该 multi/form‐data 请求的最大数量,默认值为‐1, 表示无限制。 -->
		<max‐request‐size>10485760</max‐request‐size>
		<!-- 当数量量大于该值时, 内容会被写入文件。 -->
		<file‐size‐threshold>0</file‐size‐threshold>
	</multipart‐config>
</servlet>
listener (Listener配置)

Listener用于监听servlet中的事件,例如context、request、session对象的创建、修改、删除,并触发响应事件。Listener是观察者模式的实现,在servlet中主要用于对context、request、session对象的生命周期进行监控。在servlet2.5规范中共定义了8中Listener。在启动时,ServletContextListener 的执行顺序与web.xml 中的配置顺序一致, 停止时执行顺序相反。

<listener>
	<listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
</listener>
filter、filter‐mapping(Filter配置)

filter 用于配置web应用过滤器, 用来过滤资源请求及响应。 经常用于认证、日志、加密、数据转换等操作, 配置如下:

<filter>
	<filter‐name>myFilter</filter‐name>
	<filter‐class>cn.itcast.web.MyFilter</filter‐class>
	<!-- 💡 该过滤器是否支持异步 -->
	<async‐supported>true</async‐supported>
	<init‐param>
		<param‐name>language</param‐name>
		<param‐value>CN</param‐value>
	</init‐param>
</filter>
<filter‐mapping>
	<filter‐name>myFilter</filter‐name>
	<url‐pattern>/*</url‐pattern>
</filter‐mapping>
welcome‐file‐list (迎页面配置)

尝试请求的顺序,从上到下。

<welcome‐file‐list>
	<welcome‐file>index.html</welcome‐file>
	<welcome‐file>index.htm</welcome‐file>
	<welcome‐file>index.jsp</welcome‐file>
</welcome‐file‐list>
error‐page (错误页面配置)

error-page 用于配置Web应用访问异常时定向到的页面,支持HTTP响应码和异常类两种形式。

<error‐page>
	<error‐code>404</error‐code>
	<location>/404.html</location>
</error‐page>
<error‐page>
	<error‐code>500</error‐code>
	<location>/500.html</location>
</error‐page>
<error‐page>
	<exception‐type>java.lang.Exception</exception‐type>
	<location>/error.jsp</location>
</error‐page>

配置(管理)

从早期的Tomcat版本开始,就提供了Web版的管理控制台,他们是两个独立的Web应用,位于webapps目录下。

  • 用于管理的Host的host-manager
  • 用于管理Web应用的manager
# host-manager (管理host)

Tomcat启动之后,可以通过 http://localhost:8080/host-manager/html 访问该Web应用。

💡 提示

所谓host管理,是指域名与web应用的映射关系

而web引用则是由下面要提到的manager进行管理

host-manager 默认添加了访问权限控制,当打开网址时,需要输入用户名和密码(conf/tomcat-users.xml中配置) 。所以要想访问该页面,需要在conf/tomcatusers.xml 中配置,并分配对应的角色:

<!-- 用于控制页面访问权限 -->
<role rolename="admin‐gui"/>
<!-- 用于控制以简单文本的形式进行访问 -->
<role rolename="admin‐script"/>
<user username="tomcat" password="123321" roles="admin‐script,admin‐gui"/>
# manager (管理web应用)

manager的访问地址为 http://localhost:8080/manager, 同样, manager也添加了页面访问控制,因此我们需要为登录用户分配角色为:

<role rolename="manager‐gui"/>
<role rolename="manager‐script"/>
<user username="itcast" password="itcast" roles="admin‐script,admin‐gui,manager‐gui,manager‐script"/>

💡 提示

所谓web应用的管理,是管理其域名(host)后面的服务、服务名

同时,可以对系统的信息进行查看(内存、线程、软件版本)

在这里插入图片描述

配置(JVM)

# 内存调整

最常见的JVM配置当属内存配置,因为在大多数情况下,JVM默认分配的内存可能不能满足我们的需求,特别是在生产环境,此时需要手动修改Tomcat启动时的内存参数分配。

JVM内存模型图

在这里插入图片描述

JVM配置选项:

  • windows 平台(catalina.bat):

    set JAVA_OPTS=‐server ‐Xms2048m ‐Xmx2048m ‐XX:MetaspaceSize=256m ‐XX:MaxMetaspaceSize=256m ‐XX:SurvivorRatio=8
    
  • linux 平台(catalina.sh):

    JAVA_OPTS="‐server ‐Xms1024m ‐Xmx2048m ‐XX:MetaspaceSize=256m ‐XX:MaxMetaspaceSize=512m ‐XX:SurvivorRatio=8"
    
    参数含义优化建议
    -server启动Server,以服务端模式运行服务端模式建议开启
    -Xms堆内存的初始大小建议与-Xmx设置相同
    -Xmx堆内存的最大大小建议设置为可用内存的80%
    -Xmn新生代的内存大小,官方建议是整个堆得3/8。
    -XX:MetaspaceSize元空间内存初始大小, 在JDK1.8版本之前配置为 -XX:PermSize(永久代)
    -XX:MaxMetaspaceSize元空间内存最大大小, 在JDK1.8版本之前配置为 -XX:MaxPermSize(永久代)默认无限
    -XX:InitialCodeCacheSize
    -XX:ReservedCodeCacheSize
    代码缓存区大小
    -XX:MaxNewSize新生代最大内存默认16M
    -XX:NewRatio年轻代和老年代大小比值,取值为整数,默认为2
    设置新生代和老年代的相对大小比例。这种方式的优点是新生代大小会随着整个堆大小动态扩展。如 -XX:NewRatio=3 指定老年代/新生代为 3/1。 老年代占堆大小的 3/4,新生代占 1/4 。
    不建议修改
    -XX:SurvivorRatioEden区与Survivor区大小的比值,取值为整数,默认为8
    指定伊甸园区 (Eden) 与幸存区大小比例。如: -XX:SurvivorRatio=10 表示伊甸园区 (Eden)是 幸存区 To 大小的 10 倍 (也是幸存区 From的 10 倍)。 所以, 伊甸园区 (Eden) 占新生代大小的 10/12, 幸存区 From 和幸存区 To 每个占新生代的 1/12 。 注意, 两个幸存区永远是一样大的。
    不建议修改
# 垃圾回收(GC)策略

JVM垃圾回收性能有以下两个主要的指标

  • 吞吐量: 工作时间(排除GC时间)占总时间的百分比,工作时间不仅是程序运行的时间,还包含内存分配时间。
  • 暂停时间: 测试时间段内,由垃圾回收导致的应用程序停止响应次数/时间

在Sun公司推出的HotSpotJVM中,包含以下几种不同类型的垃圾回收器:

垃圾收集器含义说明
串行收集器 (Serial Collector)采用单线程执行所有的垃圾回收工作,适用于单核CPU服务器,无法利用多核硬件的优势
并行收集器 (Parallel Collector)吞吐量收集器,以并行的方式执行年轻代的垃圾回收,该方式可以显著降低垃圾回收的开销(指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态)。适用于多处理器或多线程硬件上运行的数据量较大的应用
并发收集器 (Concurrent Collector)以并发的方式执行大部分垃圾回收工作,以缩短垃圾回收的暂停时间。适用于那些响应时间优于吞吐量的应用,因为该收集器虽然最小化了暂停时间(指用户线程与垃圾收集线程同时执行,但不一定是并行的,可能会交替进行),但是会降低应用程序的性能
CMS收集器 (Concurrent Mark Sweep Collector)并发标记清除收集器,适用于那些更愿意缩短垃圾回收暂停时间,并且负担的起与垃圾回收共享处理资源的应用
G1收集器 (Garbage-First Garbage Collector)适用于大容量内存的多核服务器,可以在满足垃圾回收暂停时间目标的同时,以最大可能性实现高吞吐量(JDK1.7之后)

不同的应用程序,对于垃圾回收会有不同的需求。JVM会根据运行的平台、服务器资源配置情况选择合适的垃圾收集器、堆内存大小及运行时编译器。如果无法满足需求,参考以下准则:

  1. 程序数据量较小,选择串行收集器
  2. 应用运行在单核处理器上且没有暂停时间要求,可交由JVM自行选择或者选择串行收集器
  3. 如果考虑应用程序的峰值性能,没有暂停时间要求,可以选择并行收集器
  4. 如果应用程序的响应时间比整体吞吐量更重要,可以选择并发收集器

在这里插入图片描述

# 工具: jconsole(查看JVM情况)

查看Tomcat中的默认的垃圾收集器:

  1. 在tomcat/bin/catalina.sh的配置中加入如下配置
    在这里插入图片描述

  2. 打开jconsole,查看远程的tomcat的概要信息
    在这里插入图片描述
    在这里插入图片描述

GC参数:

参数描述
-XX:+UseSerialGC启用串行收集器
-XX:+UseParallelGC启用并行垃圾收集器。
如果配置了该选项,那么 -XX:+UseParallelOldGC 默认启用
-XX:+UseParallelOldGCFullGC采用并行收集,默认禁用。
如果设置了-XX:+UseParallelGC 则自动启动
-XX:+ParallelGCThreads年轻代及老年代垃圾回收使用的线程数。默认值依赖于JVM使用的CPU个数
-XX:+UseConcMarkSweepGC对于老年代启用CMS垃圾收集器。当并收集器无法满足应用的延迟需求时,推荐使用CMS或G1收集器
启用该选项后, -XX:+UseParNewGC 自动启用
-XX:+UseParNewGC年轻代采用并行收集器
如果设置了 -XX:+UseConcMarkSweepGC 选项,则自动启用
-XX:+UseG1GC启用G1收集器。G1是服务器类型的收集器,用于多核、大内存的机器。它在保持高吞吐量的情况下,高概率满足GC暂停时间的目标

我们也可以在测试的时候,将JVM参数调整后,将GC的信息打印出来:

选项描述
-XX:+PrintGC打印每次GC的信息
-XX:+PrintGCApplicationConcurrentTime打印最后一次暂停之后所经过的时间,即响应并发执行的时间
-XX:+PrintGCApplicationStoppedTime打印GC时应用暂停时间
-XX:+PringGCDateStamps打印每次GC的日期戳
-XX:+PrintGCDetails打印每次GC的详细信息
-XX:+PrintGCTaskTimeStamps打印每个GC工作线程任务的时间戳
-XX:+PrintGCTimeStamps打印每次GC的时间戳

在bin/catalina.sh的脚本中追加:

JAVA_OPTS="-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails ...."

配置(集群)

由于单台Tomcat的承载能力是有限的,当我们的业务系统用户量比较大,请求压力比较大时,单台Tomcat是扛不住的,这个时候,就需要搭建Tomcat的集群,而目前比较流程的做法就是通过Nginx来实现Tomcat集群的负载均衡。

在这里插入图片描述

# 案例:负载均衡、session共享

在服务器上, 安装两台tomcat, 然后分别改Tomcat服务器的端口号 :

更改端口号:
关闭指令端口:       8005 ‐‐‐‐‐‐‐‐‐> 8015 ‐‐‐‐‐‐‐‐‐> 8025
(http)服务端口:   8080 ‐‐‐‐‐‐‐‐‐> 8888 ‐‐‐‐‐‐‐‐‐> 9999
(ajp)服务端口:    8009 ‐‐‐‐‐‐‐‐‐> 8019 ‐‐‐‐‐‐‐‐‐> 8029

配置Nginx, 配置nginx.conf:

upstream serverpool{
	server localhost:8888;
	server localhost:9999;
	# 💡 weight参数用于指定轮询几率,weight的默认值为1
	# server localhost:8888 weight=3;
	# server localhost:9999 weight=1;
	# 💡 ip_hash 指定负载均衡器按照基于客户端IP的分配方式,这个方法确保了相同的客户端的请求一直发送到相同的服务器,以保证session会话。这样每个访客都固定访问一个后端服务器,
	# 可以解决session不能跨服务器的问题。
	# ip_hash;
}
server {
	listen 99;
	server_name localhost;
	location / {
		proxy_pass http://serverpool/;
	}
}
参数描述
fail_timeout与max_fails结合使用
max_fails设置在fail_timeout参数设置的时间内最大失败次数,如果在这个时间内,所有针对该服务器的请求都失败了,那么认为该服务器会被认为是停机了
fail_time服务器会被认为停机的时间长度,默认为10s
backup标记该服务器为备用服务器。当主服务器停止时,请求会被发送到它这里
down标记服务器永久停机了

session共享

  1. ip_hash

  2. session复制 (参考: https://tomcat.apache.org/tomcat-8.5-doc/cluster-howto.html)\

    • 在Tomcat的conf/server.xml 配置如下:
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
      
    • 在Tomcat部署的应用程序 servlet_demo01 的web.xml 中加入如下配置 :
      <distributable/>
      

    💡 提示

    适用于较小的集群环境(节点数不超过4个)
    如果集群的节点数比较多的话,通过这种广播的形式来完成Session的复制,会消耗大量的网络带宽,影响服务的性能。
    在这里插入图片描述

  3. 单点登录(Single Sign On),简称为 SSO

    💡 提示

    SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,也是用来解决集群环境Session共享的方案之一 。
    在这里插入图片描述

配置(安全)

# 配置
  1. 删除webapps目录下的所有文件,禁用tomcat管理界面;

  2. 注释或删除tomcat-users.xml文件内的所有用户权限;

  3. 更改关闭tomcat指令或禁用;

    💡 提示

    tomcat的server.xml中定义了可以直接关闭 Tomcat 实例的管理端口(默认8005)。可以通过 telnet 连接上该端口之后,输入 SHUTDOWN (此为默认关闭指令)即可关闭Tomcat 实例(注意,此时虽然实例关闭了,但是进程还是存在的)。由于默认关闭Tomcat 的端口和指令都很简单。默认端口为8005,指令为SHUTDOWN 。

    • 方案一:

      更改端口号和指令:
      <Server port="8456" shutdown="itcast_shut">
      
    • 方案二:

      禁用8005端口:
      <Server port="‐1" shutdown="SHUTDOWN">
      
  4. 定义错误页面

    在webapps/ROOT目录下定义错误页面 404.html,500.html;
    然后在tomcat/conf/web.xml中进行配置 , 配置错误页面:

    <error‐page>
    	<error‐code>404</error‐code>
    	<location>/404.html</location>
    </error‐page>
    <error‐page>
    	<error‐code>500</error‐code>
    	<location>/500.html</location>
    </error‐page>
    

    这样配置之后,用户在访问资源时出现404,500这样的异常,就能看到我们自定义的错误页面,而不会看到异常的堆栈信息,提高了用户体验,也保障了服务的安全性。

# 应用 (权限)

在大部分的Web应用中,特别是一些后台应用系统,都会实现自己的安全管理模块(权限模块),用于控制应用系统的安全访问,基本包含两个部分:认证(登录/单点登录)和授权(功能权限、数据权限)两个部分。对于当前的业务系统,可以自己做一套适用于自己业务系统的权限模块,也有很多的应用系统直接使用一些功能完善的安全框架,将其集成到我们的web应用中,如:SpringSecurity、Apache Shiro等。

# 传输 (https)

HTTPS的全称是超文本传输安全协议(Hypertext Transfer Protocol Secure),是一种网络安全传输协议。在HTTP的基础上加入SSL/TLS来进行数据加密,保护交换数据不被泄露、窃取。

SSL 和 TLS 是用于网络通信安全的加密协议,它允许客户端和服务器之间通过安全链接通信。SSL 协议的3个特性:

  1. 保密:通过SSL链接传输的数据时加密的。
  2. 鉴别:通信双方的身份鉴别,通常是可选的,单至少有一方需要验证。
  3. 完整性:传输数据的完整性检查。

从性能角度考虑,加解密是一项计算昂贵的处理,因为尽量不要将整个Web应用采用SSL链接, 实际部署过程中, 选择有必要进行安全加密的页面(存在敏感信息传输的页面)采用SSL通信。

HTTPS和HTTP的区别主要为以下四点:

  1. HTTPS协议需要到证书颁发机构CA申请SSL证书, 然后与域名进行绑定,HTTP不用申请证书;
  2. HTTP是超文本传输协议,属于应用层信息传输,HTTPS 则是具有SSL加密传安全性传输协议,对数据的传输进行加密,相当于HTTP的升级版;
  3. HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是8080,后者是8443。
  4. HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。

HTTPS协议优势:

  1. 提高网站排名,有利于SEO。谷歌已经公开声明两个网站在搜索结果方面相同,如果一个网站启用了SSL,它可能会获得略高于没有SSL网站的等级,而且百度也表明对安装了SSL的网站表示友好。因此,网站上的内容中启用SSL都有明显的SEO优势。
  2. 隐私信息加密,防止流量劫持。特别是涉及到隐私信息的网站,互联网大型的数据泄露的事件频发发生,网站进行信息加密势在必行。
  3. 浏览器受信任。 自从各大主流浏览器大力支持HTTPS协议之后,访问HTTP的网站都会提示“不安全”的警告信息。

Tomcat支持HTTPS

  1. 生成秘钥库文件。

    keytool ‐genkey ‐alias tomcat ‐keyalg RSA ‐keystore tomcatkey.keystore
    

    输入对应的密钥库密码, 秘钥密码等信息之后,会在当前文件夹中出现一个秘钥库文件:tomcatkey.keystore

  2. 将秘钥库文件 tomcatkey.keystore 复制到tomcat/conf 目录下。

  3. 配置tomcat/conf/server.xml

    <Connector port="8443" 
    	protocol="org.apache.coyote.http11.Http11NioProtocol"
    	maxThreads="150" schema="https" secure="true" SSLEnabled="true">
    	<SSLHostConfig certificateVerification="false">
    		<Certificate
    			certificateKeystoreFile="D:/DevelopProgramFile/apache‐tomcat‐8.5.42‐windows‐x64/apache‐tomcat‐8.5.42/conf/tomcatkey.keystore"
    			certificateKeystorePassword="itcast" type="RSA" />
    	</SSLHostConfig>
    </Connector>
    
  4. 访问Tomcat ,使用https协议。

配置(性能)

对于系统性能,用户最直观的感受就是系统的加载和操作时间,即用户执行某项操作的耗时。从更为专业的角度上讲,性能测试可以从以下两个指标量化。

  1. 响应时间:如上所述,为执行某个操作的耗时。大多数情况下,我们需要针对同一个操作测试多次,以获取操作的平均响应时间。
  2. 吞吐量:即在给定的时间内,系统支持的事务数量,计算单位为 TPS。

通常情况下,我们需要借助于一些自动化工具来进行性能测试,因为手动模拟大量用户的并发访问几乎是不可行的,而且现在市面上也有很多的性能测试工具可以使用,如:ApacheBench、ApacheJMeter、WCAT、WebPolygraph、LoadRunner。

我们课程上主要介绍两款免费的工具:ApacheBench。

# ApacheBench

ApacheBench(ab)是一款ApacheServer基准的测试工具,用户测试Apache Server的服务能力(每秒处理请求数),它不仅可以用户Apache的测试,还可以用于测试Tomcat、Nginx、lighthttp、IIS等服务器。

  1. 安装

    yum install httpd‐tools
    
  2. 查看版本号

    ab ‐V
    
  3. 部署war包, 准备环境

    A. 在Linux系统上安装Tomcat
    	上传 : alt + p ‐‐‐‐‐‐‐> put D:/apache‐tomcat‐8.5.42.tar.gz
    	解压 : tar ‐zxvf apache‐tomcat‐8.5.42.tar.gz ‐C /usr/local
    	修改端口号:8005 , 80808009
    B. 将资料中的war包上传至Tomcat的webapps下
    	上传: alt + p ‐‐‐‐‐‐‐‐‐> put D:/ROOT.war
    	启动Tomcat解压
    C. 导入SQL脚本 , 准备环境
    
  4. 测试性能

    ab ‐n 1000 ‐c 100 ‐p data.json ‐T application/json http://localhost:9000/course/search.do?page=1&pageSize=10
    

    参数说明

    参数含义描述
    -n在测试会话中所执行的请求个数,默认只执行一次请求
    -c一次产生的请求个数,默认一次一个
    -p包含了需要POST的数据文件
    -t测试所进行的最大秒数,默认没有时间限制
    -TPOST数据所需要使用的Content-Type头信息
    -v设置显示信息的详细程度
    -w以HTML表的格式输出结果,默认是白色背景的两列宽度的一张表

    结果说明

    指标含义
    Server Software服务器软件
    Server Hostname主机名
    Server Port端口号
    Document Path测试的页面
    Document Length测试的页面大小
    Concurrency Level并发数
    Time taken for tests整个测试持续的时间
    Complete requests完成的请求数量
    Failed requests失败的请求数量,这里的失败是指请求的连接服务器、发送数据、接收数据等环节发生异常,以及无响应后超时的情况。
    Write errors输出错误数量
    Total transferred整个场景中的网络传输量,表示所有请求的响应数据长度总和,包括每个http响应数据的头信息和正文数据的长度。
    HTML transferred整个场景中的HTML内容传输量,表示所有请求的响应数据中正文数据的总和
    Requests per second每秒钟平均处理的请求数(相当于 LR 中的 每秒事务数)这便是我们重点关注的吞吐率,它等于:Complete requests / Time taken for tests
    Time per request每个线程处理请求平均消耗时间(相当于 LR 中的 平均事务响应时间)用户平均请求等待时间
    Transfer rate平均每秒网络上的流量
    Percentage of the requests served within a certain time (ms)指定时间里,执行的请求百分比

    重要指标

    参数指标说明
    Requests per second吞吐率:服务器并发处理能力的量化描述,单位是reqs/s,指的是在某个并发用户数下单位时间内处理的请求数。某个并发用户数下单位时间内能处理的最大请求数,称之为最大吞吐率。这个数值表示当前机器的整体性能,值越大越好。
    Time per request用户平均请求等待时间:从用户角度看,完成一个请求所需要的时间
    Time per request:across all concurrent requests服务器平均请求等待时间:服务器完成一个请求的时间
    Concurrency Level并发用户数
# Tomcat性能配置(Connector)

调整server.xml中关于Connector的配置可以提升应用服务器的性能

参数说明
maxConnection最大连接数,当达到该值后,服务器接收但不会处理更多的请求,额外的请求将会阻塞直到连接数量低于maxConnections。
可以通过 ulimit -a 查看服务器限制。
对于CPU要求更高(计算型)时,建议不要配置过大;对于CPU要求不是特别高时,建议配置在2000左右。(当然,这个需要服务器硬件的支持)
maxThreads最大线程数,需要根据服务器的硬件情况,进行一个合理的设置
acceptCount最大排队等待数,当服务器接收的请求达到maxConnections,此时Tomcat会将后面的请求存放在任务队列中进行排序,acceptCount指的就是任务队列中排队等待的请求数。一台Tomcat的最大的请求处理数量,是maxConnections+acceptCount

其他组件

# Jasper

参考: https://lawsssscat.blog.csdn.net/article/details/103598260

# WebSocket ⭐️

参考: https://blog.csdn.net/LawssssCat/article/details/103494277

日志

https://tomcat.apache.org/tomcat-8.5-doc/logging.html

tomcat 内部日志用的是JULI(a packaged renamed fork of Apache Commons Logging that is hard-coded to use the java.util.logging framework)。

作为tomcat的一个web应用可以使用一下方法记录日志:

  • 任何一个日志框架
    Use any logging framework of its choice.
  • 系统的日志api (tomcat内部使用的,正是依照其实现的JULI日志框架)
    Use system logging API, java.util.logging.
  • servlet提供的日志api
    Use the logging API provided by the Java Servlets specification, javax.servlet.ServletContext.log(...)
# Java logging API — java.util.logging (默认)

默认实现的功能非常有限,最重要的问题是无法按web应用记录日志(因为其配置是每个vm共享的)

因此,tomcat 的 LogManager 使用了更友好的实现 JULI

JULI
  • JULI 正是依据 java.util.logging 实现的。
  • 它最主要的组件是一个自定义的LogManager实现(a custom LogManager implementation)
  • 允许一个应用一个日志配置
    (应用装载、卸载都能收到通知)
    (卸载时通知,能清除类引用,防止内存泄漏)

JULI 使用了标准JDK java.util.logging 一样的配置机制(configuration mechanisms):要么使用编码的方式(a programmatic approach)、要么使用配置文件(properties files)

使用配置文件方式

  • 配置文件一般在 ${catalina.base}/conf/logging.properties ,也可以在启动的脚本中使用系统参数(System Property) java.util.logging.config.file 来指定
  • 如果上述位置找不到,就会到 ${java.home}/lib/logging.properties 中找
  • 对于web应用,配置文件在 WEB-INF/classes/logging.properties

todo … https://tomcat.apache.org/tomcat-8.5-doc/logging.html#Using_java.util.logging_(default)

# Servlets logging API

使用 javax.servlet.ServletContext.log(...) 方法记录的日志是tomcat内部的日志

其格式如下:

org.apache.catalina.core.ContainerBase.[${engine}].[${host}].[${context}]

💡 提示

  • 你可以在 ${CATALINA_HOME}/conf/logging.properties 中找到默认配置,并修改
  • 如果只是单独为一个实例配置,就可以把修改的内容放在这个实例的 ${CATALINA_BASE}/conf/logging.properties 下

💡 提示

这个api年代久远,所以有很多(现代看起来理所当然的)功能没有:

  • 不能随意控制level:
    level方法
    INFOServletContext.log(String)
    INFO GenericServlet.log(String)
    SERVER ServletContext.log(String, Throwable)
    SERVERGenericServlet.log(String, Throwable)
# Console - catalina.out

控制台日志将输出到 catalina.out (无论是 System.out/err

  • 未捕获的日志 java.lang.ThreadGroup.uncaughtException(..)
  • 线程快照(thread dumps),如果你希望通过系统型号获得它们

💡 提示

可以通过环境变量 进行更改

# CATALINA_OUT    (Optional) Full path to a file where stdout and stderr
#                   will be redirected.
#                   Default is $CATALINA_BASE/logs/catalina.out

变量定义在 bin/catalina.sh 脚本中

💡 提示

修改contextswallowOutput 属性为true,可以使 System.out/err的内容输出到javax.servlet.ServletContext.log(...)

但这些技术都是过时的了
Note, that the swallowOutput feature is actually a trick, and it has its limitations. It works only with direct calls to System.out/err, and only during request processing cycle. It may not work in other threads that might be created by the application. It cannot be used to intercept logging frameworks that themselves write to the system streams, as those start early and may obtain a direct reference to the streams before the redirection takes place.

# Access logging ⭐️

Access logging 是一个相关但又不一样的功能,它是 Valve 接口的一个实现, 这个实现避免了额外的开销和可能的复杂的配置。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骆言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值