上篇介绍了tomcat的结构:https://blog.csdn.net/hancoder/article/details/118466983
tomcat由两大部分组成:
对应到源码类上的结构为:
介绍过,tomcat里可以有多个service
每个service可以有多个连接器
注意是连接器,不是连接数,连接器获取连接后交给容器,但在这两者内有肯定逻辑
连接器用于获取连接,由tcp协议封装成http,生成request请求,然后用适配器把request请求转成servlet request请求,交给容器
coyote
就是连接器的封装
catalina
就是servlet container容器
Coyote
是Tomcat的连接器的总称,我们要访问tomcat必须经过连接器。
每个连接器在源码里也被称为protocolHandler
(协议处理器),源码中有很多协议处理器,ProtocolHandler
只是接口,具体实现类稍后介绍
下面的话随便看看,无所谓的
Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Catalina 容器提供了统一的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦。Coyote 将Socket 输入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写入输出流 。
Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而是在Catalina 中将他们进一步封装为
ServletRequest
和ServletResponse
。
在Coyote中 , Tomcat支持的多种I/O模型和应用层协议,具体包含哪些IO模型和应用层协议,请看下表:
IO模型 | 描述 |
---|---|
BIO | 自8.5/9.0 版本起,已被移除 |
NIO | 非阻塞I/O,采用Java NIO类库实现。 |
NIO2 | 异步I/O,采用JDK 7最新的NIO2类库实现。 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库。 |
在 8.0 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2还是 APR, 在性能方面均优于以往的BIO。 如果采用APR, 甚至可以达到 Apache HTTP Server 的影响性能。
应用层协议 | 描述 |
---|---|
HTTP/1.1 | 这是大部分Web应用采用的访问协议。 |
AJP | 用于和Web服务器集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3。 |
HTTP/2 | HTTP 2.0大幅度的提升了Web性能。下一代HTTP协议 , 自8.5以及9.0版本之后支持。 |
通过协议和通信方式的组合,我们可以又把连接器分得更细了,也就是Tomcat中有6个实现类
Http11Protocol
:11代表是http1.1Http11NioProtocol
tomcat在默认启动时,也是开启了2个连接器,每个连接器可以设置处理不同的协议。如图
Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service
组件。这里请你注意,Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
协议处理器
Connector就是使用ProtocolHandler
来处理请求的,它是EndPoint和Process的组合,分别处理TCP/IP和HTTP/AJP协议。
通过两两组合就是前面实现类写的那几个,如Http11Protocol
使用的是普通BIO Socket
处理HTTP,Http11NioProtocol
使用的是NioSocket
处理http。
其中ProtocolHandler由包含了三个部件:Endpoint、Processor、Adapter。
(1)Endpoint
用来处理底层Socket的网络连接,Processor
用于将Endpoint接收到的Socket封装成Request,Adapter
用于将Request交给Container进行具体的处理。
(2)Endpoint由于是处理底层的Socket网络连接,因此Endpoint
是用来实现TCP/IP
协议的,而Processor
用来实现HTTP
协议的,Adapter
将请求适配到Servlet容器进行具体的处理。
(3)Endpoint的抽象实现类AbstractEndpoint里面定义了Acceptor
和AsyncTimeout
两个内部类和一个Handler
接口。
Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?
连接器中的各个组件的作用如下:
Acceptor
和SocketProcessor
。
Executor
),后面会详细介绍EndPoint的实现类是
Tomcat如何扩展原生的Java线程池。
EndPoint接收了socket请求会把请求发送给processor。Processor处理HTTP
或AJP
协议
Processor : Coyote 协议处理接口 ,如果说EndPoint是用来实现TCP/IP
协议的,那么Processor用来实现HTTP
协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request
和Response
对象,并通过Adapter
将其提交到容器处理,
Processor是对应用层协议的抽象。
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的ServletRequest
类来“存放”这些请求信息。ProtocolHandler
接口负责解析请求并生成Request
类。但是这个Request对象不是标准的ServletRequest
,也就意味着,不能用Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter
,这是适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Request对象,CoyoteAdapter负责将Request转成ServletRequest
,再调用容器的Service方法。
其中ProtocolHandler(连接器)又含了三个部件:Endpoint、Processor、Adapter。
Endpoint
:用来处理底层Socket的网络连接。
Acceptor
和AsyncTimeout
两个内部类和一个Handler
接口。Processor
:用于将Endpoint接收到的Socket封装成Request。他是一个线程池,我们每个【发生事件的连接】去线程池里去执行Adapter
:线程池里的连接为了去找container,他拿的是request
,但是容器接收的是http request
,那适配器就转一下
在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint
对象。JIoEndpoint维护了Acceptor和Worker:
配置了其他线程池,原理与Worker类似。在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint
对象。NIoEndpoint中除了包含Acceptor和Worker外,还使用了Poller
,处理流程如下图所示
Acceptor
接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller
,而Poller是实现NIO的关键。Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller
中,维护了一个Selector
对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。与BIO类似,Worker也可以被自定义的线程池代替。
通过上述过程可以看出,在NIoEndpoint处理请求的过程中,无论是Acceptor接收socket,还是线程处理请求,使用的仍然是阻塞方式;但在“读取socket并交给Worker中的线程”的这个过程中,使用非阻塞的NIO实现,这是NIO模式与BIO模式的最主要区别(其他区别对性能影响较小,暂时略去不提)。而这个区别,在并发量较大的情形下可以带来Tomcat效率的显著提升:
但是BIO线程池的思想是主线程获取到一个连接后,交给线程池去处理,也就是主线程不断accept()阻塞,而每个线程对应一个连接,accept()获取到一个连接后,去线程池里去操作read操作,会阻塞,处理完read等操作后释放连接,这个线程给别的连接用。如果线程池满了,那么将由排队、拒绝策略等操作,那么连接数还是等于线程数
而对于NIO,accept()虽然是阻塞的,但是接收到连接后注册到了selector,selector采用事件机制,连接可以都注册到上面,有了事件才去线程池里处理。因为可以注册很多事件,所以连接数可以大于线程数
目前大多数HTTP请求使用的是长连接(HTTP/1.1默认keep-alive为true),而长连接意味着,一个TCP的socket在当前请求结束后,如果没有新的请求到来,socket不会立马释放,而是等timeout后再释放。如果使用BIO,“读取socket并交给Worker中的线程”这个过程是阻塞的,也就意味着在socket等待下一个请求或等待释放的过程中,处理这个socket的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。而使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。
在这里我要用NIO的知识对应到连接器源码中
我在上篇文章说过bind(addr,backlog)
的问题,backlog是阻塞的个数,在源码中叫Accept[backlog]
Acceptor定义在AbstractEndpoint类中,他实现了Runnable,是个线程
源码中Accept[backlog]
什么意思:就是说创建几个线程执行accept()
,serverSock.accept();
这个逻辑可以在NioEndpoint的子类Acceptor.run()中看到【他继承了AbstractEndpoint中的Acceptor,实现了run()】
学过NIO的都知道他的意思吧,他是接收客户端的socket,然后去读数据。
与之前NIO有点不同的是,我们之前把serversocket注册到了selector,accept()
方法也是selector通过事件知道,但tomcat中我看单独线程accept()
,接收到后才注册到selector,selector只发送读写事件。
最大队列问题之前解释过了,达到最大连接数后,进去等待队列(这个等待队列有点线程池等待队列的意思,但因为有selector的关系,又感觉不全是)。等待队列满了之后再accept()
到连接后,还没注册到selector上了,就阻塞了,不让注册了,直到等待队列空出来。所以说tomcat的最大连接数是maxConnections+maxAcceptors
poller对应在NIO中的selector,因为在Poller的构造器中有一句this.selector = Selector.open()
另外poller还是一个线程,实现了Runnable接口,也就是说让每个线程去执行select()
操作,负责处理读写事件
从acceptor获取到的socket都注册到selector上,就是注册到了poller上,poller进行select()
得到发生事件的key
另外,源码中poller也是有数组的,源码中默认是new Poller[2]
,对应到前面连接器的不同协议,如HTTP
和AJP
processor就是当select()
得到发生事件的key后,比如有多个socket有read事件,我们不能让他们在一个线程里一次执行吧?我们把每个socket.read()
放到线程池里执行。
这时候需要包装socket.read()这个操作,把每个任务包装为了processor线程。
processor线程包装的时候,是可以从缓存中拿别人用过的processor对象的。
让processor去线程池里执行processor.doRun()
,里面会执行processor.process()
在service标签中,配置了一个线程池参数,他是多个Poller共享的,也就是多个连接器共享的,然后我们可以自己写executor标签后,让connector标签引用它,这样每个连接器就有了自己的线程池。
Tomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的servlet容器。
Catalina 是Servlet 容器实现,包含了之前讲到的所有的容器组件,以及后续章节涉及到的安全、会话、集群、管理等Servlet 容器架构的各个方面。它通过松耦合的方式集成Coyote,以完成按照请求协议进行数据读写。同时,它还包括我们的启动入口、Shell程序等。
Tomcat 的模块分层结构图, 如下:他跟源码中的包一一对应
Tomcat 本质上就是一款 Servlet 容器, 因此Catalina 才是 Tomcat 的核心 , 其他模块都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供JSP引擎,Naming 提供JNDI 服务,Juli 提供日志服务。
Catalina 的主要组件结构如下:
Catalina 各个组件的职责:
组件 | 职责 |
---|---|
Catalina | 负责解析Tomcat的配置文件 , 以此来创建服务器Server组件,并根据命令来对其进行管理 |
Server | 服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动 Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供了一种优雅的启动和关闭整个系统的方式 |
Service | 服务是Server内部的组件,一个Server包含多个Service。它将若干个 Connector组件绑定到一个Container(Engine)上 |
Connector | 连接器,处理与客户端的通信,它负责接收客户请求,然后转给相关的容器处理,最后向客户返回响应结果 |
Container | 容器,负责处理用户的servlet请求,并返回对象给web用户的模块 |
如下Service的源码,可以获取其中的容器
Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。, Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。
容器 | 描述 |
---|---|
Engine | 表示整个Catalina的Servlet引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine,但是一个引擎可包含多个Host |
Host | 代表一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context |
Context | 表示一个Web应用程序, 一个Web应用可包含多个Wrapper |
Wrapper | 表示一个Servlet,Wrapper 作为容器中的最底层,不能包含子容器 |
可以看看Tomcat的server.xml
。Tomcat采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件按照一定的格式要求配置在这个顶层容器中。
<Server>
<Service>
<Connector/>
<Connector/>
<Engine>
<Host>
<Context>Context>
Host>
Engine>
Service>
Server>
这些容器具有父子关系,形成一个树形结构(设计模式中的组合模式)。没错,Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。(而调用时使用的是责任链模式)
Container 接口中提供了以下方法(截图中知识一部分方法) :
在上面的接口看到了getParent、SetParent、addChild和removeChild等方法。
Container接口扩展了LifeCycle
接口,LifeCycle接口用来统一管理各组件的生命周期
Executor元素代表Tomcat中的线程池,可以由其他组件共享使用;要使用该线程池,组件需要通过executor属性指定该线程池。
Executor是Service元素的内嵌元素。一般来说,使用线程池的是Connector组件;为了使Connector能使用线程池,Executor元素应该放在Connector前面。Executor与Connector的配置举例如下:
<Executor name="tomcatThreadPool" namePrefix ="catalina-exec-" maxThreads="150" minSpareThreads="4" />
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" acceptCount="1000" />
Executor的主要属性包括:
上面介绍了Tomcat连接数、线程数的概念以及如何设置,下面说明如何查看服务器中的连接数和线程数。
查看服务器的状态,大致分为两种方案:(1)使用现成的工具,(2)直接使用Linux的命令查看。
现成的工具,如JDK自带的jconsole
工具可以方便的查看线程信息(此外还可以查看CPU、内存、类、JVM基本信息等),Tomcat自带的manager
,收费工具New Relic
等。下图是jconsole
查看线程信息的界面:
下面说一下如何通过Linux命令行,查看服务器中的连接数和线程数。
步骤 :
启动tomcat , 需要调用 bin/startup.bat
(在linux 目录下 , 需要调用 bin/startup.sh), 在startup.bat 脚本中, 调用了catalina.bat
。
在catalina.bat 脚本文件中,调用了BootStrap
中的main()
方法。
3)在BootStrap 的main 方法中调用了 init
方法 , 来创建Catalina 及 初始化类加载器。
4)在BootStrap 的main 方法中调用了 load
方法 , 在其中又调用了Catalina的load方法。
5)在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用于解析 XML。
6) 然后在调用后续组件的初始化操作 。。。
加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求。
由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期的接口,从而具有了以下生命周期中的核心方法:
各组件的默认实现
上面我们提到的Server、Service、Engine、Host、Context都是接口, 下图中罗列了这些接口的默认实现类。
当前对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint接口, 但是有一个抽象类 AbstractEndpoint
,其下有三个实现类:
NioEndpoint
:连接器的NIO模型(Tomcat8.5默认)Nio2Endpoint
:连接器的NIO2模型AprEndpoint
:连接器的APR模型ProtocolHandler : Coyote协议接口,通过封装Endpoint和Processor ,实现针对具体协议的处理功能。
Tomcat按照协议和IO提供了6个实现类。
AJP协议:
HTTP协议:
在init的过程中,init是LigecycleBase实现的,而initInternal是具体子类实现的。模板方法设计模式
从哪看起:org.apache.catalina.startup.BootStrap.java
‐‐‐‐>main()
从启动流程图中以及源码中,我们可以看出Tomcat的启动过程非常标准化, 统一按照生命周期管理接口Lifecycle的定义进行启动。首先调用init() 方法进行组件的逐级初始化操作,然后再调用start()方法进行启动。
每一级的组件除了完成自身的处理外,还要负责调用子组件响应的生命周期管理方法,组件与组件之间是松耦合的,因为我们可以很容易的通过配置文件进行修改和替换。
serverSocket.socket().bind()
逻辑// 调用start方法,一次次调用catalina.start、server、service、engine、host、context... 又去调用executor,connector、protocolHandler
在endpoint最后调用startAcceptorThread()开启接口线程;
// AbstractEndpoint.java
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
// 接收。实现了Runnable。
acceptors = new Acceptor[count];
// 创建accept线程并启动
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
// 开启线程,线程里面会调用serverSocket.accept()
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}
new ThreadPoolExecutor(
serverSock.accept();
每个连接器对应一个线程池,连接器指定了端口
appbase是webapps目录
里面包含Acceptor
public void bind(SocketAddress endpoint,//绑定到的IP地址和端口号。 int backlog);//请求进入连接队列的最大长度。 // 将ServerSocket绑定到特定地址(IP地址和端口号)。 如果地址为null ,则系统将接收临时端口和有效的本地地址来绑定套接字。 // backlog参数是套接字上请求的最大挂起连接数。 其确切语义是实现具体的。 特别地,实现可以施加最大长度,或者可以选择忽略参数altogther。 提供的价值应大于0 。 如果小于或等于0 ,则将使用实现特定的默认值。
里面的设计模式:
直接在webapps里新建目录然后开始注意格式编写
创建servlet,修改web.xml:
为了排序chrome浏览器还会请求一次图标,所以我们用火狐浏览器
设计了这么多层次的容器,Tomcat是怎么确定每一个请求应该由哪个Wrapper容器里的
Servlet来处理的呢?答案是,Tomcat是用Mapper组件来完成这个任务的。
Mapper组件的功能就是将用户请求的URL定位到一个Servlet,它的工作原理是:
Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里
Servlet映射的路径,你可以想象这些配置信息就是一个多层次的Map。
当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里去查找,就能定位到一个Servlet。请你注意,一个请求URL最后只会定位到一个Wrapper容器,也就是一个Servlet。
下面的示意图中 , 就描述了 当用户请求链接 http://www.itcast.cn/bbs/findAll 之后, 是如何找到最终处理业务逻辑的servlet
http://www.itcast.cn/bbs/findAll
那上面这幅图只是描述了根据请求的URL如何查找到需要执行的Servlet , 那么下面我们再来解析一下 , 从Tomcat的设计架构层面来分析Tomcat的请求处理。
endpoint接收到请求
accept()到之后,交给poller注册,poller发送事件后交给processor进行read
processKey()在他里面创建成processor线程对象然后提交交给线程池
步骤如下:
Executor
处理,开始执行请求响应任务。Request
对象。Pipeline
。Valve
负责部分处理逻辑。执行完Valve后会执行基础的 Valve–StandardEngineValve,负责调用Host容器的Pipeline。Pipeline
。Pipeline
。处理方法
。// HttpService
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
在前面所讲解的Tomcat的整体架构中,我们发现Tomcat中的各个组件各司其职,组件之间松耦合,确保了整体架构的可伸缩性和可拓展性,那么在组件内部,如何增强组件的灵活性和拓展性呢? 在Tomcat中,每个Container组件采用责任链模式来完成具体的请求处理。
在Tomcat中定义了Pipeline
和 Valve
两个接口,Pipeline 用于构建责任链, 后者代表责任链上的每个处理器。Pipeline 中维护了一个基础的Valve
,它始终位于Pipeline的末端(最后执行),封装了具体的请求处理和输出响应的过程。当然,我们也可以调用addValve()
方法, 为Pipeline 添加其他的Valve, 后添加的Valve 位于基础的Valve之前,并按照添加顺序执行。Pipiline通过获得首个Valve来启动整合链条的执行 。
但是!Pipeline-Valve使用的责任链模式和普通的责任链模式有些不同!区别主要有以下两点:
(1)每个Pipeline都有特定的Valve,而且是在管道的最后一个执行,这个Valve叫做BaseValve,BaseValve是不可删除的;
(2)在上层容器的管道的BaseValve中会调用下层容器的管道。
我们知道Container包含四个子容器,而这四个子容器对应的BaseValve分别在:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。
Pipeline的处理流程图如下(图D):
(1)Connector在接收到请求后会首先调用最顶层容器的Pipeline来处理,这里的最顶层容器的Pipeline就是EnginePipeline(Engine的管道);
(2)在Engine的管道中依次会执行EngineValve1、EngineValve2等等,最后会执行StandardEngineValve,在StandardEngineValve中会调用Host管道,然后再依次执行Host的HostValve1、HostValve2等,最后在执行StandardHostValve,然后再依次调用Context的管道和Wrapper的管道,最后执行到StandardWrapperValve。
(3)当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的Filter和Servlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法,这样请求就得到了处理!
(4)当所有的Pipeline-Valve都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给Connector了,Connector在通过Socket的方式将结果返回给客户端。
Jasper 简介
对于基于JSP 的web应用来说,我们可以直接在JSP页面中编写 Java代码,添加第三方的标签库,以及使用EL表达式。但是无论经过何种形式的处理,最终输出到客户端的都是标准的HTML页面(包含js ,css…),并不包含任何的java相关的语法。 也就是说, 我们可以把jsp看做是一种运行在服务端的脚本。 那么服务器是如何将 JSP页面转换为HTML页面的呢?
Jasper模块是Tomcat的JSP核心引擎,我们知道JSP本质上是一个Servlet。Tomcat使用Jasper对JSP语法进行解析,生成Servlet并生成Class字节码,用户在进行访问jsp时,会访问Servlet,最终将访问的结果直接响应在浏览器端 。另外,在运行的时候,Jasper还会检测JSP文件是否修改,如果修改,则会重新编译JSP文件。
JSP 编译方式
运行时编译
Tomcat 并不会在启动Web应用的时候自动编译JSP文件, 而是在客户端第一次请求时,才编译需要访问的JSP文件。
创建一个web项目, 并编写JSP代码 :
3.2.1.1 编译过程
Tomcat 在默认的web.xml 中配置了一个org.apache.jasper.servlet.JspServlet,用于处理所有的.jsp 或 .jspx 结尾的请求,该Servlet 实现即是运行时编译的入口。
JspServlet 处理流程图:
编译结果
如果在 tomcat/conf/web.xml 中配置了参数scratchdir , 则jsp编译后的结果,就会存储在该目录下 。
如果没有配置该选项, 则会将编译后的结果,存储在Tomcat安装目录下的work/Catalina(Engine名称)/localhost(Host名称)/Context名称 。 假设项目名称为jsp_demo 01。
如果使用的是 IDEA 开发工具集成Tomcat 访问web工程中的jsp , 编译后的结果,存放在 :
C:\Users\Administrator.IntelliJIdea2019.1\system\tomcat_project_tomcat\work\Catalina\localhost\jsp_demo_01_war_exploded\org\apache\jsp
除了运行时编译,我们还可以直接在Web应用启动时, 一次性将Web应用中的所有的JSP页面一次性编译完成。在这种情况下,Web应用运行过程中,便可以不必再进行实时编译,而是直接调用JSP页面对应的Servlet 完成请求处理, 从而提升系统性能。
Tomcat 提供了一个Shell程序JspC,用于支持JSP预编译,而且在Tomcat的安装目录下提供了一个 catalina-tasks.xml 文件声明了Tomcat 支持的Ant任务, 因此,我们很容易使用 Ant 来执行JSP 预编译 。(要想使用这种方式,必须得确保在此之前已经下载并安装了Apache Ant)。
编译后的.class 字节码文件及源码 :
编译后的.class 字节码文件及源码 :
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,org.apache.jasper.runtime.Js pSourceImports {
private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory();
private static java.util.Map<java.lang.String,java.lang.Long>
_jspx_dependants;
static {
_jspx_dependants = new java.util.HashMap<java.lang.String,java.lang.Long>(2);
_jspx_dependants.put("jar:file:/D:/DevelopProgramFile/apache‐tomcat‐ 8.5.42‐windows‐x64/apache‐tomcat‐8.5.42/webapps/jsp_demo_01/WEB‐ INF/lib/standard.jar!/META‐INF/c.tld", Long.valueOf(1098682290000L));
_jspx_dependants.put("/WEB‐INF/lib/standard.jar", Long.valueOf(1490343635913L));
}
private static final java.util.Set<java.lang.String>
_jspx_imports_packages;
private static final java.util.Set<java.lang.String>
_jspx_imports_classes;
static {
_jspx_imports_packages = new java.util.HashSet<>();
_jspx_imports_packages.add("javax.servlet");
_jspx_imports_packages.add("javax.servlet.http");
_jspx_imports_packages.add("javax.servlet.jsp");
_jspx_imports_classes = new java.util.HashSet<>();
_jspx_imports_classes.add("java.util.Date");
_jspx_imports_classes.add("java.text.SimpleDateFormat");
_jspx_imports_classes.add("java.text.DateFormat");
}
private volatile javax.el.ExpressionFactory _el_expressionfactory;
private volatile org.apache.tomcat.InstanceManager
_jsp_instancemanager;
public java.util.Map<java.lang.String,java.lang.Long>getDependants() { return _jspx_dependants;
}
public java.util.Set<java.lang.String>getPackageImports() { return _jspx_imports_packages;
}
public java.util.Set<java.lang.String>getClassImports() { return _jspx_imports_classes;
}
public javax.el.ExpressionFactory _jsp_getExpressionFactory() { if (_el_expressionfactory == null) {
synchronized (this) {
if (_el_expressionfactory == null) {
_el_expressionfactory =
_jspxFactory.getJspApplicationContext(getServletConfig().getServletContex t()).getExpressionFactory();
}
}
}
return _el_expressionfactory;
}
public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() { if (_jsp_instancemanager == null) {
synchronized (this) {
if (_jsp_instancemanager == null) {
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getSe rvletConfig());
}
}
}
return _jsp_instancemanager;
}
public void _jspInit() {
}
public void _jspDestroy() {
}
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
final java.lang.String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) &&
!"HEAD".equals(_jspx_method) &&
!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType()))
{
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
return;
}
final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html;charset=UTF‐8");
pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write("\n");
out.write("\n");
out.write("\n");
out.write("\n");
out.write("\n"); out.write("\n"); out.write(" \n");
out.write(" $Title$ \n"); out.write(" \n");
out.write(" \n"); out.write(" ");
DateFormat dateFormat = new SimpleDateFormat("yyyy‐MM‐dd HH:mm:ss");
String format = dateFormat.format(new Date());
out.write("\n");
out.write(" Hello , Java Server Page 。。。。\n"); out.write("\n");
out.write("
\n"); out.write("\n");
out.write(" "); out.print( format ); out.write("\n");
out.write("\n"); out.write(" \n"); out.write("\n");
} catch (java.lang.Throwable t) {
if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out;
if (out != null && out.getBufferSize() != 0) try {
if (response.isCommitted()) { out.flush();
} else { out.clearBuffer();
}
} catch (java.io.IOException e) {} if (_jspx_page_context != null)
_jspx_page_context.handlePageException(t); else throw new ServletException(t);
由编译后的源码解读, 可以分析出以下几点 :
1) 其类名为 index_jsp , 继承自 org.apache.jasper.runtime.HttpJspBase , 该类是
HttpServlet 的子类 , 所以jsp 本质就是一个Servlet 。
2) 通过属性 _jspx_dependants 保存了当前JSP页面依赖的资源, 包含引入的外部的JSP页面、导入的标签、标签所在的jar包等,便于后续处理过程中使用(如重新编译检测,因此它以Map形式保存了每个资源的上次修改时间)。
3) 通过属性 _jspx_imports_packages 存放导入的 java 包, 默认导入 javax.servlet ,
javax.servlet.http, javax.servlet.jsp 。
4) 通过属性 _jspx_imports_classes 存放导入的类, 通过import 指令导入的
DateFormat 、SimpleDateFormat 、Date 都会包含在该集合中。
_jspx_imports_packages 和 _jspx_imports_classes 属性主要用于配置 EL 引擎上下文
。
5) 请求处理由方法 _jspService 完成 , 而在父类 HttpJspBase 中的service 方法通过模板方法模式 , 调用了子类的 _jspService 方法。
1) _jspService 方法中定义了几个重要的局部变量 : pageContext 、Session、
application、config、out、page。由于整个页面的输出有 _jspService 方法完成,因此这些变量和参数会对整个JSP页面生效。 这也是我们为什么可以在JSP页面使用这些变量的原因。
2) 指定文档类型的指令 (page) 最终转换为 response.setContentType() 方法调用。
3) 对于每一行的静态内容(HTML) , 调用 out.write 输出。
1) 对于 <% … %>中的java 代码 , 将直接转换为 Servlet 类中的代码。 如果在 Java代码中嵌入了静态文件, 则同样调用 out.write 输出。
JSP 编译过程如下:
Compiler 编译工作主要包含代码生成 和 编译两部分 :代码生成
1) Compiler 通过一个 PageInfo 对象保存JSP 页面编译过程中的各种配置,这些配置可能来源于 Web 应用初始化参数, 也可能来源于JSP页面的指令配置(如 page ,
include)。
2) 调用ParserController 解析指令节点, 验证其是否合法,同时将配置信息保存到
PageInfo 中, 用于控制代码生成。
3) 调用ParserController 解析整个页面, 由于 JSP 是逐行解析, 所以对于每一行会创建一个具体的Node 对象。如 静态文本(TemplateText)、Java代码(Scriptlet)、定制标签(CustomTag)、Include指令(IncludeDirective)。
4) 验证除指令外其他所有节点的合法性, 如 脚本、定制标签、EL表达式等。
5) 收集除指令外其他节点的页面配置信息。
6) 编译并加载当前 JSP 页面依赖的标签
7) 对于JSP页面的EL表达式,生成对应的映射函数。
8) 生成JSP页面对应的Servlet 类源代码编译代码生成完成后, Compiler 还会生成 SMAP 信息。 如果配置生成 SMAP 信息,
Compiler 则会在编译阶段将SMAP 信息写到class 文件中 。
在编译阶段, Compiler 的两个实现 AntCompiler 和 JDTCompiler 分别调用先关框架的API 进行源代码编译。
对于 AntCompiler 来说, 构造一个 Ant 的javac 的任务完成编译。
对于 JDTCompiler 来说, 调用 org.eclipse.jdt.internal.compiler.Compiler 完成编译
websocket是HTML5新增的协议,它的目的是在浏览器和服器之间建立一个不受限的双向通信的通道,比如说。服箬器可以在任意时刻发送消息给浏览器。
为什么传统的http协议不能做到websocket实现的功能?这是因为http协议是一个请求-响应
协议,请求必须先由浏览器发给服务器,服务器才能响应这个清求,再把数据发送给浏览器。换句话说,浏览器不主动话求,服务器是没法主动发数据名浏览器的。
这样一来,要在浏览器中搞一个实时聊天,或者在线多人游戏的话就没法实现了,只能借助flash这些插件。也有人说,http协议实也能实现啊,比如用轮询或者comet。轮询是指浏览器通过js启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新港息。这个机制的骁点一是实时性不够,一是频繁的求会给报务器带来极大的压力。
comet本质上也是轮询,但是在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。这个机制暫时地解决了实时性问题,但是它带来了新的问题:以多线程式运行的服器会让大部分线程大部分时间都处于扌圭起状态,极大地浪报箬器资源。另外,一个http连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网矢是我们不可控的,这就要求comet连接必须定期发一些ping数据表示连接”正常工作“。
websocket不是全新的协议,而是利用了HTTP协议来建立连接。
websocket必须由浏览器发起,因为请求协议是一个标准的HTTP请求,ws://localhost:8080
connection:upgrade
upgrade:websocket
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
JavaEE 7中出了JSR-356:Java API for WebSocket规范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat从7.0.27开始支持 WebSocket,从7.0.47开始支持JSR-356,下面的Demo代码也是需要部署在Tomcat7.0.47以上的版本才能运行。
javax.websocket.Endpoint
@ServerEndpoint
相关注解。fastjson tomcat-websocket websocket-api
tomcat中有上面的包
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>4.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>jstlgroupId>
<artifactId>jstlartifactId>
<version>1.2version>
dependency>
<dependency>
<groupId>taglibsgroupId>
<artifactId>standardartifactId>
<version>1.1.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-messagingartifactId>
<version>4.1.7.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-websocketartifactId>
<version>4.1.7.RELEASEversion>
dependency>
<dependency>
<groupId>javax.websocketgroupId>
<artifactId>javax.websocket-apiartifactId>
<version>1.0version>
<scope>providedscope>
dependency>
package me.gacl.websocket;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
/**
@ServerEndpoint 注解是一个类层次的注解,
它的功能主要是将目前的类定义成一个websocket服务器端,
注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@ServerEndpoint("/websocket")
public class WebSocketTest {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static AtomicInteger onlineCount = 0;
// 线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
// 若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();
// Map
//与某个客户端的连接会话,需要通过它来给客户端发送数据 // websocket的session,还有一个是httpsession
private Session session;
// 如何获取httpsession,可以在注解上加configurator
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session){ //与某个客户端的连接会话,需要通过它来给客户端发送数据
this.session = session;
webSocketSet.add(this); //加入set中
WebSocketTest.onlineCount++; //在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
/* 连接关闭调用的方法 */
@OnClose
public void onClose(){
webSocketSet.remove(this); //从set中删除
WebSocketTest.onlineCount--; //在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
//群发消息
for(WebSocketTest item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/* 发生错误时调用 */
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
/** 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 */
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
}
页面
registerWebSocketHandlers:这个方法是向spring容器注册一个handler地址,我把他理解成requestMapping
addInterceptors:拦截器,当建立websocket连接的时候,我们可以通过继承spring的HttpSessionHandshakeInterceptor来搞事情。
setAllowedOrigins:跨域设置,*
表示所有域名都可以,不限制, 域包括ip:port, 指定*可以是任意的域名,不加的话默认localhost+本服务端口
withSockJS: 这个是应对浏览器不支持websocket协议的时候降级为轮询的处理。
@Configuration
@EnableWebSocket
public class SpringWebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册拦截器
registry.addHandler(webSocketHandler(),"/websocket/socketServer")
.addInterceptors(new SpringWebSocketHandlerInterceptor()).setAllowedOrigins("*");
// 注册拦截器
registry.addHandler(webSocketHandler(), "/sockjs/socketServer").setAllowedOrigins("http://localhost:28180")
.addInterceptors(new SpringWebSocketHandlerInterceptor()).withSockJS();
}
// 注入handler
@Bean
public TextWebSocketHandler webSocketHandler(){
return new SpringWebSocketHandler();
}
}
这个是创建websocket连接是的拦截器,记录建立连接的用户的session以便根据不同session来通信
每个客户端在握手的时候都会创建endpoint实例
public class SpringWebSocketHandlerInterceptor
extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("Before Handshake");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName区分WebSocketHandler,以便定向发送消息
String userName = (String) session.getAttribute("SESSION_USERNAME"); //一般直接保存user实体
if (userName!=null) {
attributes.put("WEBSOCKET_USERID",userName);
}
}
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
给内嵌Tomcat, Jetty or Undertow的websocket servlet自动配置
需要在类路径下放置相关websocket的模块
如果tomcat的WebSocket支持在类路径下被检测到,我们添加一个customizer,这个东西安装了tomcat websocket initializer初始化器
同理,如果Jetty的websocket模块被检测到,。。。。
同理Undertow。。。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, ServerContainer.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureBefore(ServletWebServerFactoryAutoConfiguration.class)
public class WebSocketServletAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, WsSci.class })
static class TomcatWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
TomcatWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new TomcatWebSocketServletWebServerCustomizer();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebSocketServerContainerInitializer.class)
static class JettyWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
JettyWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new JettyWebSocketServletWebServerCustomizer();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(io.undertow.websockets.jsr.Bootstrap.class)
static class UndertowWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
UndertowWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new UndertowWebSocketServletWebServerCustomizer();
}
}
}