Tomcat完整笔记

1. Tomcat的安装和配置环境变量

安装

到官网下载需要的Tomcat版本的压缩包即可

注意:Tomcat 9 对应的JRE必须>=JRE 8;Tomcat 8 7同理

然后解压到指定的文件夹;目录名为:“apache-tomcat-[version]”

(WINDOWS和Linux)一样的效果

配置环境变量

配置JRE环境变量,变量名为:CATALINA_HOME(required)和CATALINA_BASE(optional)

前者必须,后者可选

CATALINA_HOME:tomcat根目录的路径

CATALINA_BASE:用于Advanced Configuration - Multiple Tomcat Instances 多个Tomcat实例

==================================================
Advanced Configuration - Multiple Tomcat Instances
==================================================

In many circumstances, it is desirable to have a single copy of a Tomcat
binary distribution shared among multiple users on the same server.  To make
this possible, you can set the CATALINA_BASE environment variable to the
directory that contains the files for your 'personal' Tomcat instance.

When running with a separate CATALINA_HOME and CATALINA_BASE, the files
and directories are split as following:

In CATALINA_BASE:

 * bin  - Only the following files:

           * setenv.sh (*nix) or setenv.bat (Windows),
           * tomcat-juli.jar

          The setenv scripts were described above. The tomcat-juli library
          is documented in the Logging chapter in the User Guide.

 * conf - Server configuration files (including server.xml)

 * lib  - Libraries and classes, as explained below

 * logs - Log and output files

 * webapps - Automatically loaded web applications

 * work - Temporary working directories for web applications

 * temp - Directory used by the JVM for temporary files (java.io.tmpdir)


In CATALINA_HOME:

 * bin  - Startup and shutdown scripts

          The following files will be used only if they are absent in
          CATALINA_BASE/bin:

          setenv.sh (*nix), setenv.bat (Windows), tomcat-juli.jar

 * lib  - Libraries and classes, as explained below

 * endorsed - Libraries that override standard "Endorsed Standards"
              libraries provided by JRE. See Classloading documentation
              in the User Guide for details.
              This is only supported for Java <= 8.
              By default this "endorsed" directory is absent.

Tomcat官方说:CATALINA_HOME相当于Tomcat的安装目录,而CATALINA_BASE则是Tomcat的工作目录

所以CATALINA_BASE的存在就是为了实现多个Tomcat实例可以同时运行(而不采取copy多个Tomcat的副本的方法)

每个Tomcat实例的私有文件夹:(conf配置文件,logs日志,webapps自己的app目录,work自己的临时工作区间,temp存放JVM的临时文件)

  • conf - Server configuration files (including server.xml)
  • logs - Log and output files
  • webapps - Automatically loaded web applications
  • work - Temporary working directories for web applications
  • temp - Directory used by the JVM for temporary files (java.io.tmpdir)

2. 同时运行多个Tomcat实例的正确姿势

  1. 找到Tomcat的安装目录,然后把Tomcat的私有工作目录全部拷贝到新的(每个Tomcat实例文件夹下)

    私有工作目录如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0RoI3Mzj-1570847392385)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1569894952277.png)]

    拷贝到Tomcat实例文件夹下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDZepeP4-1570847392387)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1569894978489.png)]

创建bin目录:bin目录下创建新的startup.bat启动脚本,和把tomcat-juli.jar拷贝过来

startup.bat简易写法如下:(主要是配置CATALINA_HOME和CATALINA_BASE)

set "TITLE=Tomcat9-1"
cd..
set "CATALINA_BASE=%cd%"
set "CATALINA_HOME=D:\my_heart_note\tomcat\apache-tomcat-9.0.26"
set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"

CALL %EXECUTABLE% start
set "TITLE=Tomcat9-2"
cd..
set "CATALINA_BASE=%cd%"
set "CATALINA_HOME=D:\my_heart_note\tomcat\apache-tomcat-9.0.26"
set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"

CALL %EXECUTABLE% start

TITLE是设置Tomcat窗口名字

还要修改端口号:三个端口号(Shutdown的,connector的,AJP的)

引发的各个问题解决

  1. 引发了窗口的乱码问题:因为我们窗口名字默认为Tomcat,但是到了我们的多个Tomcat实例的时候,窗口名字就不默认了。所以手动设置TITLE;

  2. 然后我想要实现的是修改Tomcat窗口的字符集。而不是修改Tomcat打印日志的字符集,去适应Windows的gbk;而是适应我们的UTF-8

  3. 所以决定修改Windows对于某个窗口的字符集。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8Qnm7RC-1570847392388)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1569895509349.png)]

十进制修改为65001

左侧Console下面的项为窗口名字,如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZzSc1SDT-1570847392389)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1569895545382.png)]

效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8xGEAMv-1570847392391)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1569895773026.png)]

厉害吧!

上面的效果是我修改了每个Tomcat实例中的webapp里的ROOT文件夹里的index.jsp首页的title

原理是

在每个Tomcat实例中的conf配置文件中,web.xml配置了一些关于应用处理的信息(Servlet相关配置)

    <servlet>
        
        <servlet-name>defaultservlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServletservlet-class>
        <init-param>
            <param-name>debugparam-name>
            <param-value>0param-value>
        init-param>
        <init-param>
            <param-name>listingsparam-name>
            <param-value>falseparam-value>
        init-param>
        <load-on-startup>1load-on-startup>
    servlet>
    <servlet>
    	
        <servlet-name>jspservlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServletservlet-class>
        <init-param>
            <param-name>forkparam-name>
            <param-value>falseparam-value>
        init-param>
        <init-param>
            <param-name>xpoweredByparam-name>
            <param-value>falseparam-value>
        init-param>
        <load-on-startup>3load-on-startup>
    servlet>

mapping

    <servlet-mapping>
        <servlet-name>defaultservlet-name>
        
        <url-pattern>/url-pattern>
    servlet-mapping>
    
    <servlet-mapping>
        <servlet-name>jspservlet-name>
        <url-pattern>*.jspurl-pattern>
        <url-pattern>*.jspxurl-pattern>
    servlet-mapping>

  
  
  
  
  
  
  
  
  
  
  
  
  

    <welcome-file-list>
        <welcome-file>index.htmlwelcome-file>
        <welcome-file>index.htmwelcome-file>
        <welcome-file>index.jspwelcome-file>
    welcome-file-list>

意思是如果没有精准匹配到的Servlet,就会走DefaultServlet,就会调用到welcome-file-list中的网页

3. Tomcat server.xml中体现出来的架构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OklaP7Fh-1570847392393)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1569909095997.png)]

<Server port="9005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />

  
   //提供的一种服务Service:名字为Catalina
  <Service name="Catalina">

      //这个服务下有多个连接器Connector,可以连接这个服务
    <Connector port="9090" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    
    <Connector port="9009" protocol="AJP/1.3" redirectPort="8443" />

      //这个Service的底层由Engine支持,Engine里由主机或者虚拟主机构成
      (提供应用webapp支持)
      
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      Realm>
        //Host主机(或者在这里配置虚拟主机也可以,name为主机名,appBase为webapp目录的路径
      

        
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />

      Host>
    Engine>
  Service>
Server>

4. 配置虚拟目录

虚拟目录:映射到别的文件夹中

比如我们想127.0.0.1/blog/index 和 127.0.0.1/sky_chou/index

表现是:两个webapp目录blog和sky_chou,然后webapp里有各自的app应用

而不是把所有的app都部署在同一个webapp下(同时还能实现修改域名的功能)

原理是把域名请求 映射到 docBase配置的映射路径下。

(缺点:要重启服务器)

第一种方法 - 修改server.xml

在HOST标签里,添加 protected synchronized void configureStart() { // Called from StandardContext.start() if (log.isDebugEnabled()) { log.debug(sm.getString("contextConfig.start")); } if (log.isDebugEnabled()) { log.debug(sm.getString("contextConfig.xmlSettings", context.getName(), Boolean.valueOf(context.getXmlValidation()), Boolean.valueOf(context.getXmlNamespaceAware()))); } webConfig(); //初始化JSP解析器 context.addServletContainerInitializer(new JasperInitializer(),null);

pom文件


<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.0modelVersion>

    <groupId>org.apache.tomcatgroupId>
    <artifactId>apache-tomcat-9.0-srcartifactId>
    <version>1.0-SNAPSHOTversion>

    <build>
        <finalName>Tomcat9.0finalName>
        <sourceDirectory>src/main/javasourceDirectory>
        <resources>
            <resource>
                <directory>src/main/javadirectory>
            resource>
        resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>2.3version>
                <configuration>
                    <encoding>UTF-8encoding>
                    <source>1.8source>
                    <target>1.8target>
                configuration>
            plugin>
        plugins>
    build>

    <dependencies>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.easymockgroupId>
            <artifactId>easymockartifactId>
            <version>3.4version>
        dependency>
        <dependency>
            <groupId>antgroupId>
            <artifactId>antartifactId>
            <version>1.7.0version>
        dependency>
        <dependency>
            <groupId>wsdl4jgroupId>
            <artifactId>wsdl4jartifactId>
            <version>1.6.2version>
        dependency>
        <dependency>
            <groupId>javax.xmlgroupId>
            <artifactId>jaxrpcartifactId>
            <version>1.1version>
        dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compilergroupId>
            <artifactId>ecjartifactId>
            <version>4.6.1version>
        dependency>
    dependencies>
project>

注意:Tomcat 9 里面的JDTCompiler源码里有CompilerOptions.VERSION_1_9这种高于1.9版本的,如果没有安装这些版本的话,识别不了

Tomcat9中有部分代码使用CompilerOptions.VERSION_1_9来判断java虚拟机版本,里面涉及1.9版本的jvm,我机器上识别不了。

java/org/apache/jasper/compiler/JDTCompiler.java类中把上述代码找出,删除即可,总共有3处。

参考链接:https://blog.csdn.net/linxdcn/article/details/72811928

8. 为什么用源码启动Tomcat 9的时候,控制台输出的中文全部都是乱码

如何发现编码是哪种乱码所导致的呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4OBaPFd-1570847392405)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570003119787.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDI0VcEm-1570847392406)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570003125879.png)]

一步步debug的过程找寻原因:

  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HOmcuZVo-1570847392407)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570002765099.png)]

  2. 找到这个国际化配置文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNKIMBcw-1570847392408)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570002802066.png)]

  3. 继续debug下去,sm.getString()底层调用的ResourceBundle实现类去读取的Properties文件

    我们看下面的文档注释(知道ResourceBundle只会按ISO8859-1去读取,要想读取非ISO8859-1编码的内容,需要以UNICODE形式去填写properties文件。

 * <p>
 * The implementation of a {
     @code PropertyResourceBundle} subclass must be
 * thread-safe if it's simultaneously used by multiple threads. The default
 * implementations of the non-abstract methods in this class are thread-safe.
 *
 * <p>
 * <strong>Note:</strong> PropertyResourceBundle can be constructed either
 * from an InputStream or a Reader, which represents a property file.
 * Constructing a PropertyResourceBundle instance from an InputStream requires
 * that the input stream be encoded in ISO-8859-1.  In that case, characters
 * that cannot be represented in ISO-8859-1 encoding must be represented by Unicode Escapes
 * as defined in section 3.3 of
 * <cite>The Java&trade; Language Specification</cite>
 * whereas the other constructor which takes a Reader does not have that limitation.
  1. 效果如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T70Jpttl-1570847392410)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570002943894.png)]

