????????Tomcat 是一個廣泛使用的開源 Servlet 容器,用于運行 Java Web 應用程序。深入理解 Tomcat 的工作原理對于 Java 開發者來說是非常有價值的。本文將帶領大家手動實現一個簡易版的 Tomcat,通過這個過程,我們可以更清晰地了解 Tomcat 是如何處理 HTTP 請求和響應的。
????????tomcat涉及到的知識點較多,主要有注解、抽象類、反射、IO流等,對基礎掌握度要求較高,掌握這些基礎后,我們開始手寫tomcat
一、創建包
一個基本的 Tomcat 主要完成以下幾個核心功能:
- 監聽端口:等待客戶端的 HTTP 請求。
- 解析請求:從客戶端請求中提取關鍵信息,如請求方法、請求路徑等。
- 處理請求:根據請求信息調用相應的處理邏輯。
- 返回響應:將處理結果封裝成 HTTP 響應返回給客戶端。
根據以上功能,我們創建如下圖所示的包
--tomcat類的作用是啟動整個tomcat容器
--webapp包下存放你自己創建的servlet動態資源
--httpServletRe包下有兩個類HttpServletRequest和HttpservletResponse,這兩個類可以說是與前端請求直接關聯。
????????在前端的請求信息中,請求方式和訪問路徑都要放在HttpServletRequest類中,可以說是相當的重要,我們可以通過socket將前端信息裝到該類中,具體寫法之后細講,也就是說我們知道該類有請求方式和訪問路徑,通過和項目本身的資源對比從而定位到某個實際的靜態資源或動態資源
而HttpServletResponse則是向前端發送我們自己寫的消息,依賴outputStream來完成,該消息需要遵循響應信息的特定格式,之后的工具類中會給出
--servlet包下則是仿照Java類庫中的servlet繼承關系,創建了servlet接口,Gservlet抽象類和Httpservlet抽象類
--util包下存放兩個工具類,一個類的作用是找到webapp下所有servlet類的類路徑,用來進行反射;
一個類的作用是創建響應消息的返回格式,包展開如下:
二、寫tomcat邏輯
要想得到前端發送的請求,我們首先需要創建一個ServerSocket對象實例來接收:
ServerSocket serverSocket = new ServerSocket(9090);
while(true){Socket socket = serverSocket.accept();
}
寫一個while語句循環接收前端請求,一旦接收到請求,我們就可以創建輸入流對象,將前端發送的消息存下來,在這里我們先只獲取請求行的信息也就是第一行的信息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String s = bufferedReader.readLine();
可以看到,瀏覽器中第一行有請求方式和訪問路徑,我們是以字符串形式接收的第一行數據,所以可以通過split()方法將請求方式和訪問路徑分開
String[] s1 = s.split(" ");
tomcat代碼展示:(端口的話可以自己選,我寫的是9090,tomcat默認端口是8080)
三、寫HttpServletRequest的邏輯
得到請求方式和訪問路徑之后,我們需要將請求方式和訪問路徑封裝到HttpServletRequest類中,在HttpServletRequest類中定義字符串類型的變量url和method,創建getter和setter方法:
public class HttpServletRequest {private String url;private String method;public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getMethod() {return method;}public void setMethod(String method) {this.method = method;}
}
四、補充tomcat邏輯
我們已經定義好HttpServletRequest類,接下來我們將tomcat類中拿到的請求方式和訪問路徑賦給該類中的變量method和url,首先在tomcat創建HttpServletRequest類的實例對象request,調用request的set方法,入參是inputStream獲取到的請求方式和訪問路徑
代碼如下:
寫到該階段我們可以先測試一下,在HttpServletRequest中重寫toString方法,在tomcat中調用
到瀏覽器上輸入http://localhost:9090,發現控制臺輸出:
請求方法和訪問路徑成功封裝到了request中,我們繼續下面的邏輯。
五、仿寫servlet的繼承關系
servlet包下是仿照Java類庫中的servlet繼承關系,創建了servlet接口,Gservlet抽象類和Httpservlet抽象類,我們現在在這三個類中添加邏輯
Servlet接口
該接口中定義以下方法:init方法、service方法和destroy方法,service方法用來判斷是get請求還是post請求,在本接口中只定義,在HttpServlet抽象類中實現。其中有兩個入參,HttpServletRequest類型的request和HttpservletResponse的response,之前request對象封裝過請求方式,該方法會通過拿到入參的請求方式判斷是get請求還是post請求
package com.qcby.servlet;import com.qcby.httpServletRe.HttpServletRequest;
import com.qcby.httpServletRe.HttpServletResponse;public interface Servlet {void init();void service(HttpServletRequest request, HttpServletResponse response) throws Exception;void destroy();
}
Gservlet抽象類
該抽象類繼承Servlet接口并實現init方法和destroy方法,這兩種方法我們不寫實際功能,了解java類庫中這兩種方法的作用即可
package com.qcby.servlet;public abstract class Gservlet implements Servlet{@Overridepublic void init() {System.out.println("初始化");}@Overridepublic void destroy() {System.out.println("銷毀");}
}
HttpServlet抽象類
該類繼承Gservlet抽象類,實現了Servlet接口中的service方法,此外HttpServlet類中還增加了兩種方法,doGet方法和doPost方法,相信學過servlet的小伙伴們并不陌生,doGet方法和doPost方法作為抽象方法不具體實現,子類也就是我們自己創建的servlet類繼承該類時就必須實現該方法,至于service方法是用來區分get方法和post方法的,所以之前的request入參就是為了提供method做if判斷
package com.qcby.servlet;import com.qcby.httpServletRe.HttpServletRequest;
import com.qcby.httpServletRe.HttpServletResponse;import java.io.IOException;public abstract class HttpServlet extends Gservlet{@Overridepublic void service(HttpServletRequest request, HttpServletResponse response) throws Exception{if(request.getMethod().equals("GET")){doGet(request,response);}else{doPost(request,response);}}public abstract void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException;public abstract void doPost(HttpServletRequest request, HttpServletResponse response);
}
六、創建注解類WebSocket
在工具包下創建WebSocket注解類,該注解類的作用是定義servlet類的url路徑
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(value = ElementType.TYPE)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface WebSocket {public String url() default "";
}
七、創建servlet容器
tomcat啟動時,Servlet 容器需要將客戶端請求的 URL 路徑映射到具體的 Servlet 上,以便正確處理請求。為了實現這一功能,容器使用 Map 來存儲 URL 路徑和 Servlet 名稱或實例之間的映射關系。
要想獲取URL路徑和Servlet實例對象,我們首先要通過反射獲取到類的類信息,而獲取類信息需要獲取類的全路徑,所以寫一個工具類獲取webapp包下所有類的全路徑,然后初始化map容器
import com.qcby.servlet.HttpServlet;import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;public class FindServletAllPath {public static HashMap<String, HttpServlet> map = new HashMap<>();public static List<String> getClassPaths(String packageName) throws IOException, ClassNotFoundException {List<String> classPaths = new ArrayList<>();// 將包名轉換為文件系統路徑String path = packageName.replace('.', '/');// 獲取類加載器ClassLoader classLoader = Thread.currentThread().getContextClassLoader();// 獲取指定路徑下的所有資源Enumeration<URL> resources = classLoader.getResources(path);while (resources.hasMoreElements()) {URL resource = resources.nextElement();// 獲取資源的文件路徑String filePath = resource.getFile();// 遞歸掃描目錄scanDirectory(new File(filePath), packageName, classPaths);}return classPaths;}/*** 遞歸掃描目錄,查找所有的 .class 文件* @param directory 要掃描的目錄* @param packageName 當前包名* @param classPaths 存儲類路徑的列表*/private static void scanDirectory(File directory, String packageName, List<String> classPaths) {if (!directory.exists()) {return;}// 獲取目錄下的所有文件和文件夾File[] files = directory.listFiles();if (files != null) {for (File file : files) {if (file.isDirectory()) {// 遞歸掃描子目錄scanDirectory(file, packageName + "." + file.getName(), classPaths);} else if (file.getName().endsWith(".class")) {// 獲取類名String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);classPaths.add(className);}}}}static{try {// 指定要掃描的包名String packageName = "com.qcby.webapp";// 獲取類路徑列表List<String> classPaths = getClassPaths(packageName);// 打印類路徑for (String classPath : classPaths) {Class<?> aClass = Class.forName(classPath);WebSocket annotation = aClass.getAnnotation(WebSocket.class);HttpServlet o = (HttpServlet) aClass.newInstance();map.put(annotation.url(),o);}} catch (Exception e) {e.printStackTrace();}}
}
我們把這個過程裝入static塊中,這樣在類加載時就能完成容器的映射
八、創建servlet動態資源
在webapp包下創建一個servlet類,繼承HttpServlet抽象類,重寫doGet和doPost方法,各自添加一個輸出語句,添加WebSocket注解,確定該類的url路徑
import com.qcby.httpServletRe.HttpServletRequest;
import com.qcby.httpServletRe.HttpServletResponse;
import com.qcby.servlet.HttpServlet;
import com.qcby.util.WebSocket;import java.io.IOException;@WebSocket(url = "/first")
public class FirstServlet extends HttpServlet {@Overridepublic void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {System.out.println("first的doGet方法被調用");}@Overridepublic void doPost(HttpServletRequest request, HttpServletResponse response) {System.out.println("first的doPost方法被調用");}
}
九、補充tomcat邏輯
前端發送的請求方式和url路徑已經封裝到request對象當中,map中k值存放著請求路徑,通過map映射可以找到對應的servlet實例對象,此時servlet的引用類型是父類類型,我們可以直接調用父類的方法service,有兩個參數request和response,創建一個response對象放入對應參數位置
if(FindServletAllPath.map.containsKey(request.getUrl())){HttpServlet servlet = FindServletAllPath.map.get(request.getUrl());servlet.service(request,response);
}
進入service方法后,會通過獲取request的請求方式判斷是執行doget方法還是doPost方法,因為方法被子類重寫,所以最終會調用到實際servlet的doget方法或者doPost方法
十、寫HttpServletResponse邏輯
我們已經完成了請求的接受和處理,接下來需要將信息返回給前端
創建BufferOutputStream包裝流將socket.outputStream包裝后進行發送,需要注意的是,發送回前端的數據需要遵循特定的響應格式,我們再util包下創建一個工具類將數據封裝到固定格式中
package com.qcby;public class ResponseUtil {public static final String responseHeader200 = "HTTP/1.1 200 \r\n"+"Content-Type:text/html; charset=utf-8 \r\n"+"\r\n";public static String getResponseHeader404(){return "HTTP/1.1 404 \r\n"+"Content-Type:text/html; charset=utf-8 \r\n"+"\r\n" + "404";}public static String getResponseHeader200(String context){return "HTTP/1.1 200 \r\n"+"Content-Type:text/html; charset=utf-8 \r\n"+"\r\n" + context;}
}
之后通過bufferoutputStream的write方法將數據返回給前端,注意發送后需要進行刷新
import com.qcby.ResponseUtil;
import java.io.BufferedOutputStream;
import java.io.IOException;import java.net.Socket;
import java.nio.charset.StandardCharsets;public class HttpServletResponse {Socket socket;public HttpServletResponse(Socket socket){this.socket = socket;}public void write(String s) throws IOException {BufferedOutputStream outputStream = new BufferedOutputStream(socket.getOutputStream());s = ResponseUtil.getResponseHeader200(s);outputStream.write(s.getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
tomcat的整體流程如上,現在測試一下接收前端數據以及向前端返回數據信息: