Tomcat配置与应用

简单了解Tomcat之后, 我们来看看他的实际配置和相关的应用吧,

我们之前已经说过了, 对于Tomcat运行的核心配置文件就是server.xml. 各个层级组件的顺序是这样子的:

server -> service -> connector -> engine -> host -> context

我们还说过在部署的时候, tomcat启动一个jvm, 产生运行时区域, 主要就是堆和栈两个部分, 其中对象都保存在堆中.

在CentOS7上部署使用Tomcat

之前我们已经将相关的环境变量配置好了, 也成功的部署了Tomcat的默认页面, 现在我们来先看一下默认的页面的组织结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@VM-node0 host-manager]# tree .
.
├── images
│ ├── add.gif
│ ├── asf-logo.svg
│ ├── code.gif
│ ├── design.gif
│ ├── docs.gif
│ ├── fix.gif
│ ├── tomcat.gif
│ ├── update.gif
│ └── void.gif
├── index.jsp
├── manager.xml
├── META-INF
│ └── context.xml
└── WEB-INF
├── jsp
│ ├── 401.jsp
│ ├── 403.jsp
│ └── 404.jsp
└── web.xml

4 directories, 16 files

这个是默认tomcat页面的host-manager的目录结构 从中我们可以得到一个Java Web工程的目录组织结构.

一个JavaWeb程序, 都有特定的组织形式, 层次性的目录结构, 从上面也可以看到了. 主要包含了servlet代码文件, jsp页面文件, 类文件, 部署描述符文件等等.

例如我们在上面看到的, WEB-INF, META-INF 这两个目录是完全大写的, 但是却并不一定是必须要有的, 不过基本上每一个JavaWeb程序都有这个. 其中, 前者是webapp的私有资源目录, 通常存放的是webapp自用的web.xml.而后者基本相似, 存放的是webapp的context.xml

显然的, 我们这两个目录是不能被访问到的. 另外, 还有classes, 用来存放webapp的私有类的, 以及lib, 这也是webapp的私有类, 但是是被打包成jar格式的类. 当然了, 还有一个webapp的主页, 也就是index.jsp了.

说完了webapp的组织结构, 接下来我们再来说一下webapp的几种归档格式.

  • war webapp
  • jar EJB的类
  • rar 资源适配器
  • ear 企业级应用程序

以上就是关于Java的Webapp组织结构的相关了, 接下来我们就来手动添加一个测试应用程序, 步骤如下:

  1. 创建一个webapp的特有的目录结构
  2. 提供webapp的各个文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@VM-node0 webapps]# mkdir myapp
[root@VM-node0 webapps]# cd myapp/
[root@VM-node0 myapp]# mkdir ./{lib,classes,WEB-INF,META-INF} -pv
mkdir: created directory ‘./lib’
mkdir: created directory ‘./classes’
mkdir: created directory ‘./WEB-INF’
mkdir: created directory ‘./META-INF’
[root@VM-node0 myapp]# tree .
.
├── classes
├── lib
├── META-INF
└── WEB-INF

4 directories, 0 files

接下来就是创建一些必要的文件.

来随便写一个:

1
2
3
4
5
6
7
8
9
10
11
<%@ page language="java" %>
<%@ page import="java.util.*"%>
<html>
<head>
<title>JSP Test Page</title>
</head>
<body>
<% out.println("Hello, World."); %>
</body>
</html>

ok, 接下来我们就来直接访问一下. 默认配置的是自动部署的 我们先不要关心这方面的.

直接访问的结果:

tomcat_test

结果和我们设想的结果是一致的.

我们之前说过, webapp中的源文件会被翻译成Java代码和Class文件在work目录下, 我们去看一下吧.

1
2
3
4
5
6
7
8
9
10
11
12
[root@VM-node0 myapp]# pwd
/usr/local/tomcat/work/Catalina/localhost/myapp
[root@VM-node0 myapp]# tree .
.
└── org
└── apache
└── jsp
├── index_jsp.class
└── index_jsp.java