9. Tomcat整体架构

Tomcat完整笔记_第1张图片

9.1 Http工作原理

客户端->服务端(在浏览器输入url)

用的是HTTP协议

所以在传输层先通过TCP协议与服务器建立三次握手

连接完成后,客户端再封装请求数据为HTTP报文格式

然后从高层往底层封装,传输到服务器那边去。

服务器拆包,解析HTTP报文。

然后服务端调用服务器软件程序去处理这个HTTP请求,(比如Tomcat,调用不同的Servlet去处理请求)

然后把响应数据封装成HTTP报文,再传回去。

9.2 Servlet容器工作流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQfaHx7A-1570847392412)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570004188277.png)]

为了解耦,HTTP服务器并不直接调用Servlet去处理每个用户请求。而是交给了更为专业的Servlet容器(Tomcat)去处理请求。

首先,把用户请求的HTTP报文,解析封装成一个ServletRequest对象,然后传给Servlet容器,根据这个对象里的url,通过事先在(web.xml或者注解上定义的映射关系),找到对应的Servlet进行处理。(这里利用了反射技术,动态地创建相应的Servlet)

把ServletRequest传给Servlet之后,Servlet再调用核心方法Service()进行逻辑的处理,根据不同的请求格式,如POST,GET分别处理(doPost(),doGet() )

然后再把响应信息封装到servletResponse对象中,最后通过HTTP服务的处理,加上响应头,封装成HTTP报文传回给客户端。

9.3 Tomcat整体架构-两大组件

从宏观上来看,且从数据流转的角度来看,Tomcat最外面的两个核心组件就是Connector连接器和Container容器

  1. Connector:负责处理Socket请求,然后把请求数据封装成Request对象;还把Response中的响应数据封装成HTTP报文返回给客户端。(相当于HTTP服务器,不处理具体的逻辑业务,只负责转发和接收)

  2. Container容器(Servlet容器):主要是拿到Connector传过来的Request对象,通过解析里面的url,

    找到对应的Servlet进行处理。先检查这个Servlet是否已经加载(如果加载过了,就直接用),没有加载过的话,就通过反射进行创建);所以从这里可以看得出来Servlet是单例的。

    然后Servlet调用Service()方法进行处理,把响应数据封装成Response对象传回给Connector

(中间还要经过catalina容器)

(Catalina容器接收的是ServletRequest对象)

(所以在Connector组件和Catalina之间还有好多组件)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TLRrZZWo-1570847392413)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570005479858.png)]

解耦!

9.4 Connector组件的IO模型

应用层协议

HTTP/1.1 AJP(Apache服务器) HTTP/2.0

传输层

NIO(非阻塞IO) NIO2.0(异步IO,JDK7.0后支持) APR(C/C++网络通讯库)

“阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,提供一个从分布式系统角度的回答。
1.同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用*发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

典型的异步编程模型比如Node.js

举个通俗的例子:
你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

\2. 阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

还是上面的例子,
你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

如果是关心阻塞 IO/ 异步 IO, 参考 Unix Network Programming View Book

还是2014年写的以解释概念为主,主要是同步异步 阻塞和非阻塞会被用在不同层面上,可能会有不准确的地方,并没有针对 阻塞 IO/ 异步 IO 等进行讨论,大家可以后续看看这两个回答:

怎样理解阻塞非阻塞与同步异步的区别?

怎样理解阻塞非阻塞与同步异步的区别?

9.5 Connector重要组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sGXMV2JV-1570847392414)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570009028902.png)]

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

Tomcat没有Endpoint接口,而是定义了AbstractEndpoint,里面定义了两个内部类:Acceptor和SocketProcessor

Acceptor用来监听Socket请求,SocketProcessor用来处理接收到的Socket请求,它实现了Runnable接口

 /**
     * Thread used to accept new connections and pass them to worker threads.
     */
    protected Acceptor<U> acceptor;

在run()方法里调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到线程池执行,这个线程池叫Executor。Tomcat扩展了原生的Java线程池。java.concurrent包下的。

    protected abstract SocketProcessorBase<S> createSocketProcessor(
            SocketWrapperBase<S> socketWrapper, SocketEvent event);

这是抽象类SocketProcessorBase

    @Override
    public final void run() {
     
        synchronized (socketWrapper) {
     
            if (socketWrapper.isClosed()) {
     
                return;
            }
            doRun();
        }
    }

这是NIOEndpoint的内部类SocketProcessor

    // ---------------------------------------------- SocketProcessor Inner Class

    /**
     * This class is the equivalent of the Worker, but will simply use in an
     * external Executor thread pool.
     */
    protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
     

        public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
     
            super(socketWrapper, event);
        }

        @Override
        protected void doRun() {
     
            NioChannel socket = socketWrapper.getSocket();
            SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector());
            Poller poller = NioEndpoint.this.poller;
            if (poller == null) {
     
                socketWrapper.close();
                return;
            }

            try {
     
                int handshake = -1;

                try {
     
                    if (key != null) {
     
                        if (socket.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 = socket.handshake(key.isReadable(), key.isWritable());
                            // 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 (log.isDebugEnabled()) log.debug("Error during SSL handshake",x);
                } catch (CancelledKeyException ckx) {
     
                    handshake = -1;
                }
                if (handshake == 0) {
     
                    SocketState state = SocketState.OPEN;
                    // Process the request from this socket
                    if (event == null) {
     
                        state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
                    } else {
     
                        state = getHandler().process(socketWrapper, event);
                    }
                    if (state == SocketState.CLOSED) {
     
                        poller.cancelledKey(key, socketWrapper);
                    }
                } else if (handshake == -1 ) {
     
                    getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
                    poller.cancelledKey(key, socketWrapper);
                } else if (handshake == SelectionKey.OP_READ){
     
                    socketWrapper.registerReadInterest();
                } else if (handshake == SelectionKey.OP_WRITE){
     
                    socketWrapper.registerWriteInterest();
                }
            } catch (CancelledKeyException cx) {
     
                poller.cancelledKey(key, socketWrapper);
            } catch (VirtualMachineError vme) {
     
                ExceptionUtils.handleThrowable(vme);
            } catch (Throwable t) {
     
                log.error(sm.getString("endpoint.processing.fail"), t);
                poller.cancelledKey(key, socketWrapper);
            } finally {
     
                socketWrapper = null;
                event = null;
                //return to cache
                if (running && !paused && processorCache != null) {
     
                    processorCache.push(this);
                }
            }
        }
    }

Processor:Coyote协议处理接口,如果说Endpoint是用来实现TCP/IP协议的,那么Processor是专门用来实现HTTP协议的。Processor接收来自Endpoint的Socket,读取字节流(按照HTTP协议)解析成Tomcat Request和Response对象,并通过Adapter提交到容器进行处理。

ProcessorHandler:Coyote协议接口,通过Endpoint和Processor,实现对具体协议的处理能力。

9.5.1 Protocol协议

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xuHQSHZq-1570847392415)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570093202977.png)]

Tomcat按照协议和I/O提供了4个抽象类 和 6个实现类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5Zj7J1G-1570847392416)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570087837941.png)]

我们在server.xml中要在连接器Connector中指定具体的协议,起码要写协议名protocol=""

    
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

Adater:适配器的应用

Connector里的方法:createRequest和createResponse(创建了两个低级的Request和Response)

最终返回的

    /**
     * Create (or allocate) and return a Request object suitable for
     * specifying the contents of a Request to the responsible Container.
     *
     * @return a new Servlet request object
     */
    public Request createRequest() {
     
        return new Request(this);
    }


    /**
     * Create (or allocate) and return a Response object suitable for
     * receiving the contents of a Response from the responsible Container.
     *
     * @return a new Servlet response object
     */
    public Response createResponse() {
     
        if (protocolHandler instanceof AbstractAjpProtocol<?>) {
     
            int packetSize = ((AbstractAjpProtocol<?>) protocolHandler).getPacketSize();
            return new Response(packetSize - org.apache.coyote.ajp.Constants.SEND_HEAD_LEN);
        } else {
     
            return new Response();
        }
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWIYXTiX-1570847392417)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570088616988.png)]

9.5.2 Mapper组件(MapperListener的初始化)

    /**
     * Array containing the virtual hosts definitions.
     */
    protected Host[] hosts = new Host[0];


    /**
     * Default host name.
     */
    protected String defaultHostName = null;

    /**
     * Context associated with this wrapper, used for wrapper mapping.
     */
    protected Context context = new Context();

    protected static abstract class MapElement {
     

        public String name = null;
        public Object object = null;

    }

    protected static final class Host
        extends MapElement {
     

        public ContextList contextList = null;

    }

    protected static final class ContextList {
     

        public Context[] contexts = new Context[0];
        public int nesting = 0;

    }

    protected static final class Context
        extends MapElement {
     

        public String path = null;
        public String[] welcomeResources = new String[0];
        public javax.naming.Context resources = null;
        public Wrapper defaultWrapper = null;
        public Wrapper[] exactWrappers = new Wrapper[0];
        public Wrapper[] wildcardWrappers = new Wrapper[0];
        public Wrapper[] extensionWrappers = new Wrapper[0];
        public int nesting = 0;

    }

    protected static class Wrapper
        extends MapElement {
     

        public String path = null;
        public boolean jspWildCard = false;
    }

(上面用的是Tomat7的源码结构,其实差不多)这个Mapper数据结构:有Host[]主机数组,每个主机内部类里有

ContextsList应用列表。每个Context里有Wrapper

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-01G1IF0N-1570847392419)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570258949302.png)]

在StandardService的initInternal()方法中初始化了这个mapperListener(),而这个mapperListener里面就有Mapper组件

// Initialize mapper listener
	mapperListener.init();

最后在CoyoteAdaptor生成request的时候,就把已经在每个request中拥有一堆与这个request相映射的容器信息,比如Host,Context,Wrapper等等。都在mappingData里

(所以映射是Tomcat启动的时候就根据xml配置文件完成一系列容器的创建和映射对象的初始化。)

所以当请求来临时,可以按部就班的跟着pipeline走就能达到对应的servlet。

9.6 Catalina容器结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfmoKqCf-1570847392420)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570061700582.png)]

Catalina负责管理Server,Server就是整个服务器。一个Server里包含着多个Service,(默认的Service是Catalina Service)

每个Service(表示对外提供的服务),里面可以包含多个Connector(对外的连接器) (实现是Coyote实现)

和一个容器组件(Container)

Container又含有四大基本容器类:Engine,Host,Context,Wrapper

10. Tomcat服务器配置

10.1 Server.xml

10.1.1 Server

有很多Listener

