一,前言

说一句大实话,“平时一直在用 Tomcat,但是我从来没有用过 Tomcat”。

“平时一直在用 Tomcat”,是因为搬砖用的 SpringBoot,内嵌了 Tomcat,每次启动程序的时候,都需要启动 Tomcat。

“我从来没有用过 Tomcat”,是因为没有专门去用过 Tomcat,没有写过 Servlet,没有写过 JSP,没有配置过 Tomcat。

这篇博客介绍如何使用 Tomcat,根据官方提供的例子,分析如何写 Servlet 程序,JSP 页面,WebSocket 程序。

在继续源码之前,不妨先用用 Tomcat 吧。代码请看这里:https://github.com/zzk0/tomcat-example

二,Tomcat

2.1 运行 Tomcat

首先点击这里去下载一个 Tomcat 先吧。

解压一下,我们来看看里面都有些什么东西。

bin: 启动关闭脚本等
conf: 配置文件,server.xml 服务器配置,web.xml 应用配置
lib: Tomcat 的包,比如有 catalina.jar
logs: 日志
temp: 临时文件
webapps: 存放网站应用(webapp),一个文件夹对应一个 webapp,在域名端口后面,输入文件夹名字就可以访问对应的 webapp,比如 localhost:8080/examples
work: Tomcat 的工作目录,不断点进去,会发现一些 .class 文件,这些对应动态生成的页面。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

进入 bin 目录,点击 startup 脚本。启动之后,界面显示如下。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

进入 work 目录,不断深入。我们可以发现有一个 index_jsp.java 及其 class 文件。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

用 IDE 看看 index_jsp.java,看 _jspService 方法,里面有很多 out.write,而写出去的内容正是我们上面看到的网页。这启示我们,其实 JSP 的原理就是生成 java 文件,并通过 out.write 写到网页中,因此可以将一些变量动态的写入到网页,而不是只能看到一个静态的 html。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

2.2 Tomcat 概念和结构

有一些基本概念需要理解,请看这里。这些概念有:Server,Service,Engine,Host,Context,Wrapper,Pipeline,Valve,Realm,Connector。名词很多,知道个大概意思和作用就行了。

下面这个图就清晰地展示了 Tomcat 的结构图,仔细去看 conf/server.xml 这个文件的 xml 树结构。一个 Server 可以跑多个 Service,默认配置了一个名字为 Catalina 的 Service,这个 Service 下面可以配置多个 Connector 和 一个 Engine。这个 Connector 负责监听端口,并将客户端请求转发给 Engine。一个 Engine 可以有多个 Host,每个 Host 对应一个站点。一个 Host 中可以有多个 Context,一个 Context 对应于一个应用。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

一张更全的结构图。一个请求,从 Connector 进来,通过 Pipeline 进入 Engine,再进入 Host、Context,最终找到对应的 Servlet 然后进行调用。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

三,例子

运行 startup,输入 http://localhost:8080/examples/ 查看官方的例子。

官方提供了三类例子,分别是 Servlet,JSP,WebSocket 的例子。我们可以点进去看看 Tomcat 能够做什么。后面我们来开发一下自己的 Servlet,JSP,WebSocket 程序,看看这些程序是如何创建的。

那么这些例子在哪里呢?我们可以进入到 webapps 目录下面。我们可以看到有 examples。一个目录对应一个网站应用,比如 examples,我们可以用 http://localhost:8080/examples/ 来访问。对于 ROOT,可以直接用域名和端口访问。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

进入 examples 目录,我们看看一个 webapp 有哪些组成部分。其中 WBE-INF 里面包含了网站的配置,类文件。META-INF 是打包的时候,提供的元数据。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

四,自己动手

3.1 开发和部署

我们怎么开发一个 Tomcat 的 webapp 呢?开发完了之后,又需要如何部署呢?我们需要配置哪些东西呢?

接下来,我们用 IDEA 来开发和部署。我用的版本是:IntelliJ IDEA 2020.2.1 (Ultimate Edition)。

建项目

首先我们来新建一个项目,使用 Gradle 来构建,勾选 Web。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

设置项目名称。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

在 build.gradle 中引入下面的依赖,我用的是 Tomcat 10,所以需要引入 Jakarta 开头的包,如果你用的是别的版本的 Tomcat,请自行找到对应版本的包。

// https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api
providedCompile group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '5.0.0'

// https://mvnrepository.com/artifact/jakarta.websocket/jakarta.websocket-api
providedCompile group: 'jakarta.websocket', name: 'jakarta.websocket-api', version: '2.0.0'

配置项目

点击右上角,添加配置。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

添加 Tomcat Server,注意不要选到后面的 TomcatEE 版本了。选择 Local 版本。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

点击 Configure 按钮,找到 Tomcat 解压目录即可。不需要进入到 bin 当中。我们还可以看到左下角有个 Warning,它提示你需要配置部署。于是,我们选中 Deployment,去配置。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

点击那个加号,然后选择 exploded 版本。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

点击 ok 之后,修改 Application Context,这个 Context 用来配置访问时候 url 的名字。可以理解为这个 webapp 的名字。之后,我们可以使用 localhost:8080/example 来访问。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

