寫在前面
Apache Tomcat 是Java Servlet, JavaServer Pages (JSP),Java表達(dá)式語(yǔ)言和Java的WebSocket技術(shù)的一個(gè)開源實(shí)現(xiàn) ,通常我們將Tomcat稱為Web容器或者Servlet容器 。
今天,我們就來手寫tomcat,但是說明一下:咱們不是為了裝逼才來寫tomcat,而是希望大家能更多的理解和掌握tomcat。
廢話不多說了,直接開干。
基本結(jié)構(gòu)
tomcat架構(gòu)圖
我們可以把上面這張架構(gòu)圖做簡(jiǎn)化,簡(jiǎn)化后為:
什么是http協(xié)議
Http是一種網(wǎng)絡(luò)應(yīng)用層協(xié)議,規(guī)定了瀏覽器與web服務(wù)器之間如何通信以及數(shù)據(jù)包的結(jié)構(gòu)。
通信大致可以分為四步:
優(yōu)點(diǎn)
web服務(wù)器可以利用有限的連接為盡可能多的客戶請(qǐng)求服務(wù)。
tomcat中Servlet的運(yùn)作方式
可以總結(jié)唯一張圖:
什么是Servlet呢?
Servlet是JavaEE規(guī)范的一種,主要是為了擴(kuò)展Java作為Web服務(wù)的功能,統(tǒng)一接口。由其他內(nèi)部廠商如tomcat,jetty內(nèi)部實(shí)現(xiàn)web的功能。如一個(gè)http請(qǐng)求到來:容器將請(qǐng)求封裝為servlet中的HttpServletRequest對(duì)象,調(diào)用init(),service()等方法輸出response,由容器包裝為httpresponse返回給客戶端的過程。
什么是Servlet規(guī)范?
- 從 Jar 包上來說,Servlet 規(guī)范就是兩個(gè) Jar 文件。servlet-api.jar 和 jsp-api.jar,Jsp 也是一種 Servlet。
- 從package上來說,就是 javax.servlet 和 javax.servlet.http 兩個(gè)包。
- 從接口來說,就是規(guī)范了 Servlet 接口、Filter 接口、Listener 接口、ServletRequest 接口、ServletResponse 接口等。類圖如下:
第一版:Socket版
使用Socket編程,實(shí)現(xiàn)簡(jiǎn)單的客戶端和服務(wù)端的聊天。
服務(wù)端代碼如下:
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 = “服務(wù)端:”; public static final int PORT = 8090; public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(PORT); System.out.println(“服務(wù)端已經(jīng)準(zhǔn)備好了”); 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 = “服務(wù)端回復(fù):”; int port = 8090; byte[] ipAddressTemp = {127, 0, 0, 1}; InetAddress ipAddress = InetAddress.getByAddress(ipAddressTemp); //首先直接創(chuàng)建socket,端口號(hào)1~1023為系統(tǒng)保存,一般設(shè)在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); } }}
過程如下:
,時(shí)長(zhǎng)00:44
第二版:我們直接請(qǐng)求http://localhost:8090
實(shí)現(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 { /** * 設(shè)定啟動(dòng)和監(jiān)聽端口 */ private int port = 8090; /** * 啟動(dòng)函數(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(); } } /** * 啟動(dòng)入口 */ public static void main(String[] args) throws IOException { MyTomcat tomcat = new MyTomcat(); tomcat.start(); }}
再寫一個(gè)工具類,內(nèi)容如下;
ackage com.tian.v2;public class HttpProtocolUtil { /** * 200 狀態(tài)碼,頭信息 * * @param contentLength 響應(yīng)信息長(zhǎng)度 * @return 200 header info */ public static String getHttpHeader200(long contentLength) { return “HTTP/1.1 200 OK ” + “Content-Type: text/html ” + “Content-Length: ” + contentLength + ” ” + “r”; } /** * 為響應(yīng)碼 404 提供請(qǐng)求頭信息(此處也包含了數(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; }}
啟動(dòng)main方法:
使用IDEA訪問:
在瀏覽器訪問:
自此,我們的第二版本搞定。下面繼續(xù)第三個(gè)版本;
第三版:封裝請(qǐng)求信息和響應(yīng)信息
一個(gè)http協(xié)議的請(qǐng)求包含三部分:
- 方法 URI 協(xié)議/版本
- 請(qǐng)求的頭部
- 主體內(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
簡(jiǎn)單的解釋
- 數(shù)據(jù)的第一行包括:方法、URI、協(xié)議和版本。在這個(gè)例子里,方法為POST,URI為/index.html,協(xié)議為HTTP/1.1,協(xié)議版本號(hào)為1.1。他們之間通過空格來分離。
- 請(qǐng)求頭部從第二行開始,使用英文冒號(hào)(:)來分離鍵和值。
- 請(qǐng)求頭部和主體內(nèi)容之間通過空行來分離,例子中的請(qǐng)求體為表單數(shù)據(jù)。
類似于http協(xié)議的請(qǐng)求,響應(yīng)也包含三個(gè)部分。
- 協(xié)議 狀態(tài) 狀態(tài)描述
- 響應(yīng)的頭部
- 主體內(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
簡(jiǎn)單解釋
- 第一行,HTTP/1.1 200 OK表示協(xié)議、狀態(tài)和狀態(tài)描述。
- 之后表示響應(yīng)頭部。
- 響應(yīng)頭部和主體內(nèi)容之間使用空行來分離。
代碼實(shí)現(xiàn)
創(chuàng)建一個(gè)工具類,用來獲取靜態(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ù)請(qǐng)求 url 獲取完整絕對(duì)路徑 */ 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類,用來解析并存放請(qǐng)求相關(guān)參數(shù)。
package com.tian.v3;import java.io.IOException;import java.io.InputStream;public class Request { /** * 請(qǐng)求方式, eg: GET、POST */ private String method; /** * 請(qǐng)求路徑,eg: /index.html */ private String url; /** * 請(qǐng)求信息輸入流 * 示例 * * 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進(jìn)行小小調(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)建一個(gè)index.html,內(nèi)容很簡(jiǎn)單:
hello world
you already succeed!
這一需要注意,index.html文件的存放路徑不放錯(cuò)了,視本地路徑來定哈,放在classes文件夾下的。你可以debug試試,看看你應(yīng)該放在那個(gè)目錄下。
啟動(dòng)MyTomcat。
訪問http://localhost:8090/index.html
自此,我們針對(duì)于Http請(qǐng)求參數(shù)和相應(yīng)參數(shù)做了一個(gè)簡(jiǎn)單的解析以及封裝。
盡管其中還有很多問題,但是字少看起來有那點(diǎn)像樣了。我們繼續(xù)第四版,
第四版:實(shí)現(xiàn)動(dòng)態(tài)請(qǐng)求資源
用過servlet的同學(xué)都知道,Servlet中有三個(gè)很重要的方法init、destroy 、service 。其中還記得我們自己寫LoginServlet的時(shí)候,還會(huì)重寫HttpServlet中的doGet()和doPost()方法。下面?zhèn)兙妥约簛砀阋粋€(gè):
Servlet類代碼如下:
public interface Servlet { void init() throws Exception; void destroy() throws Exception; void service(Request request, Response response) throws Exception;}
然后再寫一個(gè)HttpServlet來實(shí)現(xiàn)Servlet。
代碼實(shí)現(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;}
下面我們就來寫一個(gè)自己的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 { }}
大家是否還記得,我們?cè)趯W(xué)習(xí)Servlet的時(shí)候,在resources目錄下面有個(gè)web.xml。我們這個(gè)版本也把這個(gè)xml文件給引入。
login com.tian.v4.LoginServlet login /login
既然引入了xml文件,那我們就需要去讀取這個(gè)xml文件,并解析器內(nèi)容。所以這里我們需要引入兩個(gè)jar包。
dom4j dom4j 1.6.1 jaxen jaxen 1.1.6
萬(wàn)事俱備,只欠東風(fēng)了。這時(shí)候我們來吧MyTomcat這個(gè)類做一些調(diào)整即可。
下面有個(gè)很重要的initServlet()方法,剛剛是對(duì)應(yīng)下面這張圖中的List servlets,但是我們代碼里使用的是Map來存儲(chǔ)Servlet的,意思就那么個(gè)意思,把Servlet放在集合里。
這也就是為什么大家都把Tomcat叫做Servlet容器的原因,其實(shí)真正的容器還是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 { /** * 設(shè)定啟動(dòng)和監(jiān)聽端口 */ private static final int PORT = 8090; /** * 存放 Servlet信息,url: Servlet 實(shí)例 */ 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)資源,不為空即為動(dòng)態(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解析出來, * 并保存到一個(gè)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()); } }}
啟動(dòng),再次訪問http://localhost:8090/index.html
同時(shí),我們可以訪問http://localhost:8090/login
到此,第四個(gè)版本也搞定了。
但是前面四個(gè)版本都有一個(gè)共同的問題,全部使用的是BIO。
BIO:同步并阻塞,服務(wù)器實(shí)現(xiàn)模式為一個(gè)連接一個(gè)線程,即客戶端有連接請(qǐng)求時(shí)服務(wù)器端就需要啟動(dòng)一個(gè)線程進(jìn)行處理,如果這個(gè)連接不做任何事情會(huì)造成不必要的線程開銷,當(dāng)然可以通過線程池機(jī)制改善。
所以,大家在網(wǎng)上看到的手寫tomcat的,也有使用線程池來做的,這里希望大家能get到為什么使用線程池來實(shí)現(xiàn)。另外,其實(shí)在tomcat高版本中已經(jīng)沒有使用BIO了。
而 HTTP/1.1默認(rèn)使用的就是NIO了。
但這個(gè)只是通信方式,重點(diǎn)是我們要理解和掌握tomcat的整體實(shí)現(xiàn)。
總結(jié)
另外,發(fā)現(xiàn)上面都是講配置文件解析,并將對(duì)應(yīng)數(shù)據(jù)保存起來。熟悉這個(gè)套路后,大家是不是想到,我們很多配置項(xiàng)都是在server.xml中,還記得否?也是可以通過解析某個(gè)目錄下的server.xml文件,并把內(nèi)容賦給java中相應(yīng)的變量罷了。
比如:
1.server.xml中的端口配置,我們是在代碼里寫死的而已,改成MyTomcat啟動(dòng)的時(shí)候去解析并獲取不久得了嗎?
2.我們通常是將我們項(xiàng)目的打成war,然后解壓到某個(gè)目錄下,最后還不是可以通過讀取這個(gè)解壓后的某個(gè)目錄中找到web.xml,然后用回到上面的web.xml解析了。
本文主要是分享如何從一個(gè)塑料版到黃金版、然后鉑金版,最后到磚石版??梢园鸭尤刖€程池的版本稱之為星耀版,最后把相關(guān)server.xml解析,以及讀取我們放入到tomcat中項(xiàng)目解析可以稱之為王者版。