//这个是版本日志监听器(用于输出版本信息,JVM信息,OS操作系统信息等等)  
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  
//ARP库(C/C++)的监听器
  
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  
//避免JRE内存泄漏
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
//全局资源命名的监听器
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
//避免ThreadLocal泄露
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

10.1.2 Service

一个Service包含着多个Connector,共享着单一的容器Container.

因为Service本身不是容器Container,所以有关的配置要在Engine里面编写(一个Service对应一个Engine)

  
  <Service name="Catalina">

10.1.x Tomcat中的Value是什么东西

不知道上面说的Values是什么的话:看https://www.iteye.com/blog/gearever-1536022

主要是容器里的一些Value,在catalina包下,有四大容器类:StandardEngine,StandardHost,StandardContext,StandardWrapper

这四大容器类会在Tomcat的启动流程中,一 一 初始化,并进行信息的传递以及start()

四大容器类有着默认的Value实现:

  • Engine:org.apache.catalina.core.StandardEngineValve
  • Host: org.apache.catalina.core.StandardHostValve
  • Context:org.apache.catalina.core.StandardContextValve
  • Wrapper:org.apache.catalina.core.StandardWrapperValve

可以自定义定制每个容器的Value,用于容器间有序的通讯

在每个容器类里有着pipeline和Value模块。必不可缺!

看一下Tomcat 包的大体结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ffnAYMSV-1570847392421)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570063332465.png)]

主要的是:coyote组件,catalina容器,jasper引擎

实现了Host接口的StandardHost

ContainerBase基类定义了Pipeline成员。

    /**
     * The Pipeline object with which this Container is associated.
     */
    protected final Pipeline pipeline = new StandardPipeline(this);
/**
 * Standard implementation of the Host interface.  Each
 * child container must be a Context implementation to process the
 * requests directed to a particular web application.
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class StandardHost extends ContainerBase implements Host {
     

    private static final Log log = LogFactory.getLog(StandardHost.class);

    // ----------------------------------------------------------- Constructors


    /**
     * Create a new StandardHost component with the default basic Valve.
     */
    public StandardHost() {
     

        super();
        
        //初始化设置Value
        pipeline.setBasic(new StandardHostValve());

    }

举例:StandardHostValue

/**
	这个Value实现了StandardHost容器的基础行为
 * Valve that implements the default basic behavior for the
 * StandardHost container implementation.
 */
//只在处理HTTP请求时有效

 final class StandardHostValve extends ValveBase {
     
     
 }

Value的作用如下图:

Tomcat完整笔记_第2张图片

当在server.xml文件中配置了一个定制化valve时,会调用pipeline对象的addValve方法,将valve以链表方式组织起来,看一下代码;

为容器定制Value

<Engine name="Catalina" defaultHost="localhost">  
  <Valve className="MyValve0"/>  
  <Valve className="MyValve1"/>  
  <Valve className="MyValve2"/>  
   ……  
  <Host name="localhost"  appBase="webapps">  
  Host>  
Engine>  
    public void addValve(Valve valve) {
     

        // Validate that we can add this Valve
        if (valve instanceof Contained)
            ((Contained) valve).setContainer(this.container);

        // Start the new component if necessary
        if (getState().isAvailable()) {
     
            if (valve instanceof Lifecycle) {
     
                try {
     
                    ((Lifecycle) valve).start();
                } catch (LifecycleException e) {
     
                    log.error(sm.getString("standardPipeline.valve.start"), e);
                }
            }
        }

        // Add this Valve to the set associated with this Pipeline
        
        //(比较特殊的插入链表处理:是尾插,但是确保了basic:也就是StandardXXXValue每个容器的默认Value实现一定要在链表的最末尾)
        
        if (first == null) {
     
            first = valve;
            valve.setNext(basic);
        } else {
     
            Valve current = first;
            while (current != null) {
     
                if (current.getNext() == basic) {
     
                    current.setNext(valve);
                    valve.setNext(basic);
                    break;
                }
                current = current.getNext();
            }
        }

        container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
    }

valve按照容器作用域的配置顺序来组织valve,每个valve都设置了指向下一个valve的next引用。同时,每个容器缺省的标准valve都存在于valve链表尾端,这就意味着,在每个pipeline中,缺省的标准valve都是按顺序,最后被调用。

消息流

Tomcat完整笔记_第3张图片

图中显示的是各个容器默认的valve之间的实际调用情况。从StandardEngineValve开始,一直到StandardWrapperValve,完成整个消息处理过程。

10.1.3 Engine

Tomcat完整笔记_第4张图片

从图中可以看出,engine有四大组件:

  • Cluster: 实现tomcat集群,例如session共享等功能,通过配置server.xml可以实现,对其包含的所有host里的应用有效,该模块是可选的。其实现方式是基于pipeline+valve模式的,有时间会专门整理一个pipeline+valve模式应用系列;
  • Realm:实现用户权限管理模块,例如用户登录,访问控制等,通过通过配置server.xml可以实现,对其包含的所有host里的应用有效,该模块是可选的;
  • Pipeline:这里简单介绍下,之后会有专门文档说明。每个容器对象都有一个pipeline,它不是通过server.xml配置产生的,是必须有的。它就是容器对象实现逻辑操作的骨架,在pipeline上配置不同的valve,当需要调用此容器实现逻辑时,就会按照顺序将此pipeline上的所有valve调用一遍,这里可以参考责任链模式;
  • Valve:实现具体业务逻辑单元。可以定制化valve(实现特定接口),然后配置在server.xml里。对其包含的所有host里的应用有效。定制化的valve是可选的,但是每个容器有一个缺省的valve,例如engine的StandardEngineValve,是在StandardEngine里自带的,它主要实现了对其子host对象的StandardHostValve的调用,以此类推。

Engine接口的方法:(与Service的绑定,默认的Host,以及集群相关的JvmRoute配置)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXAA4rTm-1570847392425)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570065906793.png)]

    

    

这个AJP的Engine是用来做Apache + Tomcat的负载均衡时用到的配置(还需要在Engine标签里配置Cluster集群标签)

参考链接:https://www.cnblogs.com/jdonson/archive/2009/12/02/1615331.html
//一般来说,用这个默认的就好
<Engine name="Catalina" defaultHost="localhost">

      
      

10.1.4 Host

Tomcat完整笔记_第5张图片

StandardHost主要是对Context的预处理,比如下面的关于部署deploy的问题

(如war包的自动部署)

    /**
     * @return the value of the deploy on startup flag.  If true, it indicates
     * that this host's child webapps should be discovered and automatically
     * deployed at startup time.
     */
    @Override
    public boolean getDeployOnStartup() {
     
        return this.deployOnStartup;
    }

又比如app目录的设置(或者采用默认的)

    /**
     * The application root for this Host.
     */
    private String appBase = "webapps";

10.1.5 Context

Context是wrapper容器的集合

StandardContext

    @Override
    public void setWrapperClass(String wrapperClassName) {
     

        this.wrapperClassName = wrapperClassName;

        try {
     
            wrapperClass = Class.forName(wrapperClassName);
            if (!StandardWrapper.class.isAssignableFrom(wrapperClass)) {
     
                throw new IllegalArgumentException(
                    sm.getString("standardContext.invalidWrapperClass",
                                 wrapperClassName));
            }
        } catch (ClassNotFoundException cnfe) {
     
            throw new IllegalArgumentException(cnfe.getMessage());
        }
    }

Tomcat完整笔记_第6张图片

Manager: 它主要是应用的session管理模块。其主要功能是session的创建,session的维护,session的持久化(persistence),以及跨context的session的管理等。Manager模块可以定制化,tomcat也给出了一个标准实现;

manager模块是必须要有的,可以在server.xml中配置,如果没有配置的话,会在程序里生成一个manager对象。

  • Resources: 它是每个web app对应的部署结构的封装,比如,有的app是tomcat的webapps目录下的某个子目录或是在context节点配置的其他目录,或者是war文件部署的结构等。它对于每个web app是必须的。
  • Loader:它是对每个web app的自有的classloader的封装。具体内容涉及到tomcat的classloader体系,会在一篇文档中单独说明。Tomcat正是有一套完整的classloader体系,才能保证每个web app或是独立运营,或是共享某些对象等等。它对于每个web app是必须的。
  • Mapper:它封装了请求资源URI与每个相对应的处理wrapper容器的映射关系。

Mapper简单介绍

Tomcat完整笔记_第7张图片

注意到,上图有两个Mapper。一个是Connector里的Mapper,一个是StandardContext里的Mapper对象

对于mapper对象,可以抽象的理解成一个map结构,其key是某个访问资源,例如/*.do那么其value就是封装了处理这个资源TestServlet的某个wrapper对象。当访问/*.do资源时,TestServlet就会在mapper对象中定位到。这里需要特别说明的是,通过这个mapper对象定位特定的wrapper对象的方式,只有一种情况,那就是在servlet或jsp中通过forward方式访问资源时用到。例如,

Mapper对象在tomcat中存在于两个地方(注意,不是说只有两个mapper对象存在),

其一,是每个context容器对象中,它只记录了此context内部的访问资源与相对应的wrapper子容器的映射;

其二,是connector模块中,这是tomcat全局的变量,它记录了一个完整的映射对应关系,即根据访问的完整URL如何定位到哪个host下的哪个context的哪个wrapper容器。

这样,通过上面说的forward方式访问资源会用到第一种mapper,除此之外,其他的任何方式,都是通过第二种方式的mapper定位到wrapper来处理的。也就是说,forward是服务器内部的重定向,不需要经过网络接口,因此只需要通过内存中的处理就能完成。这也就是常说的forward与sendRedirect方式重定向区别的根本所在。

StandardContext的Mapper:使用场景是

request.getRequestDispatcher(url).forward(request,response)

这种转发的情况下:用于在Context里的mapper中查找url与其对应的Servlet(封装的Wrapper对象)的映射

看源码:

StandardContext

    /**
    	添加新的Servlet mapping(根据配置的url pattern)
    	
     * Add a new servlet mapping, replacing any existing mapping for
     * the specified pattern.
     *
     	urlPattern
     * @param pattern URL pattern to be mapped
     
     	这个name参数:就是ServletName
     * @param name Name of the corresponding servlet to execute
     * @param jspWildCard true if name identifies the JspServlet
     * and pattern contains a wildcard; false otherwise
     *
     * @exception IllegalArgumentException if the specified servlet name
     *  is not known to this Context
     */
    @Override
    public void addServletMappingDecoded(String pattern, String name,
                                  boolean jspWildCard) {
     
        // Validate the proposed mapping
        if (findChild(name) == null)
            throw new IllegalArgumentException
                (sm.getString("standardContext.servletMap.name", name));
        //调整一下urlPattern
        String adjustedPattern = adjustURLPattern(pattern);
        
        //验证是否合法
        if (!validateURLPattern(adjustedPattern))
            throw new IllegalArgumentException
                (sm.getString("standardContext.servletMap.pattern", adjustedPattern));

        
        // Add this mapping to our registered set
        //把这个mapping假如到我们的注册的集合中
        
        //要加锁,否则会由于并发而导致的一个url对应着两个Servlet,这样会引发不可想象的错误
        synchronized (servletMappingsLock) {
     
            //看下mapping里有没有这条url的映射
            String name2 = servletMappings.get(adjustedPattern);
            //有的话,就移除掉旧的
            if (name2 != null) {
     
                // Don't allow more than one servlet on the same pattern
                Wrapper wrapper = (Wrapper) findChild(name2);
                wrapper.removeMapping(adjustedPattern);
            }
            //然后添加新的
            servletMappings.put(adjustedPattern, name);
        }
        //wrapper里面也有个mapping(是ArrayList类型),存储着url
        //为什么是ArrayList 很简单
        //(因为一个url只能对应一个Servlet,但是多个url可以对应一个Servlet啊)
        //所以一个Wrapper可以对应多个url
        Wrapper wrapper = (Wrapper) findChild(name);
        wrapper.addMapping(adjustedPattern);

        fireContainerEvent("addServletMapping", adjustedPattern);
    }

