1. 程式人生 > >【Tomcat 原始碼系列】認識 Tomcat

【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 ## 2.1 執行 Tomcat 首先[點選這裡](https://tomcat.apache.org/download-10.cgi)去下載一個 Tomcat 先吧。 解壓一下,我們來看看裡面都有些什麼東西。 ``` bin: 啟動關閉指令碼等 conf: 配置檔案,server.xml 伺服器配置,web.xml 應用配置 lib: Tomcat 的包,比如有 catalina.jar logs: 日誌 temp: 臨時檔案 webapps: 存放網站應用(webapp),一個資料夾對應一個 webapp,在域名埠後面,輸入資料夾名字就可以訪問對應的 webapp,比如 localhost:8080/examples work: Tomcat 的工作目錄,不斷點進去,會發現一些 .class 檔案,這些對應動態生成的頁面。 ``` ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116115924056-2016051748.png) 進入 `bin` 目錄,點選 startup 指令碼。啟動之後,介面顯示如下。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116115643947-1011876680.png) 進入 `work` 目錄,不斷深入。我們可以發現有一個 index_jsp.java 及其 class 檔案。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116120356836-1556056341.png) 用 IDE 看看 index_jsp.java,看 _jspService 方法,裡面有很多 out.write,而寫出去的內容正是我們上面看到的網頁。這啟示我們,其實 JSP 的原理就是生成 java 檔案,並通過 out.write 寫到網頁中,因此可以將一些變數動態的寫入到網頁,而不是隻能看到一個靜態的 html。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116120624481-1896390664.png) ## 2.2 Tomcat 概念和結構 有一些基本概念需要理解,請看[這裡](https://juejin.cn/post/6844903952991911949)。這些概念有: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 對應於一個應用。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210115231202448-1934948369.png) 一張更全的結構圖。一個請求,從 Connector 進來,通過 Pipeline 進入 Engine,再進入 Host、Context,最終找到對應的 Servlet 然後進行呼叫。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210115232446725-1958187542.png) # 三,例子 執行 startup,輸入 http://localhost:8080/examples/ 檢視官方的例子。 官方提供了三類例子,分別是 Servlet,JSP,WebSocket 的例子。我們可以點進去看看 Tomcat 能夠做什麼。後面我們來開發一下自己的 Servlet,JSP,WebSocket 程式,看看這些程式是如何建立的。 那麼這些例子在哪裡呢?我們可以進入到 webapps 目錄下面。我們可以看到有 examples。一個目錄對應一個網站應用,比如 examples,我們可以用 http://localhost:8080/examples/ 來訪問。對於 ROOT,可以直接用域名和埠訪問。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116141308771-2009768397.png) 進入 examples 目錄,我們看看一個 webapp 有哪些組成部分。其中 WBE-INF 裡面包含了網站的配置,類檔案。META-INF 是打包的時候,提供的元資料。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116141522485-1674909270.png) # 四,自己動手 ## 3.1 開發和部署 我們怎麼開發一個 Tomcat 的 webapp 呢?開發完了之後,又需要如何部署呢?我們需要配置哪些東西呢? 接下來,我們用 IDEA 來開發和部署。我用的版本是:IntelliJ IDEA 2020.2.1 (Ultimate Edition)。 ### 建專案 首先我們來新建一個專案,使用 Gradle 來構建,勾選 Web。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116142435078-179564037.png) 設定專案名稱。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116142531230-1795585441.png) 在 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' ``` ### 配置專案 點選右上角,新增配置。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116142840117-1389339115.png) 新增 Tomcat Server,注意不要選到後面的 TomcatEE 版本了。選擇 Local 版本。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116143004322-226396779.png) 點選 Configure 按鈕,找到 Tomcat 解壓目錄即可。不需要進入到 bin 當中。我們還可以看到左下角有個 Warning,它提示你需要配置部署。於是,我們選中 Deployment,去配置。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116143113625-2140857260.png) 點選那個加號,然後選擇 exploded 版本。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116143508613-1499849135.png) 點選 ok 之後,修改 Application Context,這個 Context 用來配置訪問時候 url 的名字。可以理解為這個 webapp 的名字。之後,我們可以使用 `localhost:8080/example` 來訪問。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116143626876-1385009163.png) 至此,我們的第一個 webapp 就配置好了。 ## 3.2 JSP 接下來,展開 src,main,webapp,找到 index.jsp。我們可以在這裡開始寫程式碼。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116143940689-1912219262.png) 編輯內容,注意到下面有 java 程式碼,其實 jsp 就是 html 和 java 的混合體。下面的 jsp,就是向瀏覽器輸出了 Hello World 這個字串。我們點選執行,啟動一下。這裡就不再展開 JSP 了,如果又需要再去學一學吧。 ``` <%@ page contentType="text/html;charset=UTF-8" language="java" %> $Title$ <% String s = "Hello World"; out.write(s); %> ``` 可以看到 Hello World 了。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116144355793-508358031.png) ## 3.3 Servlet 接下來,我們來寫第一個 Servlet 程式。寫個鬼咧,寫程式碼是不可能寫的,這輩子都不會寫程式碼。直接從 `webapps\examples\WEB-INF\classes` 中複製一個過來。你也可以複製我的程式碼。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116145148490-22733553.png) 下面這段程式碼,可以視為一個 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(""); out.println(""); out.println("Hello World!
"); out.println(""); out.println(""); out.println("

Hello World!

"); out.println(""); out.println(""); } } ``` 接下來,我們還要配置,如何去呼叫這個 Servlet 程式。在 webapp 下面新建資料夾 WEB-INF,並在下面新建一個 web.xml 檔案。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116145400210-1802517791.png) 同樣,我去找一份配置,這次我在 `webapps/ROOT` 下面到 web.xml,然後新增一些資訊來配置 url。servlet 標籤定義了一個 servlet 的名字及其所在地點。這個 servlet-class 需要根據包的路徑來,前面我新建的 ExampleServlet 並沒有包,所以直接這樣子配就行。配好了 servlet,還要去配呼叫這個 servlet 的 URL。 ```
Welcome to Tomcat Welcome to Tomcat ExampleServlet ExampleServlet ExampleServlet /hello
``` 點選啟動,訪問這個連結 http://localhost:8080/example/hello ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116145906491-450016916.png) ## 3.4 WebSocket 接下來,我們參考官方的例子,搞一個基於 WebSocket 的聊天室。不寫程式碼,全靠複製貼上。 我們需要從 `\webapps\examples\WEB-INF\classes\websocket\chat` 複製程式碼。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116150209966-1002210028.png) 將下面程式碼複製到 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 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 下面就好了。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116150531614-1358548585.png) 之後還需要修改 `chat.xhtml` 中 websocket 的端點。將下面紅框中的東西,改成一開始 IDEA 啟動配置中的 Application Context。在這裡,我們只需要去掉 s 就好了。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116150653491-1696254635.png) 接下來啟動! 通過這個地方訪問聊天室:http://localhost:8080/example/chat.xhtml ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116150926269-611086163.png) 傳送的訊息,都可以即時被推送。 ![](https://img2020.cnblogs.com/blog/1616773/202101/1616773-20210116151113447-1297291907.png) # 五,總結 這篇部落格展示瞭如何使用 Tomcat,開發使用 Servlet,JSP,WebSocket 的 Demo。 總結一下,Tomcat 就是一個實現了 Servlet,JSP,WebSocket 規範的 HTTP 伺服器。上面展示了使用這些技術的例子,要明白這背後做了什麼,還得了解這些技術的規範,還要去看實現,看 Tomcat