Hytale之我见 专注hytale服务器技术

Tomcat研究

2019-10-28
hytaleme

tomcat是常用的web容器(就是一种服务程序),主要提供对servlet/jsp的支持,那么tomcat有什么特点,有没有其它容器.

这篇文章主要目标是让初识tomcat的用户能够从整体上了解tomcat,而不仅仅是复制tomcat官方文档或介绍tomcat配置.

todo: tomcat的nio线程模型

介绍

tomcat是一种web容器,它实现了servlet/jsp规范,并提供了一些额外的功能.

因此,使用tomcat就是为了运行你的servlet程序与jsp,其它只是辅助功能.

而且除了tomcat外,还有其它web容器,它们可能也实现了servlet/jsp规范,因此也能用来运行你的servlet程序(或许其它容器性能更好,或者提供更多的额外支持)

servlet是什么?

servlet(server applet)直译过来就是服务端小应用程序,但实际上它并不.

从源码上看,Servlet接口里有init方法在初始化时调用,有destroy方法在销毁时调用, 有service方法,传入ServletRequestServletResponse,就是处理请求,产生响应.

在大多数情况下,我们用的是它的子类HttpServlet,传入的相应变成HttpServletRequestHttpServletResponse,也就是http请求与响应. 那么很明显了,servlet容器(像tomcat)就负责管理servlet,包括调用初始化,销毁方法,并且在收到http请求时,将新建http请求与http响应实例,并调用service方法传入servlet来进行处理.

jsp是什么?

jsp(JavaServer Pages)直译过来就是java服务器页面,即由服务器生成的页面.

从源码上看,jsp会被jsp编译器编译成servlet(推荐网上搜下jsp编译后所变成的java代码,可以很好地加深理解).

jsp页面语法怎样?其实就是html的语法,另外增加了被称为jsp动作的xml标签,使你可以在jsp中嵌入java代码, 然后在请求时动态地生成html页面返回给客户端.

目前流行前后端分离,而jsp存在许多不足,如:

  1. 因为是动态的所以无法像html一样被缓存
  2. jsp应该由前端还是后端开发较为模糊,分工不够明确

当然,也不是说jsp被废弃了,有需要的时候仍然可以使用.

tomcat整体结构

以下用server.xml的xml元素层次结构来描述:

  • server(一个): 代表整个容器

    • services(若干): service就是个组织结构,里面的所有connector共享一个engine

      • connectors(若干): connector将在指定端口上监听请求,并将请求交给engine处理
      • engine(一个): 可内置若干个虚拟主机,并可配置一个默认虚拟主机

        • hosts(若干): 虚拟主机,每个虚拟主机对应一个域名,内可配置若干web应用

          • contexts(若干): 每个context对应一个web应用,都有一个context path,用来匹配请求

请求处理流程

  1. 请求从某个connector进来
  2. 匹配host,找到对应的虚拟主机来处理(匹配不到则使用默认配置的虚拟主机)
  3. 匹配path,找到对应的web应用来处理

目录结构

  • bin: 运行脚本目录
  • conf: 配置目录(最常修改的是server.xml文件)
  • lib: tomcat的类库,如果需要添加tomcat依赖的jar文件,可以放到此目录内
  • logs: 日志目录
  • temp: 临时文件目录,里面的内容可以在tomcat停止后删除
  • webapps: web应用目录,里面的每个目录都是一个项目,其中ROOT是很有用的特殊项目,访问时地址栏中不需要给出项目名
  • work: 工作目录

快速使用

1. 下载java

2. 下载tomcat

3. 配置环境变量

