首頁 > 軟體

【Tomcat 原始碼系列】認識 Tomcat

2021-01-16 16:00:35

一,前言

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


IT145.com E-mail:sddin#qq.com