至此,我们的第一个 webapp 就配置好了。

3.2 JSP

接下来,展开 src,main,webapp,找到 index.jsp。我们可以在这里开始写代码。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

编辑内容,注意到下面有 java 代码,其实 jsp 就是 html 和 java 的混合体。下面的 jsp,就是向浏览器输出了 Hello World 这个字符串。我们点击运行,启动一下。这里就不再展开 JSP 了,如果又需要再去学一学吧。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
    <%
      String s = "Hello World";
      out.write(s);
    %>
  </body>
</html>

可以看到 Hello World 了。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

3.3 Servlet

接下来,我们来写第一个 Servlet 程序。写个鬼咧,写代码是不可能写的,这辈子都不会写代码。直接从 webapps\examples\WEB-INF\classes 中复制一个过来。你也可以复制我的代码。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

下面这段代码,可以视为一个 Servlet,它接收 GET 请求,并将一个 html 逐行逐行写给前端。因为 Java 代码里面太多这些 out.println 了,导致要修改前端必须要改 Java,这样不好。因此,才有了 JSP。

import java.io.*;
import jakarta.servlet.*;
import jakarta.servlet.http.*;

public class ExampleServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException
    {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Hello World!</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1>Hello World!</h1>");
        out.println("</body>");
        out.println("</html>");
    }
}

接下来,我们还要配置,如何去调用这个 Servlet 程序。在 webapp 下面新建文件夹 WEB-INF,并在下面新建一个 web.xml 文件。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

同样,我去找一份配置,这次我在 webapps/ROOT 下面到 web.xml,然后添加一些信息来配置 url。servlet 标签定义了一个 servlet 的名字及其所在地点。这个 servlet-class 需要根据包的路径来,前面我新建的 ExampleServlet 并没有包,所以直接这样子配就行。配好了 servlet,还要去配调用这个 servlet 的 URL。

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0"
         metadata-complete="true">

    <display-name>Welcome to Tomcat</display-name>
    <description>
        Welcome to Tomcat
    </description>

    <servlet>
        <servlet-name>ExampleServlet</servlet-name>
        <servlet-class>ExampleServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>ExampleServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

</web-app>

点击启动,访问这个链接 http://localhost:8080/example/hello

【Tomcat 源码系列】认识 Tomcat-LMLPHP

3.4 WebSocket

接下来,我们参考官方的例子,搞一个基于 WebSocket 的聊天室。不写代码,全靠复制粘贴。

我们需要从 \webapps\examples\WEB-INF\classes\websocket\chat 复制代码。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

将下面代码复制到 ChatAnnotation 中,@ServerEndpoint 用来配置提供 websocket 协议服务的端点,它支持服务端推送消息。

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

@ServerEndpoint(value = "/websocket/chat")
public class ChatAnnotation {

    private static final String GUEST_PREFIX = "Guest";
    private static final AtomicInteger connectionIds = new AtomicInteger(0);
    private static final Set<ChatAnnotation> connections =
            new CopyOnWriteArraySet<>();

    private final String nickname;
    private Session session;

    public ChatAnnotation() {
        nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
    }


    @OnOpen
    public void start(Session session) {
        this.session = session;
        connections.add(this);
        String message = String.format("* %s %s", nickname, "has joined.");
        broadcast(message);
    }


    @OnClose
    public void end() {
        connections.remove(this);
        String message = String.format("* %s %s",
                nickname, "has disconnected.");
        broadcast(message);
    }


    @OnMessage
    public void incoming(String message) {
        // Never trust the client
        String filteredMessage = String.format("%s: %s",
                nickname, message.toString());
        broadcast(filteredMessage);
    }




    @OnError
    public void onError(Throwable t) throws Throwable {
    }


    private static void broadcast(String msg) {
        for (ChatAnnotation client : connections) {
            try {
                synchronized (client) {
                    client.session.getBasicRemote().sendText(msg);
                }
            } catch (IOException e) {
                connections.remove(client);
                try {
                    client.session.close();
                } catch (IOException e1) {
                    // Ignore
                }
                String message = String.format("* %s %s",
                        client.nickname, "has been disconnected.");
                broadcast(message);
            }
        }
    }
}

然后,我们再从 \webapps\examples\websocket 偷一个 chat.xhtml 文件。放到 webapp 下面就好了。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

之后还需要修改 chat.xhtml 中 websocket 的端点。将下面红框中的东西,改成一开始 IDEA 启动配置中的 Application Context。在这里,我们只需要去掉 s 就好了。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

接下来启动!

通过这个地方访问聊天室:http://localhost:8080/example/chat.xhtml

【Tomcat 源码系列】认识 Tomcat-LMLPHP

发送的消息,都可以即时被推送。

【Tomcat 源码系列】认识 Tomcat-LMLPHP

五,总结

这篇博客展示了如何使用 Tomcat,开发使用 Servlet,JSP,WebSocket 的 Demo。

总结一下,Tomcat 就是一个实现了 Servlet,JSP,WebSocket 规范的 HTTP 服务器。上面展示了使用这些技术的例子,要明白这背后做了什么,还得了解这些技术的规范,还要去看实现,看 Tomcat 源码。

01-17 02:10