以下有些变量有可能会自动配置,但保险起见,推荐手动配置一遍.

  1. CATALINA_HOMECATALINA_BASE: CATALINA_HOME就相当于tomcat根目录,CATALINA_BASE则是当前有效的tomcat配置根目录,默认情况下CATALINA_BASE值跟CATALINA_HOME相同

    那么它们有什么区别呢?一般情况下是没区别的,你配置个CATALINA_HOME就好了. 但如果你想配置多实例(不是指集群,而指让服务器上的若干用户使用同一个tomcat发布版),那CATALINA_BASE就有用了. 就是大家使用同一个发布版,但配置,日志,工作目录都是互相隔离的,感觉这种情况非常少,有需要可以看官方文档.

  2. JRE_HOMEJAVA_HOME: 作用很简单,就是指定java目录(要不然tomcat怎么知道你java安装在哪里)
  3. 其它环境变量,可以在catalina.batcatalina.sh脚本文件内查看(在文件顶部位置)

官方推荐在setenv脚本内配置以上的环境变量.

环境变量CATALINA_OPTS与JAVA_OPTS的区别

CATALINA_OPTS只会在运行start,run,debug命令时被使用; 而JAVA_OPTS则会在运行任意命令时被使用,比如停止进程,version命令等.

CATALINA_OPTS最为常用,并且官方推荐将java运行时选项如heap size等配置在这.

4. 启动

运行startup.batstartup.sh文件

启动后,默认配置下可以访问网址http://localhost:8080/

5. 停止

运行shutdown.batshutdown.sh文件

setenv脚本文件

有时我们想指定java运行时参数,如限制tomcat内存上限,或者设置环境变量等, 这时,官方推荐将这些设置在setenv.bat(windows)或setenv.sh(*nix)脚本文件内.

文件没找到?

默认情况下,这个文件是不存在的,需要手动创建,文件放置在$CATALINA_HOME/bin目录下(*nix下注意文件名大小写).

在这个文件内设置有什么好处?

可以更加分离(要不然修改catalina.sh就会’污染’原生的文件)

所有环境变量都能设置在这个文件内吗?

除了CATALINA_HOMECATALINA_BASE外(因为这两个是用来定位这个文件的)

这个脚本文件会在什么环境下被使用?

只有在使用标准脚本来启动tomcat时才会被使用,像将tomcat安装为windows服务,那么这个脚本就不会被使用.(其实简单的说就是在catalina.bat/catalina.sh脚本里会调用setenv这个脚本)

以daemon方式运行tomcat

(以下以*inx系统为例,windows系统类似,可以参考官方文档)

好处

以daemon即守护进行的方式运行tomcat有以下好处:

  1. 使tomcat不受终端影响,不因终端退出而终止
  2. 使tomcat可以用普通用户的身份运行: 从安全角度考虑,生产环境上一般不会使用root用户来运行tomcat,否则一旦web应用出现漏洞,可能导致的影响非常大.
  3. 可以让tomcat在系统启动时自动运行

配置方法

将tomcat配置成守护进程需要借助apache-commons-daemon项目的jsvc工具,通常该工具会包含在我们下载的tomcat包中($CATALINA_HOME/bin/commons-daemon-native.tar.gz)

具体操作步骤为:

  1. 配置JAVA_HOME, CATALINA_HOME两个环境变量

     export JAVA_HOME=/opt/jdk
     export CATALINA_HOME=/opt/tomcat
    
  2. 编译安装jsvc

     cd $CATALINA_HOME/bin
     tar -xvf commons-daemon-native.tar.gz
     cd commons-daemon-1.0.x-native-src/unix
     ./configure
     make
     cp jsvc ../..
     cd ../..
    
  3. 运行jsvc: 这里直接使用官方提供的脚本模板daemon.sh

    要注意的是,daemon脚本模板内默认以用户tomcat来启动,所以要新建用户并更新目录权限:

     useradd -s /sbin/nologin tomcat
     chown -R tomcat $CATALINA_HOME
     chgrp -R tomcat $CATALINA_HOME
    

    或者你可以更改启动tomcat的用户,在daemon.sh脚本文件顶部添加:

     TOMCAT_USER=root
    

    最后,运行daemon.sh文件:

     cd $CATALINA_HOME/bin
     ./daemon.sh start
    

配置说明

