一,前言
说一句大实话,“平时一直在用 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 文件,这些对应动态生成的页面。
进入 bin
目录,点击 startup 脚本。启动之后,界面显示如下。
进入 work
目录,不断深入。我们可以发现有一个 index_jsp.java 及其 class 文件。
用 IDE 看看 index_jsp.java,看 _jspService 方法,里面有很多 out.write,而写出去的内容正是我们上面看到的网页。这启示我们,其实 JSP 的原理就是生成 java 文件,并通过 out.write 写到网页中,因此可以将一些变量动态的写入到网页,而不是只能看到一个静态的 html。
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 对应于一个应用。
一张更全的结构图。一个请求,从 Connector 进来,通过 Pipeline 进入 Engine,再进入 Host、Context,最终找到对应的 Servlet 然后进行调用。
三,例子
运行 startup,输入 http://localhost:8080/examples/ 查看官方的例子。
官方提供了三类例子,分别是 Servlet,JSP,WebSocket 的例子。我们可以点进去看看 Tomcat 能够做什么。后面我们来开发一下自己的 Servlet,JSP,WebSocket 程序,看看这些程序是如何创建的。
那么这些例子在哪里呢?我们可以进入到 webapps 目录下面。我们可以看到有 examples。一个目录对应一个网站应用,比如 examples,我们可以用 http://localhost:8080/examples/ 来访问。对于 ROOT,可以直接用域名和端口访问。
进入 examples 目录,我们看看一个 webapp 有哪些组成部分。其中 WBE-INF 里面包含了网站的配置,类文件。META-INF 是打包的时候,提供的元数据。
四,自己动手
3.1 开发和部署
我们怎么开发一个 Tomcat 的 webapp 呢?开发完了之后,又需要如何部署呢?我们需要配置哪些东西呢?
接下来,我们用 IDEA 来开发和部署。我用的版本是:IntelliJ IDEA 2020.2.1 (Ultimate Edition)。
建项目
首先我们来新建一个项目,使用 Gradle 来构建,勾选 Web。
设置项目名称。
在 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 Server,注意不要选到后面的 TomcatEE 版本了。选择 Local 版本。
点击 Configure 按钮,找到 Tomcat 解压目录即可。不需要进入到 bin 当中。我们还可以看到左下角有个 Warning,它提示你需要配置部署。于是,我们选中 Deployment,去配置。
点击那个加号,然后选择 exploded 版本。
点击 ok 之后,修改 Application Context,这个 Context 用来配置访问时候 url 的名字。可以理解为这个 webapp 的名字。之后,我们可以使用 localhost:8080/example
来访问。
至此,我们的第一个 webapp 就配置好了。
3.2 JSP
接下来,展开 src,main,webapp,找到 index.jsp。我们可以在这里开始写代码。
编辑内容,注意到下面有 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 了。
3.3 Servlet
接下来,我们来写第一个 Servlet 程序。写个鬼咧,写代码是不可能写的,这辈子都不会写代码。直接从 webappsexamplesWEB-INFclasses
中复制一个过来。你也可以复制我的代码。
下面这段代码,可以视为一个 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 文件。
同样,我去找一份配置,这次我在 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
3.4 WebSocket
接下来,我们参考官方的例子,搞一个基于 WebSocket 的聊天室。不写代码,全靠复制粘贴。
我们需要从 webappsexamplesWEB-INFclasseswebsocketchat
复制代码。
将下面代码复制到 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);
}
}
}
}
然后,我们再从 webappsexampleswebsocket
偷一个 chat.xhtml
文件。放到 webapp 下面就好了。
之后还需要修改 chat.xhtml
中 websocket 的端点。将下面红框中的东西,改成一开始 IDEA 启动配置中的 Application Context。在这里,我们只需要去掉 s 就好了。
接下来启动!
通过这个地方访问聊天室:http://localhost:8080/example/chat.xhtml
发送的消息,都可以即时被推送。
五,总结
这篇博客展示了如何使用 Tomcat,开发使用 Servlet,JSP,WebSocket 的 Demo。
总结一下,Tomcat 就是一个实现了 Servlet,JSP,WebSocket 规范的 HTTP 服务器。上面展示了使用这些技术的例子,要明白这背后做了什么,还得了解这些技术的规范,还要去看实现,看 Tomcat 源码。