3 directories, 2 files

已经生成了对应的Java代码和类文件.

我们部署webapp应用, 其实就是将源文件放置于目标目录, 接着配置tomcat服务器是的能够给予context.xml文件中定义的路径来访问此webapp. 接着通过特有的类来将class loader装载到tomcat.

主要有两种方式, 也即是自动部署和手动部署, 其实手动部署还有两种, 一个是冷部署, 也就是把webapp复制倒指定位置, 接着再启动tomcat. 另外一个就是热部署, 通过一些manager的部署工具, ant, tcd等.

除了部署操作, 还有反部署, 重新部署, 停止, 启动等等几种和部署有关的操作. 其中稍微有点疑问的应该就是反部署操作了吧. 这个其实是说停止webapp, 并且从tomcat实例中拆除其部分文件和部署名.

Tomcat的组件配置

我们再来看一遍tomcat主配置文件的目录结构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<server>
<service>
<connector />
<connector />
...
<engine>
<host name="">
</host>
<host name="">
<context />
...
</host>
...
</engine>
</service>
</server>

除了上面的这些, 还有一些被嵌套的组件, 例如Valve(阀门). 这个东西可以存在在任何容器类的组件中, 这个东西可以理解成是一种拦截器, 可以用来进行日志收集或者试请求拦截. 另外, 还有日志记录器Logger, 可以存放在Context之外的任何容器中, 用来定义日志是如何存储, 如何记录的. 除此之外, 还有一个用来进行用户身份认证的组件叫做Realm这个在之前也提到过.

Server.xml

我们现在就打开server.xml来看一下, 首先还是先复制一份吧 然后更方便进行实验.

首先我们可以看到默认定义的Server实例, 后面跟上了一个端口和一个shutdown属性. 这是干嘛的? 相信聪明的你一定看出来了, 对. 这是销毁虚拟机的一个接口, 我们可以使用telnet连接过去之后, 输入shutdown属性所定义的语句, tomcat就会销毁虚拟机了, 来试试吧.

1
2
3
4
5
6
7
8
9
10
[root@VM-node0 tomcat]# ss -tnl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 100 127.0.0.1:25 *:*
LISTEN 0 128 *:22 *:*
LISTEN 0 100 ::1:25 :::*
LISTEN 0 1 ::ffff:127.0.0.1:8005 :::*
LISTEN 0 100 :::8009 :::*
LISTEN 0 100 :::8080 :::*
LISTEN 0 128 :::22 :::*

这个是启动之后的情况, 我们可以看到监听本机的8005端口, 接下来使用telnet去连接并且发送对应的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@VM-node0 tomcat]# telnet 127.0.0.1 8005
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
SHUTDOWN
Connection closed by foreign host.
[root@VM-node0 tomcat]# ss -tnl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 100 127.0.0.1:25 *:*
LISTEN 0 128 *:22 *:*
LISTEN 0 100 ::1:25 :::*
LISTEN 0 128 :::22 :::*

看 这样监听的套接字都被释放了, 事实上, 虚拟机都销毁了, 整个tomcat进程其实都没了.

再往下面看, 我们可以看到tomcat对用户认证相关的属性:

1
2
3
4
5
6
7
8
9
10
11
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>

这里定义了一个全局的命名资源, 创建了一个基于内存的用户数据库, 所对应的配置文件就是conf/tomcat-user.xml.

再往下走, 我们看到了第一个对于HTTP/1.1协议版本的连接器:

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

在下面的注释中, 还有一个使用https也就是SSL/TLS的连接器:

1
2
3
4
5
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" />
-->

在上面做销毁实例的实验中 我们注意到除了这个服务实例监听的8005端口和HTTP连接器监听的8080端口之外, 还有一个8009端口, 这个端口是干什么的呢? 向下看就可以看到了:

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

就像上面注释写的, 这是一个AJP协议的连接器.