host就是虚拟主机的意思, context就是应用上下文/web应用的意思, appBase指Host元素的appBase属性配置值, xmlBase指Host元素的xmlBase属性配置值, docBase指Context元素的docBase属性配置值,

context.xml与web.xml的关系

你可能会想,它们都是web应用相关的配置,那么主要区别是什么呢?

实际上,tomcat是实现了servlet/jsp规范的web容器,而web.xml是这个规范设定的,tomcat无法修改,tomcat要对应用进行额外的描述,那这些描述符就只能放到另一个文件里了,这就是context.xml文件.

假设有另一个web容器也实现了servlet/jsp规范,那么那个web容器对应用进行的额外描述可能放置在xxx.xml文件里.

或者,你可以将context.xml看做应用外的描述,主要描述应用位置,应用匹配路径等信息; 将web.xml看做应用内的描述,主要描述应用内的listener,filter,servlet等配置.

在哪里定义context?

  1. $CATALINA_BASE/conf/[engine名]/[host名]/[应用名].xml(优先于第2项定义的)
  2. 应用内的/META-INF/context.xml
  3. $CATALINA_BASE/conf/server.xml文件的Host元素内

此外,还有默认context定义(如果某个配置没有在上面定义,则会使用默认定义里的值),定义位置有:

  1. $CATALINA_BASE/conf/context.xml: 会应用到所有web应用内
  2. $CATALINA_BASE/conf/[engine名]/[host名]/context.xml.default: 会应用到那个host下的所有web应用内

Host元素的几个重要属性

  1. appBase: host的根目录,可以指定绝对路径或相对路径(相对于$CATALINA_BASE),默认值为webapps
  2. xmlBase: 此host内放置web应用描述符文件(就是上面说的context)的目录,可以指定绝对路径或相对路径(相对于$CATALINA_BASE),默认值为$CATALINA_BASE/conf/<engine名>/<host名>
  3. deployOnStartup: 是否在tomcat启动时自动部署此host内的web应用,默认为true
  4. autoDeploy: 是否周期性检测此host内新的或有更新的web应用并重新部署(会周期性检测appBasexmlBase目录),默认为true

Context元素的几个重要属性

  1. docBase: 应用根目录/war文件路径,可以指定绝对路径或相对路径(相对于appBase)
  2. path: 应用路径,(空字符串代表默认应用,如果一个请求进来匹配不到其它应用路径,就会进这个默认应用)

关于部署web应用

默认情况下,你只要将foo.war放入webapps文件夹内,启动tomcat时就会自动部署了, 然后用地址http://localhost:8080/foo就能访问这个web应用了. 如果不想输入后面的foo,那么只要将foo.war改名为ROOT.war(ROOT是特殊名字,表示tomcat的默认应用),就可以用地址http://localhost:8080来访问了.

自动部署的应用路径是什么?

deployOnStartup为true时,自动部署开启,那么tomcat就会尝试从文件名来推出应用路径,比如文件名是foo.warfoo目录,那么应用路径就是/foo.

有个特殊的情况,当文件名为ROOT.warROOT目录时,应用路径是空字符串,即不需要加应用路径就可以访问.

更详细说明可以参考官方文档

如何手动部署

首先将自动部署关闭,即deployOnStartupautoDeploy属性设置为false,然后明确地在Host元素里定义Context元素,即手动定义.

要注意的是手动定义上面说的特殊情况ROOT目录就不生效了,比如你可以配置为:

<Context docBase="foo.war" path=""/>

这样不加应用路径也能访问foo应用,或者:

<Context docBase="ROOT.war" path="/foo"/>

这样必须加应用路径/foo才能访问ROOT应用

防止双重部署

比如你在appBase里放了个foo.war,同时deployOnStartup属性为true,那么在tomcat启动时这个foo应用会被自动部署一次;假设你同时在server.xml里的Host元素里配置了<Context docBase="foo.war" path="/foo"/>,那么foo应用又会被部署一次,就造成了双重部署.

那么如何解决这个问题呢?

  1. 如果你想要自动部署的功能,那么docBase必须不在appBase内(这样自动部署就不会在appBase内找到这个web应用了)
  2. 如果你不想要自动部署的功能,将deployOnStartupautoDeploy都设置为false,那么tomcat就不会自动部署了(也就防止了双重部署),但是这样就需要你手动在server.xml里定义每个web应用了

类加载机制

ClassLoader(类加载器)

  1. Bootstrap:
    1. jvm提供的运行时类
    2. $JAVA_HOME/jre/lib/ext内的jar包
  2. System: 正常情况下从环境变量CLASSPATH中加载;但tomcat标准启动脚本catalina.bat/catalina.sh会忽略环境变量CLASSPATH,它们会从以下位置加载:
    1. $CATALINA_HOME/bin/bootstrap.jar: 包含main()方法
    2. $CATALINA_BASE/bin/tomcat-juli.jar$CATALINA_HOME/bin/tomcat-juli.jar: 日志实现类
    3. $CATALINA_HOME/bin/commons-daemon.jar: 来自Apache Commons Daemon项目的类
  3. Common: 对tomcat与所有web应用都可见的类,类搜索位置在$CATALINA_BASE/conf/catalina.properties文件的common.loader属性中设置,默认配置为:
    1. $CATALINA_BASE/lib内未打包的类与资源
    2. $CATALINA_BASE/lib内的jar包
    3. $CATALINA_HOME/lib内未打包的类与资源
    4. $CATALINA_HOME/lib内的jar包
  4. Web应用: 只对web应用本身可见
    1. /WEB-INF/classes内未打包的类与资源
    2. /WEB-INF/lib内的jar包

类加载顺序