Connector里的Request 的getRequestDispatcher(String path)

    /**
     * @return a RequestDispatcher that wraps the resource at the specified
     * path, which may be interpreted as relative to the current request path.
     *
     * @param path Path of the resource to be wrapped
     */
    @Override
    public RequestDispatcher getRequestDispatcher(String path) {
     

        Context context = getContext();
        if (context == null) {
     
            return null;
        }

        if (path == null) {
     
            return null;
        }

        int fragmentPos = path.indexOf('#');
        if (fragmentPos > -1) {
     
            log.warn(sm.getString("request.fragmentInDispatchPath", path));
            path = path.substring(0, fragmentPos);
        }

        // If the path is already context-relative, just pass it through
        if (path.startsWith("/")) {
     
            return context.getServletContext().getRequestDispatcher(path);
        }

        /*
         * Relative to what, exactly?
         *
         * From the Servlet 4.0 Javadoc:
         * - The pathname specified may be relative, although it cannot extend
         *   outside the current servlet context.
         * - If it is relative, it must be relative against the current servlet
         *
         * From Section 9.1 of the spec:
         * - The servlet container uses information in the request object to
         *   transform the given relative path against the current servlet to a
         *   complete path.
         *
         * It is undefined whether the requestURI is used or whether servletPath
         * and pathInfo are used. Given that the RequestURI includes the
         * contextPath (and extracting that is messy) , using the servletPath and
         * pathInfo looks to be the more reasonable choice.
         */

        // Convert a request-relative path to a context-relative one
        String servletPath = (String) getAttribute(
                RequestDispatcher.INCLUDE_SERVLET_PATH);
        if (servletPath == null) {
     
            servletPath = getServletPath();
        }

        // Add the path info, if there is any
        String pathInfo = getPathInfo();
        String requestPath = null;

        if (pathInfo == null) {
     
            requestPath = servletPath;
        } else {
     
            requestPath = servletPath + pathInfo;
        }

        int pos = requestPath.lastIndexOf('/');
        String relative = null;
        if (context.getDispatchersUseEncodedPaths()) {
     
            if (pos >= 0) {
     
                relative = URLEncoder.DEFAULT.encode(
                        requestPath.substring(0, pos + 1), StandardCharsets.UTF_8) + path;
            } else {
     
                relative = URLEncoder.DEFAULT.encode(requestPath, StandardCharsets.UTF_8) + path;
            }
        } else {
     
            if (pos >= 0) {
     
                relative = requestPath.substring(0, pos + 1) + path;
            } else {
     
                relative = requestPath + path;
            }
        }

        return context.getServletContext().getRequestDispatcher(relative);
    }

ApplicationContext(实现了ServletContext)

getRequestDispatcher()方法

ApplicationContext内部类:

    /**
     * Internal class used as thread-local storage when doing path
     * mapping during dispatch.
     */
    private static final class DispatchData {
     

        public MessageBytes uriMB;
        public MappingData mappingData;

        public DispatchData() {
     
            uriMB = MessageBytes.newInstance();
            CharChunk uriCC = uriMB.getCharChunk();
            uriCC.setLimit(-1);
            mappingData = new MappingData();
        }
    }
    @Override
    public RequestDispatcher getRequestDispatcher(final String path) {
     

        // Validate the path argument
        if (path == null) {
     
            return null;
        }
        if (!path.startsWith("/")) {
     
            throw new IllegalArgumentException(
                    sm.getString("applicationContext.requestDispatcher.iae", path));
        }

        // Same processing order as InputBuffer / CoyoteAdapter
        // First remove query string
        String uri;
        String queryString;
        int pos = path.indexOf('?');
        if (pos >= 0) {
     
            uri = path.substring(0, pos);
            queryString = path.substring(pos + 1);
        } else {
     
            uri = path;
            queryString = null;
        }

        // Remove path parameters
        String uriNoParams = stripPathParams(uri);

        // Then normalize
        String normalizedUri = RequestUtil.normalize(uriNoParams);
        if (normalizedUri == null) {
     
            return null;
        }

        // Mapping is against the normalized uri

        if (getContext().getDispatchersUseEncodedPaths()) {
     
            // Decode
            String decodedUri = UDecoder.URLDecode(normalizedUri);

            // Security check to catch attempts to encode /../ sequences
            normalizedUri = RequestUtil.normalize(decodedUri);
            if (!decodedUri.equals(normalizedUri)) {
     
                getContext().getLogger().warn(
                        sm.getString("applicationContext.illegalDispatchPath", path),
                        new IllegalArgumentException());
                return null;
            }

            // URI needs to include the context path
            uri = URLEncoder.DEFAULT.encode(getContextPath(), StandardCharsets.UTF_8) + uri;
        } else {
     
            // uri is passed to the constructor for ApplicationDispatcher and is
            // ultimately used as the value for getRequestURI() which returns
            // encoded values. Therefore, since the value passed in for path
            // was decoded, encode uri here.
            uri = URLEncoder.DEFAULT.encode(getContextPath() + uri, StandardCharsets.UTF_8);
        }

        // Use the thread local URI and mapping data
        
        //ThreadLocal dispatchData   线程本地类型
        DispatchData dd = dispatchData.get();
        if (dd == null) {
     
            dd = new DispatchData();
            dispatchData.set(dd);
        }

        MessageBytes uriMB = dd.uriMB;
        uriMB.recycle();

        // Use the thread local mapping data
        MappingData mappingData = dd.mappingData;

        try {
     
            // Map the URI
            CharChunk uriCC = uriMB.getCharChunk();
            try {
     
                uriCC.append(context.getPath());
                uriCC.append(normalizedUri);
                service.getMapper().map(context, uriMB, mappingData);
                if (mappingData.wrapper == null) {
     
                    return null;
                }
            } catch (Exception e) {
     
                // Should never happen
                log(sm.getString("applicationContext.mapping.error"), e);
                return null;
            }

            Wrapper wrapper = mappingData.wrapper;
            String wrapperPath = mappingData.wrapperPath.toString();
            String pathInfo = mappingData.pathInfo.toString();
            HttpServletMapping mapping = new ApplicationMapping(mappingData).getHttpServletMapping();

            // Construct a RequestDispatcher to process this request
            //构建Dispatcher,定位到本context旗下的wrapper子容器
            return new ApplicationDispatcher(wrapper, uri, wrapperPath, pathInfo,
                    queryString, mapping, null);
        } finally {
     
            // Recycle thread local data at the end of the request so references
            // are not held to a completed request as there is potential for
            // that to trigger a memory leak if a context is unloaded.
            mappingData.recycle();
        }
    }
            return new ApplicationDispatcher(wrapper, uri, wrapperPath, pathInfo,
                    queryString, mapping, null);
//解释了Dispatcher为什么不能跨越Context访问资源,因为是通过本Context来定位到wrapper子容器,去访问资源的。

10.1.6 Wrapper

Wrapper的基本功能已经说了。那么再说一个wrapper比较重要的概念。严格的说,并不是每一个访问资源对应一个wrapper对象。而是每一种访问资源对应一个wrapper对象。其大致可分为三种:

  • 处理静态资源的一个wrapper:例如html,jpg等静态资源的wrapper,它包含了一个tomcat的实现处理静态资源的缺省servlet
org.apache.catalina.servlets.DefaultServlet  

处理jsp的一个wrapper:例如访问的所有jsp文件,它包含了一个tomcat的实现处理jsp的缺省servlet:

org.apache.jasper.servlet.JspServlet  

它主要实现了对jsp的编译等操作

  • 处理servlet的若干wrapper:它包含了自定义的servlet对象,就是在web.xml中配置的servlet。

需要注意的是,前两种wrapper分别是一个,主要是其对应的是DefaultServlet及JspServlet。这两个servlet是在tomcat的全局conf目录下的web.xml中配置的,当app启动时,加载到内存中。

<servlet>  
  <servlet-name>defaultservlet-name>  
  <servlet-class>org.apache.catalina.servlets.DefaultServletservlet-class>  
  <init-param>  
    <param-name>debugparam-name>  
    <param-value>0param-value>  
  init-param>  
  <init-param>  
    <param-name>listingsparam-name>  
    <param-value>falseparam-value>  
  init-param>  
  <load-on-startup>1load-on-startup>  
servlet>  
   
<servlet>  
  <servlet-name>jspservlet-name>  
  <servlet-class>org.apache.jasper.servlet.JspServletservlet-class>  
  <init-param>  
    <param-name>forkparam-name>  
    <param-value>falseparam-value>  
  init-param>  
  <init-param>  
    <param-name>xpoweredByparam-name>  
    <param-value>falseparam-value>  
  init-param>  
  <load-on-startup>3load-on-startup>  
servlet>  

Tomcat完整笔记_第8张图片

主要说说servlet对象与servlet stack对象。这两个对象在wrapper容器中只存在其中之一,也就是说只有其中一个不为空。

当以servlet对象存在时,说明此servlet是支持多线程并发访问的,也就是说不存在线程同步的过程,此wrapper容器中只包含一个servlet对象(这是我们常用的模式);

当以servlet stack对象存在时,说明servlet是不支持多线程并发访问的,每个servlet对象任一时刻只有一个线程可以调用,这样servlet stack实现的就是个简易的线程池,此wrapper容器中只包含一组servlet对象,它的基本原型是worker thread模式实现的。(废弃)

(提供的并发方式并不安全)

Wrapper接口下的方法:(可以看得出是处理Servlet的)

    /**
     * @return the fully qualified servlet class name for this servlet.
     */
    public String getServletClass();


    /**
     * Set the fully qualified servlet class name for this servlet.
     *
     * @param servletClass Servlet class name
     */
    public void setServletClass(String servletClass);

10.1.7 Connector

10.2 tomcat-users.xml

11. Tomcat的启动流程

时序图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NBixZigZ-1570847392432)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570085332850.png)]

startup.bat里

set "CURRENT_DIR=%cd%"

## 查看系统变量中是否有CATALINA_HOME(相当于Tomcat安装目录)
if not "%CATALINA_HOME%" == "" goto gotHome
##没有的话,默认设置为本文件夹
set "CATALINA_HOME=%CURRENT_DIR%"
echo %CATALINA_HOME%
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo %CATALINA_HOME%
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome

