Tomcat学习笔记
Tomcat学习笔记
转载请声明作者和出处!!!
本文如有错误欢迎指正,感激不尽。
一、Tomcat概述
1.1 浏览器访问流程
我们先从一个http请求的流程开始
浏览器访问服务器使用的是HTTP协议,HTTP协议是应用层协议,数据传输是TCP/IP协议。我们的tomcat就是一个HTTP服务器。
1.2 tomcat总体架构
上面我们的请求最终会走到HTTP服务器中,而我们的tomcat也是一个http服务器,因为tomcat能够接收并处理http请求。
当请求走到tomcat后,接收到请求之后把请求交给Servlet容器来处理,Servlet 容器通过Servlet接口调⽤业务类。Servlet接口和Servlet容器这⼀整套内容叫作Servlet规范。因此tomcat既有http服务器的功能又按照Servlet规范实现了Servlet容器,所以tomcat又是一个Servlet容器。
tomcat处理流程
当请求到来时:
- HTTP服务器会把请求信息使用ServletRequest对象封装起来
- Servlet容器拿到请求后,根据URL和Service的对应关系,定位到到具体的Servlet中
- 如果Servlet还没有被加载,则通过反射创建这个Servlet,并调用Servlet的init方法来完成初始化。
- 接着调用对应的业务类方法处理请求,然后将结果用ServletResponse封装
- 把ServletResponse对象返回给HTTP服务器,HTTP服务器会把相应发送给客户端。
tomcat总体架构
通过上面的流程我们发现tomcat有两个很重要的功能,一个是与客户端交互,进行socket通信,将字节流和Request/Response进行转换另一个就是Servlet容器业务处理逻辑。
tomcat设计了两个核心组件:连接器(Connector)和容器(Container)来完成这两个功能。
下面我们介绍这两个组件。
1.3 连接器组件Coyote
Coyote是tomcat中连接器的组件名称,客户端通过Coyote和服务器建立连接,发送请求并接受响应,同时它使Catalina与具体的请求协议和IO操作完全解耦。Coyote负责的时具体协议(应用层)和IO(传输层)的相关内容,它将Socket输入转换封装为Request对象,进一步封装后交由Catalina容器进行处理,处理完请求后,Catalina通过Coyote提供的Response对象将结果写入输出流。
Tomcat Coyote支持的协议:
应用层协议 | 描述 |
---|---|
HTTP/1.1 | 大部分web应用采用的协议,也是tomcat默认协议 |
AJP | 用于和WX集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3 |
HTTP/2 | HTTP2.0大幅度提升了Web性能。自8.5以及9.0版本之后支持 |
Tomcat Coyote支持的IO模型:
IO模型 | 描述 |
---|---|
NIO | 非阻塞IO,采用Java NIO类库实现 |
NIO2 | 异步IO,采用JDK7最新的NIO2类库实现 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库 |
在tomcat8.0前,默认采用BIO,之后改为NIO。⽆论 NIO、NIO2 还是 APR, 在性能⽅⾯均优于以往的BIO。 如果采⽤APR, 甚⾄可以达到 Apache HTTP Server 的影响性能。
Coyote内部组件
组件 | 作用描述 |
---|---|
EndPoint | EndPoint是Coyote的通信端点,即通信监听的端口,是具体Socket接收和发送处理器,是传输层的对象,因此EndPoint用来实现TCP/IP协议的 |
Processor | Processor是Coyote协议处理接口,如果说Endpoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。 |
ProtocolHandler | Coyote协议接口,通过EndPoint和Processor,实现针对具体协议的处理能力。Tomcat按照协议和IO提供了6个实现类:AjpNioProtocol,AjpAprProtocol,AjpNio2Protocol,Http11NioProtocol,Http11Nio2Protocol,Http11AprProtocol |
Adapter | 由于协议不同,客户端发过来的消息也不同,tomcat定义了自己的Request类,来封装这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Tomcat Request对象不是标准的ServletRequest。所以引入了CoyoteAdapter,这是适配器模式的经典应用,连接器调用CoyoteAdapter的service方法,传入Tomcat Request对象,然后CoyoteAdapter将Tomcat Request对象转换成Servlet Request对象然后调用容器。 |
1.4 Servlet容器Catalina
Tomcat是由一系列可配置(conf/server.xml)的组件构成的web容器,而Catalina是Tomcat的servlet容器。从另一个角度,Tomcat本质就是一款Servlet容器,因为Catalina才是Tomcat的核心,其他模块都是为了Catalina支撑的。比如Coyote提供链接通信,Jasper提供JSP引擎,Naming提供JBDI服务,Juli提供日志服务。
其实,可以认为整个Tomcat就是一个Catalina实例,Tomcat启动的时候会初始化这个实例,Catalina实例通过加载server.xml完成其他实例的创建,创建并管理Server,Server创建并管理多个服务,每个服务又可以有多个Connector和一个Container,各个组件介绍如下:
-
Catalina
负责解析Tomcat的配置文件server.xml,以此来创建服务器server组件并进行管理
-
Server
服务器表示整个Catalina Servlet容器以及其他组件,负责组装并启动Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供一种优雅的启动和关闭整个系统的方式
-
Service
服务是Server内部的组件,一个Server包含多个Service。它将若干个Connector组件绑定到一个Container
-
Container
容器,负责处理用户的Servlet请求,并返回对象给web用户的模块
Container组件有以下几种具体的组件,分别是Engine、Host、Context和Wrapper,这4个组件是父子关系:
-
Engine
表示整个Catalina的Servlet引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine,但一个引擎可以包含多个Host
-
Host
代表一个虚拟主机,或者一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机可包含多个Context
-
Context
表示一个Web应用程序,一个Web应用可包含多个Wrapper
-
Wrapper
表示一个Servlet,Wrapper作为容器中的最底层不能包含子容器
二、Tomcat核心配置
2.1 配置详情
上述的组件都可以在conf/server.xml文件中体现,Tomcat作为服务器的配置主要是在server.xml的配置,里面包含了Servlet容器的相关配置,即Catalina的配置。具体配置内容如下:
server.xml
<!--port:关闭服务器的监听端口
shutdown:关闭服务器的指令字符串
-->
<Server port="8005" shutdown="SHUTDOWN">
<!-- 以日志的形式输出服务器、操作系统、JVM版本信息-->
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- 加载和销毁APR,如果找不到APR库就会输出日志,并不影响Tomcat启动-->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- 避免JRE内存泄漏问题-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<!-- 加载和销毁全局命名服务-->
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<!-- 在Context停止时重建Executor池中的线程,避免ThreadLocal相关内存泄漏-->
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!--定义了全局命名服务 -->
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<!--
定义一个Service服务,一个Server可以有多个Service
-->
<Service name="Catalina">
<!--内容见下面-->
</Service>
</Server>
service标签及内部具体组件
<!-- 用于创建Service实例,默认使用 org.apahce.catalina.core.StandardService
默认情况下,仅指定Service的名称即name="Catalina"
service子标签:
Listener(用于为Service添加生命周期监听器)
Executor(用于配置共享线程池)
Connector(用于配置Service包含的链接器)
Engine(用于配置Service中链接器对应的Servlet容器引擎)
-->
<Service name="Catalina">
<!--
默认情况下,Service 并未添加共享线程池配置。 如果我们想添加⼀个线程池, 可以在<Service> 下添加如下配置:
name:线程池名称,⽤于 Connector中指定
namePrefix:所创建的每个线程的名称前缀,⼀个单独的线程名称为
namePrefix+threadNumber
maxThreads:池中最⼤线程数
minSpareThreads:活跃线程数,也就是核⼼池线程数,这些线程不会被销毁,会⼀直存在
maxIdleTime:线程空闲时间,超过该时间后,空闲线程会被销毁,默认值为6000(1分钟),单位毫秒
maxQueueSize:在被执⾏前最⼤线程排队数⽬,默认为Int的最⼤值,也就是⼴义的⽆限。除⾮特殊情况,这个值不需要更改,否则会有请求不会被处理的情况发⽣
prestartminSpareThreads:启动线程池时是否启动 minSpareThreads部分线程。默认值为false,即不启动
threadPriority:线程池中线程优先级,默认值为5,值从1到10
className:线程池实现类,未指定情况下,默认实现类org.apache.catalina.core.StandardThreadExecutor如果想使⽤⾃定义线程池⾸先需要实现org.apache.catalina.Executor接⼝
-->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
<!-- 默认情况下,server.xml 配置了两个链接器,⼀个⽀持HTTP协议,⼀个⽀持AJP协议,⼤多数情况下,我们并不需要新增链接器配置,只是根据需要对已有链接器进⾏优化
-->
<!--
port:端⼝号,Connector ⽤于创建服务端Socket 并进⾏监听,以等待客户端请求链接。如果该属性设为0,Tomcat将会随机选择⼀个可⽤的端⼝号给当前Connector 使⽤
protocol:当前Connector ⽀持的访问协议。 默认为 HTTP/1.1 , 并采⽤⾃动切换机制选择⼀个基于JAVA NIO的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)
connectionTimeOut:Connector 接收链接后的等待超时时间, 单位为 毫秒。 -1 表示不超时。
redirectPort:当前Connector 不⽀持SSL请求, 接收到了⼀个请求, 并且也符合security-constraint 约束,
需要SSL传输,Catalina⾃动将请求重定向到指定的端⼝。
executor:指定共享线程池的名称, 也可以通过maxThreads、minSpareThreads 等属性配置内部线程池。
URIEncoding:⽤于指定编码URI的字符编码, Tomcat8.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-
8859-1 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" disableUploadTimeout="true" useBodyEncodingForURI="true" URIEncoding="UTF-8" />
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector protocol="AJP/1.3"
port="8009"
redirectPort="8443" URIEncoding="UTF-8"/>
<!--name: ⽤于指定Engine 的名称, 默认为Catalina
defaultHost:默认使⽤的虚拟主机名称, 当客户端请求指向的主机⽆效时, 将交由默认的虚拟主机处
理, 默认为localhost-->
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<!--
Host用于配置主机
name: ⽤Host 的名称,即Engine指定的名称
appBase:网站资源信息的、存放位置,默认是webapps
unpackWARs:是否解压war包
autoDeploy:是否自动部署
-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
<!--Context 配置一个Web应用
docBase:web应用的路径,可以是磁盘上的绝对路径,也可以是相对于appBase的相对路径
path:Web应用的访问路径,如果我们Host名为localhost, 则该web应⽤访问的根路径为:http://localhost:8080/demo
-->
<Context docBase="..." path="/demo"/>
</Host>
</Engine>
</Service>
2.2 案例演示
server.xml中大部分配置保持默认即可,我们这里对host标签和context标签的配置进行演示:
我们首先在host中增加两个网址,用于配置host标签,我这里之前弄eureka时添加过,所以就不添加了,如下:
127.0.0.1 CloudEurekaServerA
127.0.0.1 CloudEurekaServerB
然后在server.xml中,修改host配置:
<Host name="CloudEurekaServerA" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
<Host name="CloudEurekaServerB" appBase="webapps2"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
然后我们复制一份webapps命名为webapps2,修改两个中的ROOT下的index.jsp(随便改,能区分即可)便于查看,然后我们分别访问两个host,结果如下:
我们下面在host中增加Context标签,我们可以将webapps复制到其他磁盘中,然后修改index.jsp,使用绝对路径配置(相对路径也可以,这里是为了演示方便),配置如下:
<Host name="CloudEurekaServerA" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context docBase="D:\webapps\ROOT" path="/demo" />
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
结果如下,这样我们就可以通过加/路径名
来跳转到别的项目中:
三、手写简单Tomcat
自定义Tomcat是可以作为一个服务器软件提供服务的,就是说我们可以使用浏览器客户端发送http请求,My_Tomcat可以接收请求并处理,处理成功后可以返回浏览器客户端。主要是简单的理解tomcat的主要工作流程。
我们分步来实现,首先我们请求http://localhost:8080,返回一个固定的Hello World字符串,然后封装Request和Response,返回html静态文件即静态资源,最后我们使My_Tomcat可以请求动态资源。
我写的时候每一步都用了BIO和NIO两种方式,用的时候注释掉一种即可。这里只展示主要代码,具体工具类代码请自行github查看。
3.1 返回固定字符串
第一步我们先让我们的项目能监听8080端口,并返回信息:
我们新建项目然后创建Boostrap类,类信息如下,主要思路是通过监听8080端口然后返回信息,在返回信息时需要将HTTP请求体进行拼接我写在了HttpProtocolUtil类中:
public class BootStrap {
//暂时写死,可以修改到xml文件中进行读取
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* MYTomcat程序init和启动
*
* @author Loserfromlazy
* @date 2022/1/12 15:35
*/
public void start() throws IOException {
//===================BIO
/*ServerSocket serverSocket = new ServerSocket(port);
System.out.println("My_Tomcat Listening on port 8080");
while (true){
Socket socket = serverSocket.accept();
OutputStream outputStream = socket.getOutputStream();
String data = "Hello,World!";
String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes(StandardCharsets.UTF_8).length) + data;
outputStream.write(responseText.getBytes(StandardCharsets.UTF_8));
socket.close();
}*/
//==================NIO
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
if (selector.select(2000)==0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
String data = "Hello,World!";
String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes(StandardCharsets.UTF_8).length) + data;
ByteBuffer buffer = ByteBuffer.wrap(responseText.getBytes(StandardCharsets.UTF_8));
socketChannel.write(buffer);
iterator.remove();
}
}
}
}
/**
* MYTomcat程序入口
*
* @author Loserfromlazy
* @date 2022/1/12 15:24
*/
public static void main(String[] args) {
BootStrap bootStrap = new BootStrap();
try {
bootStrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2 返回静态资源
第二步,我们可以封装请求的Request和Response对象,然后返回静态资源
这里我将上一步的Bootstrap改为Bootstrap1,然后新建一个Bootstrap,用于保存每一步的代码,后面也会这么做。
在进行封装前,我们可以查看HTTP请求头信息,可以使用以下方法查看或者F12:
public void start() throws IOException {
//===================BIO
/*ServerSocket serverSocket = new ServerSocket(port);
System.out.println("My_Tomcat Listening on port 8080");
while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte [] bytes = new byte[count];
inputStream.read(bytes);
System.out.println(new String(bytes));
socket.close();
}*/
//==================NIO
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
System.out.println("My_Tomcat Listening on port 8080");
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
if (selector.select(2000)==0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = 0;
while (count==0){
count = socketChannel.read(buffer);
}
buffer.flip();
System.out.println(new String(buffer.array()));
iterator.remove();
}
}
}
}
输出信息:
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Idea-3b85319b=6df6ddd1-3436-43bd-a172-56742a301d72
拿到了请求头,我们就可以封装Request,我们这里就封装请求方法和URL。
Request对象
封装Request对象的思路就是将HTTP头中的信息保存在Requset对象中
public class Request {
private String method;//例如GET POST
private String url;//例如 /index.html
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
private InputStream inputStream;//根据传入的inputStream解析请求头
private SocketChannel socketChannel;////根据传入的SocketChannel解析请求头(NIO)
public Request() {
}
public Request(SocketChannel socketChannel) throws IOException {
this.socketChannel = socketChannel;
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = 0;
while (count==0){
count = socketChannel.read(buffer);
}
buffer.flip();
String httpHeaderStr = new String(buffer.array());//http头协议
String[] split = httpHeaderStr.split("\\n");
String firstLine = split[0];//协议第一行
String[] firstLineItem = firstLine.split(" ");
this.method =firstLineItem[0];
this.url = firstLineItem[1];
System.out.println("method==>"+this.method+";url==>"+this.url);
}
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte [] bytes = new byte[count];
inputStream.read(bytes);
String httpHeaderStr = new String(bytes);//http头协议
String[] split = httpHeaderStr.split("\\n");
String firstLine = split[0];//协议第一行
String[] firstLineItem = firstLine.split(" ");
this.method =firstLineItem[0];
this.url = firstLineItem[1];
System.out.println("method==>"+this.method+";url==>"+this.url);
}
}
Response对象
封装Response对象的思路就是通过构造的输出流输出我们需要输出的内容。
public class Response {
private OutputStream outputStream;
private SocketChannel socketChannel;
public Response(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
public Response() {
}
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* 通过url获取静态资源的绝对路径,根据绝对路径获取静态文件,然后使用输出流输出
*
* @author Yuhaoran
* @date 2022/1/17 13:23
*/
public void outPutHtml(String url) throws IOException {
String absolutePath = StaticResourceUtil.getAbsolutePath(url);
File file = new File(absolutePath);
if (file.exists() && file.isFile()){
FileInputStream inputStream = new FileInputStream(file);
//输出文件内容
int count = 0;
while (count == 0) {
count = inputStream.available();
}
int written = 0;
int byteSize = 1024;
byte[] bytes = new byte[byteSize];
outputStream.write(HttpProtocolUtil.getHttpHeader200(count).getBytes(StandardCharsets.UTF_8));
while (written < count) {
if (written + byteSize > count) {
bytes = new byte[count - written];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written += byteSize;
}
}else {
//输出404
outputStream.write(HttpProtocolUtil.getHttpHeader404().getBytes(StandardCharsets.UTF_8));
}
}
public void outPutHtmlByChannel(String url) throws IOException {
String absolutePath = StaticResourceUtil.getAbsolutePath(url);
File file = new File(absolutePath);
if (file.exists() && file.isFile()){
//输出文件内容
FileInputStream inputStream = new FileInputStream(file);
FileChannel fileChannel = inputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
fileChannel.read(buffer);
buffer.flip();
byte[] bytes = HttpProtocolUtil.getHttpHeader200(buffer.capacity()).getBytes(StandardCharsets.UTF_8);
ByteBuffer responseBuffer = ByteBuffer.wrap(bytes);
socketChannel.write(responseBuffer);
socketChannel.write(buffer);
}else {
//输出404
byte[] bytes = HttpProtocolUtil.getHttpHeader404().getBytes(StandardCharsets.UTF_8);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
socketChannel.write(buffer);
}
}
public void outPut(String contect) throws IOException {
outputStream.write(contect.getBytes(StandardCharsets.UTF_8));
}
public void outPutByChannel(String contect) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(contect.getBytes(StandardCharsets.UTF_8));
socketChannel.write(buffer);
}
}
封装完成后我们就可以在主方法中使用:
public class BootStrap2 {
//暂时写死,可以修改到xml文件中进行读取
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* MYTomcat程序init和启动
*
* @author Loserfromlazy
* @date 2022/1/15 12:38
*/
public void start() throws IOException {
//===================BIO
// ServerSocket serverSocket = new ServerSocket(port);
// System.out.println("My_Tomcat Listening on port 8080");
// while (true) {
// Socket socket = serverSocket.accept();
// InputStream inputStream = socket.getInputStream();
// OutputStream outputStream = socket.getOutputStream();
// Request request = new Request(inputStream);
// Response response = new Response(outputStream);
// response.outPutHtml(request.getUrl());
// socket.close();
// }
//==================NIO
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
System.out.println("My_Tomcat Listening on port 8080");
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
if (selector.select(2000)==0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress()+"发来请求");
Request request = new Request(socketChannel);
Response response = new Response(socketChannel);
response.outPutHtmlByChannel(request.getUrl());
socketChannel.close();
iterator.remove();
}
}
}
}
/**
* MYTomcat程序入口
*
* @author Loserfromlazy
* @date 2022/1/12 15:24
*/
public static void main(String[] args) {
BootStrap2 bootStrap = new BootStrap2();
try {
bootStrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3 返回动态资源
关于Xpath表达式可以在菜鸟上看一下语法很简单https://www.runoob.com/xpath/xpath-syntax.html
动态资源就需要我们进行对Servlet的封装,代码如下,首先需要定义一下Servlet规范,如下面的Servlet接口,然后封装一个具体的HttpServlet,最后我们自己编写Servlet继承HttpServlet写自己的业务逻辑:
public interface Servlet {
void init() throws Exception;
void destory() throws Exception;
void service(Request request,Response response) throws Exception;
}
public abstract class HttpServlet implements Servlet{
public abstract void doGet(Request request,Response response);
public abstract void doPost(Request request,Response response);
@Override
public void service(Request request,Response response) throws Exception {
if ("GET".equals(request.getMethod())){
doGet(request,response);
}else{
doPost(request,response);
}
}
}
public class MyServlet extends HttpServlet{
@Override
public void doGet(Request request, Response response) {
String content = "<h1>Hello My_Tomcat GET Method!</h1>";
try {
//BIO
//response.outPut(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
//NIO
response.outPutByChannel(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
String content = "<h1>Hello My_Tomcat POST Method!</h1>";
try {
//BIO
//response.outPut(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
//NIO
response.outPutByChannel(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void init() throws Exception {
}
@Override
public void destory() throws Exception {
}
}
有了Servlet我们还需要web.xml,这里我们参考之前的Servlet即可:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>server.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>
</web-app>
最后在主方法中使用即可:
public class BootStrap {
//暂时写死,可以修改到xml文件中进行读取
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* MYTomcat程序init和启动
*
* @author Loserfromlazy
* @date 2022/1/15 12:38
*/
public void start() throws Exception {
//加载web.xml
loadServlet();
//===================BIO
/*ServerSocket serverSocket = new ServerSocket(port);
System.out.println("My_Tomcat Listening on port 8080");
while (true) {
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
Request request = new Request(inputStream);
Response response = new Response(outputStream);
if (servletMap.get(request.getUrl())==null){
response.outPutHtml(request.getUrl());
}else {
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request,response);
}
socket.close();
}*/
//==================NIO
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
System.out.println("My_Tomcat Listening on port 8080");
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
if (selector.select(2000)==0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress()+"发来请求");
Request request = new Request(socketChannel);
Response response = new Response(socketChannel);
if (servletMap.get(request.getUrl())==null){
response.outPutHtmlByChannel(request.getUrl());
}else {
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request,response);
}
socketChannel.close();
iterator.remove();
}
}
}
}
private Map<String,HttpServlet> servletMap = new HashMap<>();
/**
* 加载解析web.xml
* @author Loserfromlazy
* @date 2022/1/18 13:18
*/
private void loadServlet() {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List<Element> selectNodes = rootElement.selectNodes("//servlet");
for (Element element : selectNodes) {
//获取servlet-name
Node servletNameNode = element.selectSingleNode("servlet-name");
String servletName = servletNameNode.getStringValue();
//获取servlet-class
Node servletClassNode = element.selectSingleNode("servlet-class");
String servletClass = servletClassNode.getStringValue();
//根据servlet-name找到servlet-class
Node servletMapping = rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name = '" + servletName + "']");
Node urlPatternNode = servletMapping.selectSingleNode("url-pattern");
String urlPattern = urlPatternNode.getStringValue();
//存入map
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* MYTomcat程序入口
*
* @author Loserfromlazy
* @date 2022/1/12 15:24
*/
public static void main(String[] args) {
BootStrap bootStrap = new BootStrap();
try {
bootStrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.4 使用线程池的方式改造
我们只需要创建一个线程池和一个线程类来包装我们的执行逻辑,然后将线程提交到线程池即可。
这里以NIO的线程类为例:
public class RequestProcess implements Runnable{
private SocketChannel socketChannel;
private Map<String,HttpServlet> servletMap = new HashMap<>();
private Selector selector;
public RequestProcess(SocketChannel socketChannel, Map<String, HttpServlet> servletMap, Selector selector) {
this.socketChannel = socketChannel;
this.servletMap = servletMap;
this.selector = selector;
}
@Override
public void run() {
try {
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress()+"发来请求");
Request request = new Request(socketChannel);
Response response = new Response(socketChannel);
if (servletMap.get(request.getUrl())==null){
response.outPutHtmlByChannel(request.getUrl());
}else {
HttpServlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request,response);
}
socketChannel.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
四、Tomcat源码
4.1 源码构建
官方文档指定使用ant进行构建,但是使用maven也是能构建的。因为本质上就是一个Java项目。
我们首先在官网上下载tomcat的源码,我的源码版本是8.5.75,下载apache-tomcat-8.5.75-src.zip即可下载完成后将zip包解压。解压后创建一个source文件夹,将conf和webapps文件夹拷贝到source文件夹下:
然后在解压后的文件夹下创建pom.xml文件:
pom文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>apache-tomcat-8.5.75-src</artifactId>
<name>Tomcat8.5</name>
<version>8.5</version>
<properties>
<!-- 设置项目编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<build>
<!--指定源目录-->
<finalName>Tomcat8.5</finalName>
<sourceDirectory>java</sourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<plugins>
<!--引入编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<!--tomcat 依赖的基础包-->
<dependencies>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>javax.xml.soap</groupId>
<artifactId>javax.xml.soap-api</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
</project>
然后打开idea导入这个工程,idea会自动识别为maven工程。导入工程后启动BootStrap类,启动前加上启动参数:
添加启动参数的方法如下:
打开idea的启动类的配置编辑
然后再下图的VM options中编写,如果没有这个文本框,就需要在下图右上角的这个Modify options中添加VM options然后在编写。
VM参数如下:参数主要是用来指定tomcat的日志路径和产品目录路径。
Bootstrap启动的时候使用了两个系统变量catalina.home和catalina.base,从官网和源码中的注释可以知道这两者的区别主要是:catalina.home是Tomcat产品的安装目录,而catalina.base是tomcat启动过程中需要读取的各种配置及日志的根目录。默认情况下catalina.base是和catalina.home是相同的。
-Dcatalina.home=D:\tomcat\apache-tomcat-8.5.75-src\source
-Dcatalina.base=D:\tomcat\apache-tomcat-8.5.75-src\source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=D:\tomcat\apache-tomcat-8.5.75-src\source\conf\logging.properties
然后启动BootStrap类,打开localhost:8080,正常弹出tomcat首页即可。
4.2 源码构建的问题
以下是构件中编译遇到的各种问题:
4.2.1 JSP报错
如果打开8080弹出JSP报错(PS:我的是JSP报错加乱码),如下图:
则需要手动添加JSP的编译器,在ContextConfig中的configureStart()方法中增加JSP编译器,如下图:
重启后就可以解决JSP报错的问题。
4.2.2 tomcat编译乱码问题
如果也想我一样有日志乱码问题,首先可以试试网上的各种改UTF-8编码的操作,这里就不做赘述。如果不行就需要改源码解决,过程如下:
但是我改了7、8个地方改成UTF-8都不行,所以我对日志进行了Debug,过程如下:
首先看控制台的乱码如下图:
然后在最开始出现乱码的地方,通过全局搜索找到这个VersionLoggerListener.log
然后打上断点开始进行debug调试,一路跟代码,发现在getString方法中的bundle.getString(key)这一句执行完后,乱码就会出现。如下图:
点进bundle.getString(key),发现是JDK中ResourceBundle的方法.这个类是用来做国际化的。
最后在stackoverflow上找到了,如下图,Java9以上属性文件默认是UTF-8,但是Java8及以下默认是使用ISO-8859-1的,我的jdk版本就是8所以日志会有乱码问题。
解决方法是:修改方法,手动进行转码(改的时候注意有很多getString,别改错了):
重启后发现第一个乱码问题已解决。
下一个乱码问题也是如此,最后跟到下面的地方,跟上面同理,手动进行转码
乱码问题解决。
PS:我改完这两个地方就没有乱码了
4.3 源码流程分析
建议自己跟一编源码,光看别人的源码分析是看不懂的,别人的源码分析只能提供分析思路
4.3.1 tomcat启动流程
首先我们在startip.bat或startup.sh中可以找到这个脚本会拉起catalina.bat或catalina.sh的脚本,如下图
然后再catalina.bat或catalina.sh中会发现这个脚本会启动BootStrap类,也就是main方法,然后启动时会带一个start的参数,如下图:
然后我们进入BootStrap的main,具体的流程图(流程图是自己边Debug边画的只有大概的流程)在下面。
进入main方法后会先进入init方法
这里会创建一个Catalina的实例
然后回到main方法,继续往下走,我们会发现,会执行load()方法,然后执行start()方法。load方法主要是tomcat中的各种组件初始化,start方法是各种组件依次启动。
我们先跟进load()方法,这里会调用Catalina的load方法:
我们进入Catalina的load方法中,这个方法中主要关注的代码如下,它会拉取Server的init方法:
getServer().init();
我们点进init方法,会进入到LifeCycle的init方法,LifeCycle这个接口主要就是定义了整体生命周期,我们进入LifecycleBase的init方法中(默认进这个类,可以自行debug确认)
进入后会走到initInternal()方法中,这个方法是抽象方法,debug跟进会进入到对应的实现方法中,因为我们调用的是Server.init()所以会进入StandardServer的initInternal()中。
我们进入该方法,这个方法中主要关注下面的代码:
// Initialize our defined Services
for (Service service : services) {
service.init();
}
service.init()会拉起Service的init方法,我们点进init(),会发现会回到LifeCycle的init方法,然后跟上面一样会进入到StandardService的initInternal()方法中,如下图
最后会按照流程图一步一步走下去,会把Tomcat的组件逐级init。然后会回到BootStrap的start()方法中,start()方法与init()方法流程差不多,这里就不再赘述。
Tomcat启动的整体流程图:
4.3.2 Tomcat请求处理流程
部署项目到源码
如果需要查看Tomcat的请求流程,我们就需要有效的能进入我们Tomcat源代码工程的URL。所以我们可以新建一个JavaWeb工程,然后将这个JavaWeb工程部署到我们的Tomcat源码项目中。
首先我们先新建一个webdemo工程,然后创建一个Servlet,如下:
@WebServlet(name = "webdemo",value = "/web/webdemo")
public class DemoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("进入了GET方法");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("进入了POST方法");
}
}
然后我们将webdemo工程部署到tomcat中启动(注意是Tomcat不是我们的Tomcat源码工程),启动后会生成文件夹,如下图:
然后我们将Tomcat生成的这个webdemo文件夹拷贝到我们的Tomcat源码工程中:
然后就可以在源码中打断点查看Tomcat的请求流程了。
以上面的工程为例,请求地址是http://localhost:8080/webdemo/web/webdemo
,Tomcat处理的大体流程如下图:
Tomcat请求流程
首先,我们需要找到断点的入口,我们在NioEndPoint中的Poller类的run方法中打上断点。
为什么在NioEndPoint中的Poller类的run方法中打上断点?
Tomcat是从Connector接收请求的,在上面的启动流程源码分析中,如果debug跟代码的话,最后会发现在init流程中会执行socket的bind()方法在start流程中会执行socket的accept()方法。这两个方法都是在NioEndpoint中的,如下图:
而在NioEndpoint的startInternal()方法中,在执行accept之前,会创建轮询器,
我们进入到轮询器类中,发现其继承了Runnable,所以我们找到run方法,会发现,此方法会监听socket通道中的事件,因此这个轮询线程就是为了轮询监听socket通道事件的。因此我们在监听到事件之后的处理方法即
processKey(sk, socketWrapper);
上打断点开始debug。如下图:
下面我们debug运行BootStrap然后浏览器发送一个请求,开始查看请求处理流程:
这里只是演示请求处理流程,建议自己跟一遍源码。
首先我们看到请求确实来到了processKey方法,如下图:
我们跟进这个方法,在processKey中有一个processSocket方法这个方法会继续处理我们的socket和key的信息,我们跟进这个processSocket方法。
进入之后我们会发现进入到了NioEndPoint的抽象父类的方法中,然后我们会发现在这里会用线程池执行一个线程,因此我们需要关注这个线程的run方法。
我们进入到SocketProcessorBase类中的run方法,代码如下:
@Override
public final void run() {
synchronized (socketWrapper) {
// It is possible that processing may be triggered for read and
// write at the same time. The sync above makes sure that processing
// does not occur in parallel. The test below ensures that if the
// first event to be processed results in the socket being closed,
// the subsequent events are not processed.
if (socketWrapper.isClosed()) {
return;
}
doRun();
}
}
我们在doRun()打上断点,让程序运行到这里。(因为是线程池执行这个线程,所以可以跳过来)
然后我们继续跟进doRun()方法,在这个方法中,我们会发现他会执行handler的process方法。
进入process方法之后,在这个方法中会得到一个Processor对象,然后调用Processor对象的process方法,如下图:
继续跟,会进入到AbstractProcessorLight中(这个类是一个轻量级的抽象处理器实现,旨在作为从轻量级升级处理器到HTTP/AJP处理器的所有处理器实现的基础。)也就是说在这里会找到具体的处理器Processor去处理。
我们在这个方法中能找到service方法如下,这个方法会继续处理我们我们第一步传过来的socketWrapper对象。
state = service(socketWrapper);
我们跟进这个方法,果然进入到了具体的处理器中,也就是会进入到具体的Http1.1协议处理器的service方法中:
进入到service方法后,这个方法会将我们传过来的request和response对象转换成标准的HttpServlet对象,然后调用容器,getContainer方法会拿到service关联的Engine。
这里会通过获取容器的管道然后调用管道中的第一个Value的invoke方法,如下图:
我们跟进invoke方法:(此时会进入StandardEngineValve类) 此方法中会调用host,从host中管道取第一个Value执行invoke方法,去处理这个http请求。管道Pipeline其实就是一些按顺序执行Value#invoke()方法的对应的类的封装
Pipeline是一个处理管道的标准实现,它将调用一系列已配置为按顺序调用的阀门。此实现可用于任何类型的容器。Pipeline里面是Value接口的各种实现类。
getFirst方法:
在调用pipeline的getFirst()方法时,如果first上有Value就返回该Value,否则就返回basic上的Value。
protected Valve basic = null; protected Container container = null; protected Valve first = null; @Override public Valve getFirst() { if (first != null) { return first; } return basic; }
basic和first都是Value,Value是一个接口,下面有很多实现类比如StandardEngineValve
basic其实就是它本身的标准Value实现类:
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined.
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404);
}
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
PS:在获取
host.getPipeline().getFirst().invoke(request, response);
host的pipeline的顺序是AbstractAccessLogValve->ErrorReportValve ->StandardHostValve。host的pipeline的basic时StandardHostValve,如图:
我们继续跟进host的invoke,接下来会走到AbstractAccessLogValve的invoke方法然后调用getNext().invoke(request, response);
我们继续跟invoke会走到ErrorReportValve的invoke方法,此方法中继续调用getNext().invoke(request, response);
我们继续跟,此时会进入StandardHostValve类,然后会从context中拿管道调用invoke。
if (!response.isErrorReportRequired()) {
context.getPipeline().getFirst().invoke(request, response);
}
我们继续跟,进入到AuthenticatorBase#invoke()方法然后调用getNext().invoke(request, response);
我们继续跟invoke,会进入到StandardContextValve#invoke()方法
这里会从wrapper中获取管道然后执行
wrapper.getPipeline().getFirst().invoke(request, response);
跟进invoke会进入StandardWrapperValve#invoke()方法
在这里会创建一个过滤器链,代码如下:
关于过滤器链的注解:filterchain的实现,用于管理针对特定请求的一组过滤器的执行。当一组定义的过滤器全部执行完毕后,对doFilter()的下一次调用将执行servlet的service()方法本身。
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
上面代码往下几行,创建完之后会执行这个过滤器链:
filterChain.doFilter
(request.getRequest(), response.getResponse());
我们跟进doFilter方法:一直往下跟最后会执行service方法
servlet.service(request, response);
从connector调用容器到servlet.service的过程:
这个方法就是我们的Servlet的的service,然后就会取执行我们自己的Servlet方法了。此时看servlet,就是上面我们部署的webdome项目中的servlet,也就是说tomcat一步一步的找到了我们的Servlet并执行它。
整体流程:
五、Tomcat对HTTPS的支持
https需要证书,我们可以用java自带的工具生成自己的测试用的证书。
keytool -genkey -alias loserfromlazy -keyalg RSA -keystore miyao.keystore
- -alias 别名
- -keyalg 加密方式
- -keystore 密钥库名称
- ...其它具体参数请自行查阅
这里注意名字与姓氏需要与域名保持一致,之后就会在当前目录下生成miyao.keystore文件
然后在tomcat的server.xml中配置
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/miyao.keystore"
certificateKeystorePassword="123456"
type="RSA" />
</SSLHostConfig>
</Connector>
然后浏览器访问即可(注意需要忽略浏览器的安全警告):
六、Tomcat调优
Tomcat本身也是一个Java项目,所以调优分为JVM调优和Tomcat自身调优
6.1 JVM调优
jvm调优只需要我们将jvm参数放到Catalina.bat或Catalina.sh中即可。
jvm优化主要是内存和垃圾回收策略。
简单了解Java内存模型:
- 线程共享的数据区:主要是Java堆和方法区Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存
- 方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 虚拟机栈描述的是Java方法执行的内存模型,是线程私有的。每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 程序计数器是线程私有的一块较小的内存空间,其可以看做是当前线程所执行的字节码的行号指示器
- 本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务
内存优化主要是对堆进行优化,内存相关参数:
参数 | 参数作用 | 备注 |
---|---|---|
-server | 以服务端运行 | |
-Xms | 最小堆内存 | 与-Xmx设置相同 |
-Xmx | 最大堆内存 | 可用内存的80% |
-XX:MetaspaceSize | 元空间初始值 | |
-XX:MaxMetaspaceSize | 元空间最大内存 | 默认无限 |
-XX:NewRatio | 年轻代和老年代大小比值 | 默认为2 |
-XX:SurvivorRatio | Eden区与Survivor区大小的壁纸 | 默认为8 |
举例JAVA_OPTS=-server -Xms1024m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
tomcat调优可以在bin目录下建立setenv.bat,然后在里面配置JAVA_OPTS即可,注意等号和-server之间不能有空格否则不生效,一定不要马虎。
配置完重启tomcat即可。验证方式有很多,比如可以通过jmap命令或者tomcat内置状态检查等等。http://localhost:8080/manager/status
注意使用上面的网址需要在/conf/tomcat-users.xml中配置用户名密码
<role rolename="admin-gui"/> <role rolename="manager-gui"/> <role rolename="manager-jmx"/> <role rolename="manager-script"/> <role rolename="manager-status"/> <user username="admin" password="admin" roles="admin-gui,manager-gui,manager-jmx,manager-script,manager-status"/>
简单了解一下垃圾收集器:
-
串行收集器 Serial Collector
单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
-
并行收集器 Parallel Collector
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
-
并发收集器 Concurrent Collector
-
CMS收集器 Concurrent Mark Sweep Collector
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
-
G1收集器 Garbage-First Garbage Collector
适用于大容量的多核服务器,在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合
GC策略相关参数:
参数 | 描述 |
---|---|
-XX:+UseSerialGC | 启动创行收集器 |
-XX:+UseParallelGC | 使用并行收集器,使用此配置,那么-XX:+UseParallelOldGC默认启动 |
-XX:+UseParNewGC | 年轻代采用并行收集器 |
-XX:ParallelGCThreads | 年轻代和年老代垃圾回收启用的线程数 |
-XX:+UseConcMarkSweepGC | 对于年老代使用CMS垃圾收集器,启动后,-XX:+UseParNewGC自动启用 |
-XX:+UseG1GC | 启用G1收集器,G1是服务器类型的收集器,用于多核大内存机器,在满足高吞吐率的情况下,高概率满足GC暂停时间的目标 |
6.2 Tomcat自身调优
-
禁用AJP连接器
注释禁用掉server.xml中:
<!-- Define an AJP 1.3 Connector on port 8009 --> <Connector protocol="AJP/1.3" port="8009" redirectPort="8443" URIEncoding="UTF-8"/>
-
调整IO模式
tomcat8以前默认使用BIO,tomcat8以后默认NIO模式
当tomcat并发性能有较高要求或出现瓶颈时,可以使用ARP模式从操作系统级别解决异步IO问题,(需要在操作系统上安装ARP和Native)
-
调整配置tomcat线程池,见2.1配置详解
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库