AJP的全称是Apache Jserv Protocol. 这是一个定向包协议, 使用二进制格式来传输可读文本.

接着向下就可以看到我们的Catalina的Engine了, 使用的默认host就是下面定义的localhost:

1
2
3
4
5
6
7
8
<Engine name="Catalina" defaultHost="localhost">
<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 &quot;%r&quot; %s %b" />
</Host>
</Engine>

这里省略了注释的部分, 这里的host定义了使用的webapp的目录名称和一些部署选项,例如: 是否自动部署, 解包WAR等. host内部就是我们说的valve, 定义了一个日志的存储格式和存储类型.txt

Connector

在绝大多数的应用场景中, 我们都不会把Tomcat直接面向客户提供服务, 虽然官方说Tomcat是使用event模型开发的. 那么搭配反向代理使用的时候就需要配置一下连接器组件了.

假设我们的前端服务器是Apache的话, 我们可以使用AJP协议进行通信, 这样更高效. 甚至, 在这种情况下, 我们可以直接关闭HTTP的连接器防止用户直接越过前端服务器访问到. 基本上我们的连接器类型分以下的3种:

  • HTTP连接器
  • SSL连接器
  • AJP连接器

如果是HTTP类型的连接器, 必须要配置的属性是port, 协议默认就是http, 这就意味着 我们在定义AJP连接器的时候需要定义protocol. 除这两之外, 常用的属性还有:

  • address: 连接器定义的监听地址, 默认是0.0.0.0
  • maxThreads: 最大并发连接, 默认是200
  • redirectPort: HTTP和HTTPS的转发端口, 如果连接器支持的协议是HTTP但是收到了HTTPS请求的话, 就会转发到这个接口.
  • connectionTimeout: 顾名思义了, 等到客户端发送请求的超时时长
  • enableLookups: 是否进行DNS解析, 默认是true. 这十分耗时间, 一般都会设置成为false
  • acceptCount: 设置等待队列的最大长度. 在tomcat所有的处理线程都处于繁忙状态的时候, 新发来的请求就会放置于这个队列中.

一个SSL连接器的示例:

1
2
3
<Connector port="8443" maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" acceptCount="100" debug="0" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"/>

提个醒, 这里我们还没有配置使用的证书和密钥, tomcat仅仅支持JKS, PKCS11 or PKCS12这三种格式的密钥存储格式, 也就是说我们需要先把之前的证书和密钥重新导入进去, 使用形如这样的cmd:

1
2
3
openssl pkcs12 -export -in mycert.crt -inkey mykey.key
-out mycert.p12 -name tomcat -CAfile myCA.crt
-caname root -chain

过了很久之后的补充, 通过这种方式我没有正确的配置好SSL访问, 但是连接器是启动了, 8443端口是在监听状态了. 通过查阅了官方文档, 使用另外一种Apr的方式配置好了, 当然前提是下载了一个so组件, tomcat-native. 在下面贴上一个能够使用的:

1
2
3
4
5
6
7
8
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
maxThreads="150" SSLEnabled="true"
scheme="https" secure="true" SSLenabled="true"
SSLCertificateFile="conf/tomcat.crt"
SSLCertificateKeyFile="conf/key.pem"
SSLProtocol="TLSv1+TLSv1.1+TLSv1.2"
SSLVerifyClient="false">
</Connector>

这里的前提是本机需要安装tomcat-native

Engine

engine是servlet的运行实例, 他只有三个常用的属性, defaultHost, 默认的虚拟主机实例, name: engine组件的名字, 用于日志和错误信息记录时区别不同的引擎. 最后一个是jvmRoute, 是用来进行集群搭建时用作路由的.

Host

虚拟主机, 常用的属性有appBase, 就是这个host的webapp目录. autoDeploy 是否进行自动部署, unpackWARs 是否对WAR格式的文档进行解包.

Context