##设置可执行文件为catalina.bat
set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"

##省略命令行参数

##执行catalina.bat

call "%EXECUTABLE%" start %CMD_LINE_ARGS%

:end

catalina.bat中的核心部分:

rem ----- Execute The Requested Command ---------------------------------------

echo Using CATALINA_BASE:   "%CATALINA_BASE%"
echo Using CATALINA_HOME:   "%CATALINA_HOME%"
echo Using CATALINA_TMPDIR: "%CATALINA_TMPDIR%"
if ""%1"" == ""debug"" goto use_jdk
echo Using JRE_HOME:        "%JRE_HOME%"
goto java_dir_displayed
:use_jdk
echo Using JAVA_HOME:       "%JAVA_HOME%"
:java_dir_displayed
echo Using CLASSPATH:       "%CLASSPATH%"

set _EXECJAVA=%_RUNJAVA%

#设置主类为Bootstrap,从这里开始整个Java程序,启动Tomcat
set MAINCLASS=org.apache.catalina.startup.Bootstrap

粗略介绍Tomcat启动流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JtQFO3xE-1570847392433)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570088784843.png)]

12. Tomcat启动流程源码解析

Lifecycle接口:定义了每个组件的生命周期(几乎所有的Tomcat组件都实现了这个接口)

方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tB8u11ti-1570847392435)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570089043548.png)]

继承图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-veaWrB5a-1570847392436)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570089442444.png)]

Bootstrap的main()方法:Tomcat的入口

    /**
     * Main method and entry point when starting Tomcat via the provided
     * scripts.
     *
     * @param args Command line arguments to be processed
     */
    public static void main(String args[]) {
     

        synchronized (daemonLock) {
     
            if (daemon == null) {
     
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
     
                    //初始化bootstrap
                    bootstrap.init();
                } catch (Throwable t) {
     
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                //damon 就是bootstrap了
                daemon = bootstrap;
            } else {
     
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                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")) {
     
                
                //看这里:我们的starup.bat里的call %Executable% start命令
                
                //daemon就是bootstrap实例
                daemon.setAwait(true);
                //load方法--->内部调用catalina的load方法
                daemon.load(args);
                //start方法
                daemon.start();
                if (null == daemon.getServer()) {
     
                    System.exit(1);
                }
            } 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 t) {
     
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
     
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }
    }

Bootstrap的init()方法:

    /**
     * Initialize daemon.
     * @throws Exception Fatal initialization error
     */
    public void init() throws Exception {
     

        //初始化类加载器
        initClassLoaders();


        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        
        //通过catalinaLoader类加载器来加载Catalina类
        Class<?> startupClass = 
            catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        
        //并且通过反射技术,用class对象调用其构造器去创建实例
        Object startupInstance = startupClass.getConstructor().newInstance();

        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        
        //通过反射技术,执行setParentClassLoader方法
        //(下面都是与Tomcat独有的类加载器相关的代码)
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        //设置startupInstance为catalinaDaemon
        catalinaDaemon = startupInstance;
    }

bootstrap的load()方法

    /**
     * Load daemon.
     */
    private void load(String[] arguments) throws Exception {
     

        // Call the load() method
        String methodName = "load";
        Object param[];
        Class<?> paramTypes[];
        if (arguments==null || arguments.length==0) {
     
            paramTypes = null;
            param = null;
        } else {
     
            paramTypes = new Class[1];
            paramTypes[0] = arguments.getClass();
            param = new Object[1];
            param[0] = arguments;
        }
        Method method =
            catalinaDaemon.getClass().getMethod(methodName, paramTypes);
        if (log.isDebugEnabled()) {
     
            log.debug("Calling startup class " + method);
        }
        
        //param是命令行参数
        //这是在调用catalina的load()方法
        method.invoke(catalinaDaemon, param);
    }

我个人找了下源码:看了一下Catalina的server是怎么创建的

Catalina里创建了digester对象,这是用来解析XML的

从这里就可以看出,是先通过解析XML配置文件,来反射的创建StandardServer,注入到Catalina中

        // Configure the actions we will be using
        digester.addObjectCreate("Server",
                                 "org.apache.catalina.core.StandardServer",
                                 "className");

然后Catalina在load()过程中,初始化了下一个子组件:

Catalina#load()

        // Start the new server
        try {
     
            //初始化Server组件
            getServer().init();		//多态的调用
        } catch (LifecycleException e) {
     
            if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
     
                throw new java.lang.Error(e);
            } else {
     
                log.error(sm.getString("catalina.initError"), e);
            }
        }

解析一下这里的精华:每个组件(这里是server)实现了LifeCycle接口。LifeCycle有个实现类LifeCycleBase

LifeCycleBase#init()方法

    @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());
        }
    }

//这是个让子类自己去实现的方法(具体逻辑交由子类自己实现)---所以定义为抽象方法
    /**
     * Sub-classes implement this method to perform any instance initialisation
     * required.
     *
     * @throws LifecycleException If the initialisation fails
     */
    protected abstract void initInternal() throws LifecycleException;

在这里:StandardServer继承了LifeCycleBase类,所以要重写这个抽象方法initInternal

    /**
     * Invoke a pre-startup initialization. This is used to allow connectors
     * to bind to restricted ports under Unix operating environments.
     */
    @Override
    protected void initInternal() throws LifecycleException {
     

        super.initInternal();
        //省略。。。。。。。。
        
       // Initialize our defined Services(因为一个Server可以有多个Service)
        //初始化下一个子组件:Service
        for (int i = 0; i < services.length; i++) {
     
            services[i].init();
        }

下一步:到Service.initInternal()

    /**
     * Invoke a pre-startup initialization. This is used to allow connectors
     * to bind to restricted ports under Unix operating environments.
     */
    @Override
    protected void initInternal() throws LifecycleException {
     

        super.initInternal();

        //初始化Engine
        
        if (engine != null) {
     
            engine.init();
        }

        // Initialize any Executors
        //初始化Executor (注意:这个初始化顺序其实和Server.xml里严格定义的分层结构是密切关联的)
        
        for (Executor executor : findExecutors()) {
     
            if (executor instanceof JmxEnabled) {
     
                ((JmxEnabled) executor).setDomain(getDomain());
            }
            executor.init();
        }

        // Initialize mapper listener
        
        //这个mapper是 host主机与URL相关联的;跟虚拟目录相关的映射;还有默认主机localhost的配置
        
        mapperListener.init();

        // Initialize our defined Connectors
        
        //初始化Connector
        synchronized (connectorsLock) {
     
            for (Connector connector : connectors) {
     
                connector.init();
            }
        }
    }

LifeCycleBase是一套模板,实现了LifeCycle接口的一系列生命周期方法;但是把一些具体的差异化实现改为抽象方法;让子类实现。并根据多态调用。

下一步:Connector()#initInternal()方法

        
//适配器		
adapter = new CoyoteAdapter(this);
//protocolHandler连接器的组件初始化
protocolHandler.init();

下一步:protocolHandler#initInternal()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T07mTI4y-1570847392438)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570093505540.png)]

实现类:AbstractHttp11Protocol:HTTP1.1版本的

//endpoint的初始化        
endpoint.init();

我们来看看Endpoint到底是不是和上面说的一样:是与Socket相关的

    public final void init() throws Exception {
     
        if (bindOnInit) {
     
            bindWithCleanup();
            bindState = BindState.BOUND_ON_INIT;
        }

进入后:又是AbstractEndpoint的模板方法bind()

    public abstract void bind() throws Exception;

点击左边的绿色按钮,可以看到哪个类实现了这个抽象方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L4EvCDVL-1570847392439)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570093854377.png)]

我们选择NioEndpoint(默认)

    /**
     * Initialize the endpoint.
     */
    @Override
    public void bind() throws Exception {
     
        initServerSocket();

        setStopLatch(new CountDownLatch(1));

        // Initialize SSL if needed
        initialiseSsl();

        selectorPool.open(getName());
    }

进入initServerSocket()

    // Separated out to make it easier for folks that extend NioEndpoint to
    // implement custom [server]sockets
    protected void initServerSocket() throws Exception {
     
        if (!getUseInheritedChannel()) {
     
            //创建ServerSocketChannel(通过静态方法open())
            serverSock = ServerSocketChannel.open();

            socketProperties.setProperties(serverSock.socket());
            
            //创建套接字InetSocketAddress  (IP + PORT端口)
            InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
            
            //绑定到这个套接字,然后监听
            serverSock.socket().bind(addr,getAcceptCount());
        } else {
     
            // 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"));
            }
        }
        
        serverSock.configureBlocking(true); //mimic APR behavior
    }

回到Bootstrap#main()方法里的start()方法

    /**
     * Start the Catalina daemon.
     * @throws Exception Fatal start error
     */
    public void start() throws Exception {
     
        if (catalinaDaemon == null) {
     
            init();
        }
		//用反射是因为catalinaDaemon是Object类型的(根本就没有指定具体类型)
        Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
        method.invoke(catalinaDaemon, (Object [])null);
    }

start()也有startInternal()模板方法设计模式

?的,我自己偷懒,没去看StandardContext的startInternal()方法

在Context接口下有一个方法:获取所有的应用生命周期监听器

    /**
     * Obtain the registered application lifecycle listeners.
     *
     * @return An array containing the application lifecycle listener instances
     *         for this web application in the order they were specified in the
     *         web application deployment descriptor
     */
    public Object[] getApplicationLifecycleListeners();

