寫在前面
Apache Tomcat 是Java Servlet, JavaServer Pages (JSP),Java表達式語言和Java的WebSocket技術(shù)的一個開源實現(xiàn) ,通常我們將Tomcat稱為Web容器或者Servlet容器 。
今天,我們就來手寫tomcat,但是說明一下:咱們不是為了裝逼才來寫tomcat,而是希望大家能更多的理解和掌握tomcat。
廢話不多說了,直接開干。
基本結(jié)構(gòu)
tomcat架構(gòu)圖
我們可以把上面這張架構(gòu)圖做簡化,簡化后為:
什么是http協(xié)議
Http是一種網(wǎng)絡應用層協(xié)議,規(guī)定了瀏覽器與web服務器之間如何通信以及數(shù)據(jù)包的結(jié)構(gòu)。
通信大致可以分為四步:
優(yōu)點
web服務器可以利用有限的連接為盡可能多的客戶請求服務。
tomcat中Servlet的運作方式
可以總結(jié)唯一張圖:
什么是Servlet呢?
Servlet是JavaEE規(guī)范的一種,主要是為了擴展Java作為Web服務的功能,統(tǒng)一接口。由其他內(nèi)部廠商如tomcat,jetty內(nèi)部實現(xiàn)web的功能。如一個http請求到來:容器將請求封裝為servlet中的HttpServletRequest對象,調(diào)用init(),service()等方法輸出response,由容器包裝為httpresponse返回給客戶端的過程。
什么是Servlet規(guī)范?
- 從 Jar 包上來說,Servlet 規(guī)范就是兩個 Jar 文件。servlet-api.jar 和 jsp-api.jar,Jsp 也是一種 Servlet。
- 從package上來說,就是 javax.servlet 和 javax.servlet.http 兩個包。
- 從接口來說,就是規(guī)范了 Servlet 接口、Filter 接口、Listener 接口、ServletRequest 接口、ServletResponse 接口等。類圖如下:
第一版:Socket版
使用Socket編程,實現(xiàn)簡單的客戶端和服務端的聊天。
服務端代碼如下:
package com.tian.v1;import java.io.*;import java.net.*;public class Server { public static String readline = null; public static String inTemp = null; public static String turnLine = “”; public static final String client = “客戶端:”; public static final String server = “服務端:”; public static final int PORT = 8090; public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(PORT); System.out.println(“服務端已經(jīng)準備好了”); Socket socket = serverSocket.accept(); BufferedReader systemIn = new BufferedReader(new InputStreamReader(System.in)); BufferedReader socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter socketOut = new PrintWriter(socket.getOutputStream()); while (true) { inTemp = socketIn.readLine(); if (inTemp != null &&inTemp.contains(“over”)) { systemIn.close(); socketIn.close(); socketOut.close(); socket.close(); serverSocket.close(); } System.out.println(client + inTemp); System.out.print(server); readline = systemIn.readLine(); socketOut.println(readline); socketOut.flush(); } }}
客戶端代碼如下:
package com.tian.v1;import java.io.*;import java.net.*;public class Client { public static void main(String[] args) throws Exception { String readline; String inTemp; final String client = “客戶端說:”; final String server = “服務端回復:”; int port = 8090; byte[] ipAddressTemp = {127, 0, 0, 1}; InetAddress ipAddress = InetAddress.getByAddress(ipAddressTemp); //首先直接創(chuàng)建socket,端口號1~1023為系統(tǒng)保存,一般設在1023之外 Socket socket = new Socket(ipAddress, port); BufferedReader systemIn = new BufferedReader(new InputStreamReader(System.in)); BufferedReader socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter socketOut = new PrintWriter(socket.getOutputStream()); while (true) { System.out.print(client); readline = systemIn.readLine(); socketOut.println(readline); socketOut.flush(); //處理 inTemp = socketIn.readLine(); if (inTemp != null && inTemp.contains(“over”)) { systemIn.close(); socketIn.close(); socketOut.close(); socket.close(); } System.out.println(server + inTemp); } }}
過程如下:
,時長00:44
第二版:我們直接請求http://localhost:8090
實現(xiàn)代碼如下:
package com.tian.v2;import java.io.IOException;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class MyTomcat { /** * 設定啟動和監(jiān)聽端口 */ private int port = 8090; /** * 啟動函數(shù) * * @throws IOException */ public void start() throws IOException { System.out.println(“my tomcat starting…”); String responseData = “6666666”; ServerSocket socket = new ServerSocket(port); while (true) { Socket accept = socket.accept(); OutputStream outputStream = accept.getOutputStream(); String responseText = HttpProtocolUtil.getHttpHeader200(responseData.length()) + responseData; outputStream.write(responseText.getBytes()); accept.close(); } } /** * 啟動入口 */ public static void main(String[] args) throws IOException { MyTomcat tomcat = new MyTomcat(); tomcat.start(); }}
再寫一個工具類,內(nèi)容如下;
ackage com.tian.v2;public class HttpProtocolUtil { /** * 200 狀態(tài)碼,頭信息 * * @param contentLength 響應信息長度 * @return 200 header info */ public static String getHttpHeader200(long contentLength) { return “HTTP/1.1 200 OK ” + “Content-Type: text/html ” + “Content-Length: ” + contentLength + ” ” + “r”; } /** * 為響應碼 404 提供請求頭信息(此處也包含了數(shù)據(jù)內(nèi)容) * * @return 404 header info */ public static String getHttpHeader404() { String str404 = “
404 not found
“; return “HTTP/1.1 404 NOT Found ” + “Content-Type: text/html ” + “Content-Length: ” + str404.getBytes().length + ” ” + “r” + str404; }}
啟動main方法:
使用IDEA訪問:
在瀏覽器訪問:
自此,我們的第二版本搞定。下面繼續(xù)第三個版本;
第三版:封裝請求信息和響應信息
一個http協(xié)議的請求包含三部分:
- 方法 URI 協(xié)議/版本
- 請求的頭部
- 主體內(nèi)容
比如
POST /index.html HTTP/1.1Accept: text/plain; text/htmlAccept-Language: en-gbConnection: Keep-AliveHost: localhostUser-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)Content-Length: 33Content-Type: application/x-www-form-urlencodedAccept-Encoding: gzip, deflatelastName=tian&firstName=JohnTian
簡單的解釋
- 數(shù)據(jù)的第一行包括:方法、URI、協(xié)議和版本。在這個例子里,方法為POST,URI為/index.html,協(xié)議為HTTP/1.1,協(xié)議版本號為1.1。他們之間通過空格來分離。
- 請求頭部從第二行開始,使用英文冒號(:)來分離鍵和值。
- 請求頭部和主體內(nèi)容之間通過空行來分離,例子中的請求體為表單數(shù)據(jù)。
類似于http協(xié)議的請求,響應也包含三個部分。
- 協(xié)議 狀態(tài) 狀態(tài)描述
- 響應的頭部
- 主體內(nèi)容
比如:
HTTP/1.1 200 OKServer: Microsoft-IIS/4.0Date: Mon, 5 Jan 2004 13:13:33 GMTContent-Type: text/htmlLast-Modified: Mon, 5 Jan 2004 13:13:12 GMTContent-Length: 112HTTP Response Example Welcome to Brainy Software
簡單解釋
- 第一行,HTTP/1.1 200 OK表示協(xié)議、狀態(tài)和狀態(tài)描述。
- 之后表示響應頭部。
- 響應頭部和主體內(nèi)容之間使用空行來分離。
代碼實現(xiàn)
創(chuàng)建一個工具類,用來獲取靜態(tài)資源信息。
package com.tian.v3;import com.tian.v2.HttpProtocolUtil;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;/** * 提取了一些共用類和函數(shù) */public class ResourceUtil { /** * 根據(jù)請求 url 獲取完整絕對路徑 */ public static String getPath(String url) { String path = ResourceUtil.class.getResource(“/”).getPath(); return path.replaceAll(“”, “/”) + url; } /** * 輸出靜態(tài)資源信息 */ public static void outputResource(InputStream input, OutputStream output) throws IOException { int count = 0; while (count == 0) { count = input.available(); } int resourceSize = count; output.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes()); long written = 0; int byteSize = 1024; byte[] bytes = new byte[byteSize]; while (written resourceSize) { byteSize = (int) (resourceSize – written); bytes = new byte[byteSize]; } input.read(bytes); output.write(bytes); output.flush(); written += byteSize; } }}
另外HttpProtocolUtil照樣用第二版本中。
再創(chuàng)建Request類,用來解析并存放請求相關(guān)參數(shù)。
package com.tian.v3;import java.io.IOException;import java.io.InputStream;public class Request { /** * 請求方式, eg: GET、POST */ private String method; /** * 請求路徑,eg: /index.html */ private String url; /** * 請求信息輸入流 * 示例 * * GET / HTTP/1.1 * Host: localhost * Connection: keep-alive * Pragma: no-cache * Cache-Control: no-cache * Upgrade-Insecure-Requests: 1 * User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 * */ private InputStream inputStream; public Request() { } public Request(InputStream inputStream) throws IOException { this.inputStream = inputStream; int count = 0; while (count == 0) { count = inputStream.available(); } byte[] bytes = new byte[count]; inputStream.read(bytes); // requestString 參考:this.inputStream 示例 String requestString = new String(bytes); // 按換行分隔 String[] requestStringArray = requestString.split(“n”); // 讀取第一行數(shù)據(jù),即:GET / HTTP/1.1 String firstLine = requestStringArray[0]; // 遍歷第一行數(shù)據(jù)按空格分隔 String[] firstLineArray = firstLine.split(” “); this.method = firstLineArray[0]; this.url = firstLineArray[1]; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public InputStream getInputStream() { return inputStream; } public void setInputStream(InputStream inputStream) { this.inputStream = inputStream; }}
把第二版的MyTomcat進行小小調(diào)整:
package com.tian.v3;import java.io.IOException;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class MyTomcat { private static final int PORT = 8090; public void start() throws IOException { System.out.println(“my tomcat starting…”); ServerSocket socket = new ServerSocket(PORT); while (true) { Socket accept = socket.accept(); OutputStream outputStream = accept.getOutputStream(); // 分別封裝 Request 和 Response Request request = new Request(accept.getInputStream()); Response response = new Response(outputStream); // 根據(jù) request 中的 url,輸出 response.outputHtml(request.getUrl()); accept.close(); } } public static void main(String[] args) throws IOException { MyTomcat tomcat = new MyTomcat(); tomcat.start(); }}
然后再創(chuàng)建一個index.html,內(nèi)容很簡單:
hello world
you already succeed!
這一需要注意,index.html文件的存放路徑不放錯了,視本地路徑來定哈,放在classes文件夾下的。你可以debug試試,看看你應該放在那個目錄下。
啟動MyTomcat。
訪問http://localhost:8090/index.html
自此,我們針對于Http請求參數(shù)和相應參數(shù)做了一個簡單的解析以及封裝。
盡管其中還有很多問題,但是字少看起來有那點像樣了。我們繼續(xù)第四版,
第四版:實現(xiàn)動態(tài)請求資源
用過servlet的同學都知道,Servlet中有三個很重要的方法init、destroy 、service 。其中還記得我們自己寫LoginServlet的時候,還會重寫HttpServlet中的doGet()和doPost()方法。下面?zhèn)兙妥约簛砀阋粋€:
Servlet類代碼如下:
public interface Servlet { void init() throws Exception; void destroy() throws Exception; void service(Request request, Response response) throws Exception;}
然后再寫一個HttpServlet來實現(xiàn)Servlet。
代碼實現(xiàn)如下:
package com.tian.v4;public abstract class HttpServlet implements Servlet { @Override public void init() throws Exception { } @Override public void destroy() throws Exception { } @Override public void service(Request request, Response response) throws Exception { String method = request.getMethod(); if (“GET”.equalsIgnoreCase(method)) { doGet(request, response); } else { doPost(request, response); } } public abstract void doGet(Request request, Response response) throws Exception; public abstract void doPost(Request request, Response response) throws Exception;}
下面我們就來寫一個自己的Servlet,比如LoginServlet。
package com.tian.v4;public class LoginServlet extends HttpServlet { @Override public void doGet(Request request, Response response) throws Exception { String repText = “
LoginServlet by GET method
“; response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText); } @Override public void doPost(Request request, Response response) throws Exception { String repText = “
LoginServlet by POST method
“; response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText); } @Override public void init() throws Exception { } @Override public void destroy() throws Exception { }}
大家是否還記得,我們在學習Servlet的時候,在resources目錄下面有個web.xml。我們這個版本也把這個xml文件給引入。
login com.tian.v4.LoginServlet login /login
既然引入了xml文件,那我們就需要去讀取這個xml文件,并解析器內(nèi)容。所以這里我們需要引入兩個jar包。
dom4j dom4j 1.6.1 jaxen jaxen 1.1.6
萬事俱備,只欠東風了。這時候我們來吧MyTomcat這個類做一些調(diào)整即可。
下面有個很重要的initServlet()方法,剛剛是對應下面這張圖中的List servlets,但是我們代碼里使用的是Map來存儲Servlet的,意思就那么個意思,把Servlet放在集合里。
這也就是為什么大家都把Tomcat叫做Servlet容器的原因,其實真正的容器還是java集合。
package com.tian.v4;import com.tian.v3.RequestV3;import com.tian.v3.ResponseV3;import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;import java.util.HashMap;import java.util.List;import java.util.Map;public class MyTomcat { /** * 設定啟動和監(jiān)聽端口 */ private static final int PORT = 8090; /** * 存放 Servlet信息,url: Servlet 實例 */ private Map servletMap = new HashMap(); public void start() throws Exception { System.out.println(“my tomcat starting…”); initServlet(); ServerSocket socket = new ServerSocket(PORT); while (true) { Socket accept = socket.accept(); OutputStream outputStream = accept.getOutputStream(); // 分別封裝 RequestV3 和 ResponseV3 RequestV4 requestV3 = new RequestV4(accept.getInputStream()); ResponseV4 responseV3 = new ResponseV4(outputStream); // 根據(jù) url 來獲取 Servlet HttpServlet httpServlet = servletMap.get(requestV3.getUrl()); // 如果 Servlet 為空,說明是靜態(tài)資源,不為空即為動態(tài)資源,需要執(zhí)行 Servlet 里的方法 if (httpServlet == null) { responseV3.outputHtml(requestV3.getUrl()); } else { httpServlet.service(requestV3, responseV3); } accept.close(); } } public static void main(String[] args) throws Exception { MyTomcat tomcat = new MyTomcat(); tomcat.start(); } /** * 解析web.xml文件,把url和servlet解析出來, * 并保存到一個java集合里(Map) */ public void initServlet() throws Exception { InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(“web.xml”); SAXReader saxReader = new SAXReader(); Document document = saxReader.read(resourceAsStream); Element rootElement = document.getRootElement(); List list = rootElement.selectNodes(“//servlet”); for (Element element : list) { // show Element servletnameElement = (Element) element.selectSingleNode(“servlet-name”); String servletName = servletnameElement.getStringValue(); // server.ShowServlet Element servletclassElement = (Element) element.selectSingleNode(“servlet-class”); String servletClass = servletclassElement.getStringValue(); // 根據(jù) servlet-name 的值找到 url-pattern Element servletMapping = (Element) rootElement.selectSingleNode(“/web-app/servlet-mapping[servlet-name='” + servletName + “‘]”); // /show String urlPattern = servletMapping.selectSingleNode(“url-pattern”).getStringValue(); servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).getDeclaredConstructor().newInstance()); } }}
啟動,再次訪問http://localhost:8090/index.html
同時,我們可以訪問http://localhost:8090/login
到此,第四個版本也搞定了。
但是前面四個版本都有一個共同的問題,全部使用的是BIO。
BIO:同步并阻塞,服務器實現(xiàn)模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
所以,大家在網(wǎng)上看到的手寫tomcat的,也有使用線程池來做的,這里希望大家能get到為什么使用線程池來實現(xiàn)。另外,其實在tomcat高版本中已經(jīng)沒有使用BIO了。
而 HTTP/1.1默認使用的就是NIO了。
但這個只是通信方式,重點是我們要理解和掌握tomcat的整體實現(xiàn)。
總結(jié)
另外,發(fā)現(xiàn)上面都是講配置文件解析,并將對應數(shù)據(jù)保存起來。熟悉這個套路后,大家是不是想到,我們很多配置項都是在server.xml中,還記得否?也是可以通過解析某個目錄下的server.xml文件,并把內(nèi)容賦給java中相應的變量罷了。
比如:
1.server.xml中的端口配置,我們是在代碼里寫死的而已,改成MyTomcat啟動的時候去解析并獲取不久得了嗎?
2.我們通常是將我們項目的打成war,然后解壓到某個目錄下,最后還不是可以通過讀取這個解壓后的某個目錄中找到web.xml,然后用回到上面的web.xml解析了。
本文主要是分享如何從一個塑料版到黃金版、然后鉑金版,最后到磚石版??梢园鸭尤刖€程池的版本稱之為星耀版,最后把相關(guān)server.xml解析,以及讀取我們放入到tomcat中項目解析可以稱之為王者版。