类似Apache中的路径别名, 一个Context定义用来表示tomcat实例中的web应用程序. 常用的属性有: docBase, 表示响应的webapp应用程序存放位置. ``reloadable` 是否允许重新加载context相关的web应用程序类, 默认是false.

Valve

之前我们就说过了, 这个东西像是一个过滤器,并且可以存在在很多组件之间, 按照Valve定义的次序来决定生效的次序.

我们有很多种不同的Valve:

  • AccessLogValve: 访问日志
  • ExtendedAccessValve: 扩展功能的访问日志
  • JDBCAccessLogValve: 通过JDBC将访问日志发送到数据库中
  • RemoteAddrValve: 远程地址的访问控制
  • RemoteHostValve: 远程主机名称的访问控制
  • ….

等等.

一个使用访问控制的Valve示例就像这样子:

1
<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.0\.0\.1" />

这样子配置就会只允许本机访问了:

1
2
3
4
5
6
7
8
9
10
11
[root@VM-node0 conf]# curl -I 192.168.16.100:8080
HTTP/1.1 403
Transfer-Encoding: chunked
Date: Fri, 21 Sep 2018 09:48:40 GMT

[root@VM-node0 conf]# curl -I 127.0.0.1:8080
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 21 Sep 2018 09:48:48 GMT

LNMT & LAMT

首先我们在前端加上一个Nginx实现反向代理.

1
2
3
4
5
location / {
proxy_pass http://192.168.16.100:8080/;
# root "/usr/share/nginx/html";
# index index.html index.htm;
}

这样就很简单了. 稍微细致一点的话就像这样:

1
2
3
4
5
index  index.jsp index.html;

location ~* \.(jsp|do)$ {
proxy_pass http://192.168.16.100:8080/;
}

除了使用Nginx, 我们还有一个选择就是使用httpd. 在使用Apache的选择上 我们就可以使用之前说的AJP协议而不是HTTP协议了. AJP是一个二进制协议, 相较于文本传输的HTTP性能要更好一点.

要想使用AJP, 要求我们的HTTPD加载模块, 我们来看一下当前加载的

1
2
3
4
[root@VM-node0 ~]# httpd -M
...(omitted)
proxy_ajp_module (shared)
...(omitted)

这里就装载了我们的ajp协议的代理协议模块. 当然 为了能够开启这个模块, 我们首先需要开开启总反代模块proxy_module

接着我们在配置文件中确认一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@VM-node0 ~]# cat /etc/httpd/conf.modules.d/00-proxy.conf 
# This file configures all the proxy modules:
LoadModule proxy_module modules/mod_proxy.so
LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_express_module modules/mod_proxy_express.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so
LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_scgi_module modules/mod_proxy_scgi.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so

确实是启用了模块, 接着我们就可以进行配置一下了: (在虚拟主机中配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<VirtualHost *:80>
ServerName test.wyx.com
ProxyVia On
ProxyRequests Off
ProxyPreserveHost On
<Proxy *>
Require all granted
</Proxy>
ProxyPass / http://192.168.16.100:8080/
ProxyPassReverse / http://192.168.16.100:8080/
<Location />
Require all granted
<Location>
</VirtualHost>

接着我们就可以尝试访问了.(顺便贴一下关于Tomcat主机的配置相关)

1
2
3
4
5
6
7
<Host name="test.wyx.com"  appBase="/data/webapps"
unpackWARs="true" autoDeploy="true">
<Context path="/" docBase="ROOT" reloadable="true" />
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

这个是在原来的默认Host下面增加的一个, host的名字和上面的ServerName保持一致, 这样会有什么作用呢?

当我们访问192.168.16.100的时候, 代理出现的页面就是默认的那个Tomcat的首页, 但是当我们访问test.wyx.com的时候, 呈现在我们眼前的就是当前上面配置的那个自定义的jsp页面. 这是因为我们在代理的时候保留了host主机.

至于AJP协议的使用, 直接在上面的ProxyPass那个地方把协议改成ajp就OK了.