然后在StandardContext里有个listenerStart()方法:启动所有的监听器

    /**
     * Configure the set of instantiated application event listeners
     * for this Context.
     * @return true if all listeners wre
     * initialized successfully, or false otherwise.
     */
    public boolean listenerStart() {
     

        if (log.isDebugEnabled())
            log.debug("Configuring application event listeners");

        // Instantiate the required listeners
        String listeners[] = findApplicationListeners();
        Object results[] = new Object[listeners.length];
        boolean ok = true;
        for (int i = 0; i < results.length; i++) {
     

                String listener = listeners[i];
                results[i] = getInstanceManager().newInstance(listener);

        }

        // Sort listeners in two arrays
        List<Object> eventListeners = new ArrayList<>();
        List<Object> lifecycleListeners = new ArrayList<>();
        for (int i = 0; i < results.length; i++) {
     
            if ((results[i] instanceof ServletContextAttributeListener)
                || (results[i] instanceof ServletRequestAttributeListener)
                || (results[i] instanceof ServletRequestListener)
                || (results[i] instanceof HttpSessionIdListener)
                || (results[i] instanceof HttpSessionAttributeListener)) {
     
                eventListeners.add(results[i]);
            }
            if ((results[i] instanceof ServletContextListener)
                || (results[i] instanceof HttpSessionListener)) {
     
                lifecycleListeners.add(results[i]);
            }
        }

        // Listener instances may have been added directly to this Context by
        // ServletContextInitializers and other code via the pluggability APIs.
        // Put them these listeners after the ones defined in web.xml and/or
        // annotations then overwrite the list of instances with the new, full
        // list.
        for (Object eventListener: getApplicationEventListeners()) {
     
            eventListeners.add(eventListener);
        }
        setApplicationEventListeners(eventListeners.toArray());
        for (Object lifecycleListener: getApplicationLifecycleListeners()) {
     
            lifecycleListeners.add(lifecycleListener);
            if (lifecycleListener instanceof ServletContextListener) {
     
                noPluggabilityListeners.add(lifecycleListener);
            }
        }
        setApplicationLifecycleListeners(lifecycleListeners.toArray());

        // Send application start events

        if (getLogger().isDebugEnabled())
            getLogger().debug("Sending application start events");

        // Ensure context is not null
        getServletContext();
        context.setNewServletContextListenerAllowed(false);

        Object instances[] = getApplicationLifecycleListeners();
        if (instances == null || instances.length == 0) {
     
            return ok;
        }

        ServletContextEvent event = new ServletContextEvent(getServletContext());
        ServletContextEvent tldEvent = null;
        if (noPluggabilityListeners.size() > 0) {
     
            noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext());
            tldEvent = new ServletContextEvent(noPluggabilityServletContext);
        }
        for (int i = 0; i < instances.length; i++) {
     
            if (!(instances[i] instanceof ServletContextListener))
                continue;
            ServletContextListener listener =
                (ServletContextListener) instances[i];
            try {
     
                fireContainerEvent("beforeContextInitialized", listener);
                if (noPluggabilityListeners.contains(listener)) {
     
                    listener.contextInitialized(tldEvent);
                } else {
     
                    //在这里调用监听器的初始化方法
                    listener.contextInitialized(event);
                }
                fireContainerEvent("afterContextInitialized", listener);
            } catch (Throwable t) {
     
                ExceptionUtils.handleThrowable(t);
                fireContainerEvent("afterContextInitialized", listener);
                getLogger().error
                    (sm.getString("standardContext.listenerStart",
                                  instances[i].getClass().getName()), t);
                ok = false;
            }
        }
        return ok;

    }

而这个linstenerStart()方法又是在哪里被调用的呢?

答案是:在StandardContext的startInternal()里

            // Configure and call application event listeners
            if (ok) {
     
                if (!listenerStart()) {
     	//这里调用
                    log.error(sm.getString("standardContext.listenerFail"));
                    ok = false;
                }
            }

一直按着顺序,调用子组件的start()方法

一直到最后endpoint()的start()

    /**
     * Start the NIO endpoint, creating acceptor, poller threads.
     */
    @Override
    public void startInternal() throws Exception {
     

        if (!running) {
     
            running = true;
            paused = false;

            if (socketProperties.getProcessorCache() != 0) {
     
                processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getProcessorCache());
            }
            if (socketProperties.getEventCache() != 0) {
     
                eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getEventCache());
            }
            if (socketProperties.getBufferPool() != 0) {
     
                nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getBufferPool());
            }

            // Create worker collection
            if (getExecutor() == null) {
     
                createExecutor();
            }

            initializeConnectionLatch();

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

            //主要看这句话,开启Acceptor线程;之前说过Endpoint实现类内部还有一个叫Accptor的,
            //是用来接收Socket请求的!
            startAcceptorThread();
        }
    }
//注意:Tomcat9和Tomcat8又不一样了,Tomcat8这里创建了一堆Acceptor   
protected void startAcceptorThread() {
     
        acceptor = new Acceptor<>(this);
        String threadName = getName() + "-Acceptor";
        acceptor.setThreadName(threadName);
        Thread t = new Thread(acceptor, threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }

Acceptor类(Tomcat8 是NioEndpoint的内部类)

    @Override
    public void run() {
     

        int errorDelay = 0;

        // Loop until we receive a shutdown command
        while (endpoint.isRunning()) {
     

            // Loop if endpoint is paused
            while (endpoint.isPaused() && endpoint.isRunning()) {
     
                state = AcceptorState.PAUSED;
                try {
     
                    Thread.sleep(50);
                } catch (InterruptedException e) {
     
                    // Ignore
                }
            }

            if (!endpoint.isRunning()) {
     
                break;
            }
            state = AcceptorState.RUNNING;

            try {
     
                //if we have reached max connections, wait
                endpoint.countUpOrAwaitConnection();

                // Endpoint might have been paused while waiting for latch
                // If that is the case, don't accept new connections
                if (endpoint.isPaused()) {
     
                    continue;
                }

                U socket = null;
                try {
     
                    // Accept the next incoming connection from the server
                    // socket
                    
                    //就是这句:用来accpept等待Socket请求
                    socket = endpoint.serverSocketAccept();
                } catch (Exception ioe) {
     
                    // We didn't get a socket
                    endpoint.countDownConnection();
                    if (endpoint.isRunning()) {
     
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    } else {
     
                        break;
                    }
                }
                // Successful accept, reset the error delay
                errorDelay = 0;

                // Configure the socket
                if (endpoint.isRunning() && !endpoint.isPaused()) {
     
                    // setSocketOptions() will hand the socket off to
                    // an appropriate processor if successful
                    if (!endpoint.setSocketOptions(socket)) {
     
                        endpoint.closeSocket(socket);
                    }
                } else {
     
                    endpoint.destroySocket(socket);
                }
            } catch (Throwable t) {
     
                ExceptionUtils.handleThrowable(t);
                String msg = sm.getString("endpoint.accept.fail");
                // APR specific.
                // Could push this down but not sure it is worth the trouble.
                if (t instanceof Error) {
     
                    Error e = (Error) t;
                    if (e.getError() == 233) {
     
                        // Not an error on HP-UX so log as a warning
                        // so it can be filtered out on that platform
                        // See bug 50273
                        log.warn(msg, t);
                    } else {
     
                        log.error(msg, t);
                    }
                } else {
     
                        log.error(msg, t);
                }
            }
        }
        state = AcceptorState.ENDED;
    }

13. Tomcat请求处理流程源码解析

预备工作:首先先写一个最简单的Servlet,然后通过IDEA生成class文件,测试运行成功。

然后把out路径下的war包 copy到tomcat源码工程下的home文件夹里的webapp下,

然后就可以通过编译tomcat源码,在启动Tomcat的时候,访问编写好的Servlet。

然后debug

debug流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mtHNcBsY-1570847392440)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570244886481.png)]

主要是通过pipeline + valve来进行责任链的传递,最终找到wrapper,然后经过过滤器链,最后抵达Servlet

13.1 NioEndpoint

  1. NioEndpoint启动start();并且创建acceptor线程(接收Socket请求,封装为SocketWrapper),以及开启poller threads线程(完成对selectionKey的处理)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WjZCnyCC-1570847392441)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570250105452.png)]

    还在里面创建线程池

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2s507mOV-1570847392441)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570250229071.png)]

    创建accpetor线程和Poller线程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKeGyUZ5-1570847392443)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570250274134.png)]

13.1.1 Acceptor

​ 1.1 下面我们来看看Acceptor线程的run方法

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z4GoLt9B-1570847392444)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570251006485.png)]

调用NioEndpoint的accept()方法,阻塞等待Socket请求

获取到Socket请求之后,由Acceptor配置configure这个Socket (通过NioEndpoint的setSocketOptions方法)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3XNZS9O-1570847392445)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570251148202.png)]

下面我们看看NioEndpoint的setSocketOptions(SocketChannel)方法

  1. 设置SocketChannel为非阻塞模式,然后获取其Socket

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AamutVf1-1570847392445)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570251238663.png)]

  1. 添加Channel进NioChannels中(是个同步的栈SynchronizedStack)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jmggXTWu-1570847392448)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570251345624.png)]

  2. 把SocketChannel封装成NioSocketWrapper (wrapper中含SocketChannel和endpoint)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I10UWvKH-1570847392449)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570251384133.png)]

    并且把channel注册进poller中的selector里 (注册监听事件为Read),并且创建PollerEvent(为Register),addEvent()

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TUiwfypW-1570847392450)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570251531821.png)]

    先看看Wrapper里面是什么东东(居然有NioSelectorPool,NioChannel的同步栈,对应的Poller)

    也就是一个wrapper对应着一个poller,然后还含有一个栈的niochannel通道(因为一个selector上有多个channel)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lYIPcMd2-1570847392452)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570253051467.png)]

    看看PollerEvent 也是个线程, (封装了NioChannel和channel的监听事件)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJcMVxZ6-1570847392452)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570252518986.png)]

    再分析下PollerEvent的run()方法 (上面我们说过在配置SocketChannel的时候,我们就把这个比通过poller.register方法注册到pollerEvent线程中去了,并且设置为监听Register事件)

    这里的run方法就是判断如果是register状态的channel,那么就注册到其selector中去,并且监听Read事件,

    最后还要加上attachment【SocketWrapper】(附加在SelectionKey上面的东西)->后面poller线程在处理key时会把这个比取出来

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H41rBnsM-1570847392454)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570252658708.png)]

    PollerEvent#run()方法中还会监听不是Register的事件,那么就会设置key的感兴趣状态为Read(有点懵逼,没看懂,反正就是修改了key附件中Wrapper里面的感兴趣事件interestOps)或者关闭掉这些key

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AaKLY5hw-1570847392456)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570253235319.png)]

至此:新连接进来的Socket配置完成

接下来:应该分析poller线程了

13.1.2 Poller

分析下Poller的run()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3yu2POJ-1570847392456)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570252028086.png)]

总之就是设置一个hasEvents的(判断有无事件的状态标志位)—poller维护着一个同步的事件队列

然后selector.select()熟悉的老朋友了,反正就是用来轮询我们注册在这个selector上的SocketChannel的状态(监听)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vocKVAgB-1570847392457)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570253388930.png)]

然后下一步的重头戏来了,进入processKey(sk,socketWrapper);也就是拿着SeletionKey和SocketWrapper进去做处理

ProcessKey (就是Nio那一套东西,分逻辑处理读事件,写事件)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xLL2AyPp-1570847392458)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570253810634.png)]

怎么处理呢?交给Executor去处理这个任务(Executor是在我们上面创建NioEndpoint的时候就已经创建了)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8A2JbEUa-1570847392460)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570254008315.png)]

​ execute(this)这个this是个什么东西,这个this是(上一步socketWrapper.readOperation这个操作状态线程)

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZT8fMJVE-1570847392461)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570254133353.png)]

​ 如图,这只是个抽象类,里面的run方法是空的,那么实现类当然是:Nio啦

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsA5aeMr-1570847392462)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570254320526.png)]

注意到:上面处理读事件的时候,大概率会进入到这个处理中processSocket()这里面去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZO1sXlC-1570847392463)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570254570574.png)]

13.1.3 Processor

进到下一个组件Processor中去进一步处理(我们之前分析,有SocketProcessor和ProcessorHandler两个Processor)一个负责TCP阶段的,一个负责Http

在进入另一个方法:AbstractEndpointprocessKey()中,也会提交给Executor()去执行任务(这次的任务线程是SocketProcessorBase)

然后进入SocketProcessorBase的实现类SocketProcessor(NioEndpoint的内部类)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8j8Lfpw-1570847392464)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570254903658.png)]

