一,前言
說一句大實話,“平時一直在用 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 程序。寫個鬼咧,寫代碼是不可能寫的,這輩子都不會寫代碼。直接從 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

3.4 WebSocket
接下來,我們參考官方的例子,搞一個基於 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 源碼。