注意,默认情况下tomcat的类加载顺序与java的委托机制有点区别,默认时类查找顺序为:

  1. Bootstrap
  2. /WEB-INF/classes
  3. /WEB-INF/lib/*.jar
  4. System
  5. Common

如果配置了<Loader delegate="true"/>,那么类加载顺序就与java的委托机制一致了:

  1. Bootstrap
  2. System
  3. Common
  4. /WEB-INF/classes
  5. /WEB-INF/lib/*.jar

默认servlet

tomcat内有默认servlet,主要作用就是提供静态文件列出目录(列出目录需要在配置内开启).

定义在哪里?

定义在$CATALINA_BASE/conf/web.xml

如果你需要改变默认servlet的配置,可以在/WEB-INF/web.xml里重新定义,但这并不推荐,因为一旦你的应用部署到其它web容器,可能会找不到默认servlet对应的org.apache.catalina.servlets.DefaultServlet这个类. 因此推荐在/WEB-INF/tomcat-web.xml里定义,因为这个文件只会被tomcat容器识别.

日志

tomcat内部日志使用juli(一个从Apache Commons Logging重命名的包),内部固定使用java.util.logging框架.

APR(Apache Portable Runtime)

todo

WebSocket

tomcat提供了websocket的支持,实现了Java WebSocket 1.1接口

SSL/TLS

tomcat提供了对ssl的支持,具体可以看官方文档

SSI(Server Side Includes)

ssi与jsp类似,它在服务端将动态生成的内容插入静态的html中.

tomcat提供了SSI的支持,默认情况下这个配置是关闭的,你可以在web.xml里开启.

开启方式是将org.apache.catalina.ssi.SSIServlet(servlet方式)注释打开或将org.apache.catalina.ssi.SSIFilter(filter方式)注释打开(但不要同时打开两个).

匹配的url路径是*.shtml

(缺点也与jsp类似,实际中在tomcat里用的估计不多)

CGI(Common Gateway Interface)

tomcat提供了CGI的支持,默认情况下这个配置是关闭的,你可以在web.xml里开启.

开启方式是将org.apache.catalina.servlets.CGIServlet注释打开.

匹配的url路径是/cgi-bin/*

(实际中用的估计不多)

realm(安全域)

其实就是一个存储用户名和密码的“数据库”再加上一个枚举列表。

“数据库”中的用户名和密码是用来验证web应用用户合法性的,而每一合法用户所对应的角色存储在枚举列表中。

你可以配置哪里用户可以访问哪些接口.

$CATALINA_BASE/conf/tomcat-users.xml这个配置文件可以配置realm相关用户

(实际中用的估计不多,一般都是应用内自带用户与权限系统)

SecurityManager(安全管理)

首先SecurityManager是java的而不是tomcat的,但tomcat允许你配置SecurityManager来防’内贼’, 就是防止应用的代码随意操作其它的目录之类的.

$CATALINA_BASE/conf/catalina.policy$CATALINA_BASE/conf/catalina.properties这两个文件与此安全配置相关.

(这是针对代码的安全性管理,一般用的也不多)

JNDI(Java Naming and Directory Interface, Java 命名与目录接口)

tomcat提供JNDI支持,你可以配置个全局的JNDI数据源或单个应用使用的数据源,但在tomcat的配置文件中配置,修改起来并不方便.

(一般用的估计不多)

tomcat cluster (tomcat集群)

tomcat集群主要起到会话共享的作用,这样在某个节点挂掉时,可以使用其它节点响应.

实现上,tomcat使用组播的方式进行集群内会话共享,但只适合中小型网站. 缺点是会话变更会分发给集群内的所有其它节点,对网站与服务器都存在开销,而这种开销随集群规模变大而越发严重,无法做到线性扩展.

按照官方的描述,永远不要去用它,因为它会增加配置复杂性(真的很复杂),并且调试起来更困难.

tomcat启动太慢?

查看日志的以下几行:

<DATE> org.apache.catalina.util.SessionIdGenerator createSecureRandom
INFO: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [5172] milliseconds.

如果在这里花费的时间特别长(如上百秒),那么可以设置以下系统属性来解决: -Djava.security.egd=file:/dev/./urandom, 代价则是安全性(随机性)会降低.

关于这个问题,可以查看官方wiki,对于安全性降低,网上也有文章表明其实影响不大(比如这篇文章)

有哪些其它web容器?

比如JBoss(开源),WebLogic,WebSphere.

Tomcat主要提供动态内容,但在静态内容上,通常搭配apache http server以提高性能,此外通常会搭配nginx做负载均衡. 相比而言,tomcat较为小型轻量,功能较少(支持servlet/jsp),适合中小型系统访问量不高的场合.

JBoss里面集成了tomcat,并多了一些其它服务,如ejb,jms,jaas等,实际上JBoss是应用服务器,而不仅仅是web服务器. 相对tomcat而言功能更丰富,提供更多的支持,性能更高,在静态资源访问方面媲美apache http server,并且自带负载均衡模块.

有句话可以描述业界的常态(当然不要太较真): 有钱的公司用WebLogic与WebSphere,没钱的公司用tomcat+nginx

总结

这篇文章基本上过了一遍tomcat的功能,虽然看起来比较复杂,但实际上它就是个提供servlet/jsp支持的web容器, 至于那些附加的功能则不怎么用的到,因为如果要使用,一方面会与tomcat耦合过高,另一方面通过xml配置的方式修改起来也不方便.

就算真的会用到那些功能,一般也会在web应用内tomcat之外使用,比如tomcat的realm可以用应用内的用户与权限系统代替, 比如tomcat的集群功能,一般也会在tomcat之外通过服务器的架构实现.

虽然tomcat在结构设计上可以配置很多个host,每个host可以配置很多个应用,但实际上一个tomcat一般只会配一个默认host, host内也只会部署一个应用,甚至在应用结构复杂,数据量大,访问量大的情况下,一个应用都会进一步拆成若干个, 以及部署若干个tomcat做负载均衡,要不然一个tomcat都撑不住一个应用的访问,哪会在一个tomcat里部署多个应用.

因此,只将tomcat当做一个servlet/jsp容器来用,那其实配置起来也很简单,内容并不多.


下一篇 java内存模型

目录