看吧,是TLS的握手协议,说明SocketProcessor处理的是TCP连接。(安全层)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-87dMyrya-1570847392465)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570254776226.png)]

下一步调用Handler去处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9PEc0Uqz-1570847392466)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255223333.png)]

​ 进入AbstractProtocol里的process()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mxTrZDyo-1570847392467)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255353183.png)]

这个connections是Map的映射表

通过Socket拿到这个Socket的Processor

然后进入processor的process()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXCH2Wvt-1570847392469)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255538905.png)]

进入里面的process方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ScW7WMI3-1570847392470)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255591417.png)]

看好:这个service进入之后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oe5SxMki-1570847392471)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255702442.png)]

是这个抽象类的实现类:都是实现了Processor接口的,HTTP1.1协议,选择Http11Processor进行处理

(至此,我们从TCP处理完后,进入Http处理)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PxUKiD1f-1570847392472)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255653586.png)]

在Http11Processor的service()中处理SocketWrapper里的Http报文

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vYiJi4Ta-1570847392473)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570255878208.png)]

13.1.4 Adapter()

在Adapter适配器中处理request和response (request和response都是Http11Processor处理器解析Http数据后,创建的两个核心对象)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfDjRRMx-1570847392474)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256148914.png)]

进入Adapter()的service()是个抽象方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JSuU6UG-1570847392475)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256243357.png)]

看清楚了,是唯一的适配器实现类,那就是Coyote协议的Adapter适配器

CoyoteAdapter调用connecton.getService().getContainer() catalina容器,然后通过pipeline+valve机制去进行

一条容器链上的valve处理器的执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpdAJ3CL-1570847392477)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256319215.png)]

13.1.5 容器链 pipeline+valve责任链

Service.getConnector()与Service直接对接的容器就只有一个Engine

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5yNKaxx1-1570847392477)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256454587.png)]

看下pipeline的官方解释 (我们在上面启动流程源码中解析过这个pipeline和valve,每个容器自己有个pipeline,然后这个pipeline自己设置该容器的valve)

下面是Pipeline类的getBasic()方法,说明pipeline里有这个容器的valve(按容器作用域严格区分)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-drjwTk8W-1570847392479)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256543101.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TOVOBs8K-1570847392480)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256481916.png)]

看下Valve接口的实现类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxxbG6FW-1570847392481)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256645950.png)]

主要看下面几个:都是标准的容器Valve实现类

一个Pipeline里可以有多个Valve(每个容器有自己的标准Valve实现,也可以在server.xml中自己配置)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOigvM5h-1570847392482)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256760112.png)]

最后回到上面的valve.invoke()处理方法

进入到的是StandardEngineValve#invoke()

很简单的逻辑:就是通过request拿到下一个子容器host,然后再调用host.pipeline的valve,执行其invoke()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4pDQ1e2y-1570847392483)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256852492.png)]

然后进入的是StandardContext#invoke()方法(不允许直接访问Web-INF和META-INF里的资源)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JXVpLRII-1570847392485)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570256919478.png)]

差点分析少了一步很关键的,因为到了context这里,下一步就是wrapper了(而wrapper已经和指定的Servlet绑定在一起了),那么就需要在context这个组件中选择合适的servlet去处理这个request请求

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wEyW5oa5-1570847392486)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570257702227.png)]

在StandardWrapper#invoke()方法里:调用了FilterChain#doFilter()执行过滤器过滤,让我们一起来看下吧。

FilterChain所采用的责任链设计模式

所谓责任链设计模式:

无非就是给定一组任务,要逐个按顺序的完成这些任务。只有把所有的任务都完成了,才能做下面的逻辑。

否则提早退出。(通过 在chain调用类里维护一个index索引,这个用来判断任务完成的数量)

Tomcat源码之FilterChain

Tomcat的源码,filterChain.doFilter()的调用是在StandardWrapperValve#invoke()方法里面(也就是在Servlet执行之前)。

 @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
     

        requestCount.incrementAndGet();
        StandardWrapper wrapper = (StandardWrapper) getContainer();
        Servlet servlet = null;
        Context context = (Context) wrapper.getParent();

        // Create the filter chain for this request
        ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

        // Call the filter chain for this request
        // NOTE: This also calls the servlet's service() method
        Container container = this.container;
            if ((servlet != null) && (filterChain != null)) {
     
                    if (request.isAsyncDispatching()) {
     
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
     
                        //就是在这里:filterChain.doFilter(req,resp)执行
                        filterChain.doFilter
                            (request.getRequest(), response.getResponse());
                    }
                }

            }
        }
    }

按照您的if(index == size){return}的话是不能说明实际的拦截效果的。

更合理的应该是按照源码的

if(pos < n){

//xxxxx filter.doFilter()

如果在这里递归回来了,就要退出(说明有的filter还没有执行,就退出了)

return;

}

    private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {
     

        // Call the next filter if there is one
        if (pos < n) {
     
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
     
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
     
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
     
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{
     req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
     
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
     
                throw e;
            } catch (Throwable e) {
     
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.filter"), e);
            }
            return;
        }

//而只有所有的filter执行完,pos==n,才能到达这里

执行Servlet#service(req,resp)

// We fell off the end of the chain -- call the servlet instance

            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
     
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{
     req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
     
                //service()被调用
                servlet.service(request, response);
            }

(Mapper组件开始发挥作用)

在CoyoteAdaptor中的postParseRequest方法

postParseRequest方法的执行步骤如下:

  1. 解析请求url中的参数;
  2. URI decoding的转换;
  3. 调用normalize方法判断请求路径中是否存在"", “//”, “/./“和”/…/”,如果存在则处理结束;
  4. 调用convertURI方法将字节转换为字符;
  5. 调用checkNormalize方法判断uri是否存在"", “//”, “/./“和”/…/”,如果存在则处理结束;
  6. 调用Connector的getMapper方法获取Mapper(已在《TOMCAT源码分析——请求原理分析(上)》一文中介绍),然后调用Mapper的map方法host和context进行匹配(比如http://localhost:8080/manager/status会匹配host:localhost,context:/manager),其实质是调用internalMap方法

参考链接:https://www.cnblogs.com/jiaan-geng/p/4894832.html

最后,进入到最后一个子容器:Wrapper(封装Servlet的东西)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qTBbytjU-1570847392487)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570257038966.png)]

进入StandardWrapper#invoke()方法

然后通过wrapper.allocate()尝试获取servlet

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tCYvpwbh-1570847392489)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570257147362.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-27YEKK24-1570847392490)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570257302915.png)]

说明了Servlet在这种模式下,一般来说都属于单例的(返回the same instance)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VXM2k3yp-1570847392491)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570257370316.png)]

DoubleCheck实现的单例方法,创建Servlet,然后initServlet(instance)初始化

接下来,我们回到allocate()返回后Servlet后,然后经过filterChain去完成指定的过滤

最终到达Servlet的service方法

然后调用实现类的doPost()或doGet()

14. WebSocket

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SGM4HaU7-1570847392492)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570496907062.png)]

15. 实例驱动-基于WebSocket的多人实时聊天室

15.1 聊天界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9q7DnDx-1570847392493)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570281441128.png)]

15.2 登陆界面

略(普通的登陆页面即可),成功后跳转聊天室页面

15.3 登录逻辑

  1. 判断用户密码(待优化)
  2. 封装响应数据
  3. 在服务端创建此次请求会话的HttpSession,并且设置属性username(不用考虑这么多细节,就是标识用户的httpSession属性即可)
  4. 然后返回响应数据(成功状态信息,用于前端校验,然后跳转)

(关于跳转到chat页面的,可以做个拦截器,或者叫过滤器,优化一下)

15.4 WebSocket连接建立

15.4.1 前端

前端就用JS,创建WebSocket对象,对服务器发起WebSocket请求(ws://)

然后写几个关于WebSocket的生命周期函数:如onOpen()的处理函数

15.4.2 后端

新建一个class:主要是处理WebSocket请求的类ChatSocket,继承javax.websocket.Endpoint类

(或者直接用@ServerEndpoint注解)

记录几个成员:

Session session (这个是这次WebSocket请求的session会话),主要用于提升

这个变量的作用域 (重写的onOpen()方法中就有,不用担心这个)

static Map onlineUsers (记录每个HttpSession和其对应的WebSocket类)

这样就相当于每个人都能知道别的会话信息,从而能够实现广播功能。

(静态的,共享)

当前登录的用户数(原子整形类)

(主要方法:通过定好的几个注解把,@xxxx之类的)

然后写几个生命周期的处理方法:拿到服务端的数据,然后渲染到页面上

执行流程:

  1. 登陆用户数+1

  2. 获取此次WebSocket连接会话的 用户的HttpSession(注意:不是Endpoint里的session,session直接从参数里拿就可以了)

    HttpSession需要另外创建一个类getHttpSessionConfigurator,然后继承ServerEndpointConfig.Configurator(内部类)

    用于getHttpSession的

    重写他的modifyHandshake(ServerEndpointConfig config, request, response)方法

    然后通过这个session = (HttpSession)request.getHttpSession(); 强转后拿到HttpSession

    然后存放进这个ServerEndpointConfig config中的userProperties(这是个Map)中

    config.getUserProperties().put(HttpSession.class.getName(),httpSession);

    反正就是建立key-value记录下这个httpSession,

    然后我们就可以在ChatSocket类中通过

    重写那个onOpen(Session session, EndpointConfig config)方法。

    哈哈,这个config就是我们刚刚的那个类getHttpSessionConfigurator中就存进config里的userProperties中了。

    我们直接拿就可以了

    (HttpSession)config.getUserProperties().get(HttpSession.class.getName());

    通过这个key就可以拿到Endpoint Socket连接内部处理时 预存的HttpSession了

  3. 记录进onlineUsers 映射表中

    先判断httpSession里的username是否为空。

    然后onlineUsers.put(httpSession,this)

  4. 广播信息(因为是登陆,业务要做到每个人登陆,都要发送一条广播:xxx登陆了)

    1. 拿到当前聊天室所有的用户

      通过遍历map的keySet,然后拿到每个用户username,然后组装成一个String,

      让前端去组装(形成好友列表的信息:也就是可选择的聊天对象)

    2. 组装信息(形式为JSON格式)

      {“data”:“当前登录的所有用户,用,分割”,“toName”:"",“fromName”:"",type:“user”}

      这里可以封装一个工具类,用于组装服务端的响应数据

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cFiloZYB-1570847392495)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570284523356.png)]

      DATA:发送数据

      Type:发送类型(单播,还是广播)

      From_Name:发送方名字

      TO_Name:接收方名字

      以一个Map的形式组装起来,然后转换为JSON字符串:

      形成messgae

  5. 广播信息(告知大家爷来啦!)

    遍历onlineUsers这个map,然后拿到他们里面的每个ChatSocket.session.getBasicRemote()

    然后就可以发送单行数据sendText(messgae)

