当前位置 博文首页 > 楷哥:【Tomcat 源码系列】认识 Tomcat
说一句大实话,“平时一直在用 Tomcat,但是我从来没有用过 Tomcat”。
“平时一直在用 Tomcat”,是因为搬砖用的 SpringBoot,内嵌了 Tomcat,每次启动程序的时候,都需要启动 Tomcat。
“我从来没有用过 Tomcat”,是因为没有专门去用过 Tomcat,没有写过 Servlet,没有写过 JSP,没有配置过 Tomcat。
这篇博客介绍如何使用 Tomcat,根据官方提供的例子,分析如何写 Servlet 程序,JSP 页面,WebSocket 程序。
在继续源码之前,不妨先用用 Tomcat 吧。代码请看这里:https://github.com/zzk0/tomcat-example
首先点击这里去下载一个 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。
有一些基本概念需要理解,请看这里。这些概念有: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 是打包的时候,提供的元数据。
我们怎么开发一个 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 就配置好了。
接下来,展开 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 了。
接下来,我们来写第一个 Servlet 程序。写个鬼咧,写代码是不可能写的,这辈子都不会写代码。直接从 webapps\examples\WEB-INF\classes
中复制一个过来。你也可以复制我的代码。
下面这段代码,可以视为一个 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
接下来,我们参考官方的例子,搞一个基于 WebSocket 的聊天室。不写代码,全靠复制粘贴。
我们需要从 \webapps\examples\WEB-INF\classes\websocket\chat
复制代码。
将下面代码复制到 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 下面就好了。
之后还需要修改 chat.xhtml
中 websocket 的端点。将下面红框中的东西,改成一开始 IDEA 启动配置中的 Application Context。在这里,我们只需要去掉 s 就好了。
接下来启动!
通过这个地方访问聊天室:http://localhost:8080/example/chat.xhtml
发送的消息,都可以即时被推送。
这篇博客展示了如何使用 Tomcat,开发使用 Servlet,JSP,WebSocket 的 Demo。
总结一下,Tomcat 就是一个实现了 Servlet,JSP,WebSocket 规范的 HTTP 服务器。上面展示了使用这些技术的例子,要明白这背后做了什么,还得了解这些技术的规范,还要去看实现,看 Tomcat 源码。