【Tomcat 原始碼系列】認識 Tomcat

楷哥發表於2021-01-16

一,前言

說一句大實話,“平時一直在用 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 原始碼。

相關文章