15.5 WebSocket onMessage()的处理流程

  1. 在onMessage(String msg,Session session)方法上用上注解

  2. 解析前端传过来的数据msg (这里规定为前后端都是传JSON数据) —用JSON.parseObject(msg,Map.class)

    利用fastJson包把封装过来的参数对象,解析为Map映射格式

  3. 组装信息(利用自己写的MessageUtil),封装成前后端规定好的 JSON字符串格式(也是Map转换过来的)

  4. 然后通过判断接收方 msg.toName判断对象是广播还是单播

  5. 广播的话,则遍历所有的onlineUsers,然后一一发送这次请求(注意这里,要排除掉发送给自己的,不然会与前端重复渲染)

  6. 单播,则判断是否在线,不在线直接退出这次处理。

  7. 在线则遍历onlineUsers,然后根据username找到指定的WebSocket,然后直接发送数据

15.6 关于前端界面的诸多问题

需求1:按照现实的QQ聊天软件,自己发送的消息渲染到右边,别人发给自己的渲染到左边。

需求2:想要实现,每次渲染到聊天框,都是一个组件(容器)这样的效果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YIe8oLiK-1570847392496)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570416645819.png)]

简单说下

在JQ中想要动态渲染这个,需要创建HTML元素,然后append()进去 (append会导致一堆HTML拼接代码

在JS中)

而Vue好像也没有很好的解决办法,除了用组件

  1. 先实现JQ方式下的方法 (JQ下看下有没有什么更好的简化代码的方法)
  2. 再尝试用Vue的组件渲染去优化他

假如我想加个时间显示的需求(每条信息:名字的右边都要显示一下时间)

  1. 无非就是加数据的交互上,多了一个时间字段,那么要改的地方就是时间字段,

    以及组装信息类

  2. 以及界面的数据渲染

JS时间格式化:https://blog.csdn.net/qq_33242126/article/details/79279322

(new Date()添加函数Format原型)

Date.prototype.Format = function(formatStr)  {
     
    var str = formatStr;
    var Week = ['日','一','二','三','四','五','六'];

    str=str.replace(/yyyy|YYYY/,this.getFullYear());
    str=str.replace(/yy|YY/,(this.getYear() % 100)>9?(this.getYear() % 100).toString():'0' + (this.getYear() % 100));

    str=str.replace(/MM/,this.getMonth()>9?(this.getMonth()+1).toString():'0' + this.getMonth());
    str=str.replace(/M/g,this.getMonth()+1);


    str=str.replace(/w|W/g,Week[this.getDay()]);

    str=str.replace(/dd|DD/,this.getDate()>9?this.getDate().toString():'0' + this.getDate());
    str=str.replace(/d|D/g,this.getDate());

    str=str.replace(/hh|HH/,this.getHours()>9?this.getHours().toString():'0' + this.getHours());
    str=str.replace(/h|H/g,this.getHours());
    str=str.replace(/mm/,this.getMinutes()>9?this.getMinutes().toString():'0' + this.getMinutes());
    str=str.replace(/m/g,this.getMinutes());

    str=str.replace(/ss|SS/,this.getSeconds()>9?this.getSeconds().toString():'0' + this.getSeconds());
    str=str.replace(/s|S/g,this.getSeconds());

    return str;
};

15.7 WebSocket的关闭onClose()

    //关闭连接
    @OnClose
    public void onClose(Session session, CloseReason closeReason){
     
        //1. 在线人数-1
        onlineUserCount.decrementAndGet();

        //2.移除map里的东西   (通过迭代器删除)
        Iterator<Map.Entry<HttpSession, ChatWebSocket>> iterator = onlineUsers.entrySet().iterator();
        while (iterator.hasNext()){
     
            Map.Entry<HttpSession, ChatWebSocket> entry = iterator.next();
            if(entry.getKey() == httpSession){
     
                iterator.remove();
                break;
            }
        }

        //3. 关闭会话
        try {
     
            session.close(closeReason);
        } catch (IOException e) {
     
            e.printStackTrace();
        }

        //4.重新更新好友列表 ()
        String names = getOnlineUserNames();
        String respMsg = MessageUtil.getRespMsg(MessageUtil.TYPE_USER, "", "", names);
        broadcastUsersList(respMsg);

        //5. 系统通知好友下线
        broadcastOnOrOffLine(OFF_LINE);
    }

一定要加上onError,不然前端直接关闭会话,会报错

    @OnError
    public void onError(Session session, Throwable throwable){
     
        throwable.printStackTrace();
        try {
     
            session.close();
            broadcastOnOrOffLine(OFF_LINE);
        } catch (IOException e) {
     
            e.printStackTrace();
        }
    }

15.8 WebSocket聊天室完整代码

@ServerEndpoint(value = "/websocket",configurator = getHttpSessionConfigurator.class)
public class ChatWebSocket {
     
    private static final String ON_LINE = "on";
    private static final String OFF_LINE = "off";

    private HttpSession httpSession;
    private Session session;
    private static Map<HttpSession,ChatWebSocket> onlineUsers = new HashMap<>();

    private static AtomicInteger onlineUserCount = new AtomicInteger(0);

    @OnOpen
    public void onOpen(Session session, EndpointConfig config){
     
        this.session = session;

        //1. 登陆用户数+1
        onlineUserCount.incrementAndGet();

        //2. 获取HttpSession
        HttpSession hSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        this.httpSession = hSession;

        //3. 添加进Map中
        onlineUsers.put(hSession,this);

        //4. 获取好友(可选择的聊天对象)列表  (在线用户列表)
        String names = getOnlineUserNames();

        //5. 组装响应信息
        String respMsg = MessageUtil.getRespMsg(MessageUtil.TYPE_USER, "", "", names);

        //测试用:看看这个ChatWebSocket是单例的还是一个会话,就创建一个
        System.out.println("当前登陆用户为:"+httpSession.getAttribute("username")+",Endpoint: "+hashCode());

        //6. 广播信息 (更新用户列表 + 广播用户上线的消息)
        broadcastUsersList(respMsg);
        broadcastOnOrOffLine(ON_LINE);
    }

    @OnMessage
    public void onMessage(String msg,Session session){
     
        // 1.先获取信息 (规定前端也是以JSON字符串格式传过来的)
        Map<String,String> map = JSON.parseObject(msg, Map.class);
        String fromName = map.get("fromName");
        String toName = map.get("toName");
        String content = map.get("content");

        boolean online = false;

        //组装信息
        String respMsg = MessageUtil.getRespMsgWithTime(MessageUtil.TYPE_MESSAGE, fromName, toName, content);

        //2. 判断信息是广播还是单播 (广播对象是broadcast)
        if(toName.equals("broadcast")){
     
            //广播
            broadcastAllUser(respMsg);
        }else{
     
            //单播
            //2.2. 先判断对方是否在线
            for(Map.Entry<HttpSession,ChatWebSocket> entry:onlineUsers.entrySet()){
     
                if(entry.getKey().getAttribute("username").equals(toName)){
     
                    online = true;
                    break;
                }
            }
            if(!online){
     
                return;
            }

            //2.3 在线,找出指定用户,发送信息
            for(Map.Entry<HttpSession,ChatWebSocket> entry:onlineUsers.entrySet()){
     
                if(entry.getKey().getAttribute("username").equals(toName)){
     
                    try {
     
                        entry.getValue().session.getBasicRemote().sendText(respMsg);
                    } catch (IOException e) {
     
                        e.printStackTrace();
                    }
                    return;
                }
            }
        }
    }

    //关闭连接
    @OnClose
    public void onClose(Session session, CloseReason closeReason){
     
        //1. 在线人数-1
        onlineUserCount.decrementAndGet();

        //2.移除map里的东西   (通过迭代器删除)
        Iterator<Map.Entry<HttpSession, ChatWebSocket>> iterator = onlineUsers.entrySet().iterator();
        while (iterator.hasNext()){
     
            Map.Entry<HttpSession, ChatWebSocket> entry = iterator.next();
            if(entry.getKey() == httpSession){
     
                iterator.remove();
                break;
            }
        }

        //3. 关闭会话
        try {
     
            session.close(closeReason);
        } catch (IOException e) {
     
            e.printStackTrace();
        }

        //4.重新更新好友列表 ()
        String names = getOnlineUserNames();
        String respMsg = MessageUtil.getRespMsg(MessageUtil.TYPE_USER, "", "", names);
        broadcastUsersList(respMsg);

        //5. 系统通知好友下线
        broadcastOnOrOffLine(OFF_LINE);
    }

    @OnError
    public void onError(Session session, Throwable throwable){
     
        throwable.printStackTrace();
        try {
     
            session.close();
            broadcastOnOrOffLine(OFF_LINE);
        } catch (IOException e) {
     
            e.printStackTrace();
        }
    }

    //广播 要群发的信息
    private void broadcastAllUser(String respMsg){
     
        for(Map.Entry<HttpSession,ChatWebSocket> entry:onlineUsers.entrySet()){
     
            try {
     
                //不发给自己
                if(entry.getKey() != httpSession){
     
                    entry.getValue().session.getBasicRemote().sendText(respMsg);
                }
            } catch (IOException e) {
     
                e.printStackTrace();
            }
        }
    }

    //广播上下线提醒
    private void broadcastOnOrOffLine(String lineType){
     
        String state;
        if(lineType.equals(ON_LINE)){
     
            state = "已上线";
        }else{
     
            state = "已下线";
        }

        String username = (String) httpSession.getAttribute("username");
        for(Map.Entry<HttpSession,ChatWebSocket> entry:onlineUsers.entrySet()){
     
            if(entry.getKey() != httpSession){
     
                //不包括自己
                try {
     
                    ChatWebSocket socket = entry.getValue();
                    String respMsg = MessageUtil.getRespMsg(MessageUtil.TYPE_MESSAGE, "", "", "您的好友" + username + state);
                    socket.session.getBasicRemote().sendText(respMsg);

                    String curTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
                    String onlineTime = MessageUtil.getRespMsg(MessageUtil.TYPE_MESSAGE,username,"","当前时间:"+curTime);
                    socket.session.getBasicRemote().sendText(onlineTime);
                } catch (IOException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }

    //广播好友列表
    private void broadcastUsersList(String respMsg) {
     
        if(onlineUsers.size()>0){
     
            for(Map.Entry<HttpSession,ChatWebSocket> entry:onlineUsers.entrySet()){
     
                ChatWebSocket socket = entry.getValue();
                try {
     
                    socket.session.getBasicRemote().sendText(respMsg);
                } catch (IOException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }

    //获取好友列表(不包括自己)
    private String getOnlineUserNames() {
     
        List<String> list = new ArrayList<>();
        for(HttpSession session:onlineUsers.keySet()){
     
                String username = (String) session.getAttribute("username");
                list.add(username);
        }
        if(list.size()>0){
     
            return String.join(",",list);
        }else{
     
            return "";
        }
    }
}

获取HttpSession

/**
 * 在握手时获取HttpSession,存进ServerEndpointConfig里
 */
public class getHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
     
    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
     
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}

目录结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zwEtgoti-1570847392497)(D:\my_heart_note\tomcat\笔记\Tomcat.assets\1570847350165.png)]

你可能感兴趣的:(Tomcat,Tomcat)