寫在前面
Apache Tomcat 是Java Servlet, JavaServer Pages (JSP),Java表達式語言和Java的WebSocket技術的一個開源實現 ,通常我們將Tomcat稱為Web容器或者Servlet容器 。
今天,我們就來手寫tomcat,但是說明一下:咱們不是為了裝逼才來寫tomcat,而是希望大家能更多的理解和掌握tomcat。
廢話不多說了,直接開干。
我們可以把上面這張架構圖做簡化,簡化后為:
什么是http協議
Http是一種網絡應用層協議,規(guī)定了瀏覽器與web服務器之間如何通信以及數據包的結構。
通信大致可以分為四步:
優(yōu)點
web服務器可以利用有限的連接為盡可能多的客戶請求服務。
可以總結唯一張圖:
Servlet是JavaEE規(guī)范的一種,主要是為了擴展Java作為Web服務的功能,統(tǒng)一接口。由其他內部廠商如tomcat,jetty內部實現web的功能。如一個http請求到來:容器將請求封裝為servlet中的HttpServletRequest對象,調用init(),service()等方法輸出response,由容器包裝為httpresponse返回給客戶端的過程。
使用Socket編程,實現簡單的客戶端和服務端的聊天。
服務端代碼如下:
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 = "\n";
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("服務端已經準備好了");
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);
}
}
}
過程如下:
實現代碼如下:
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;
/**
* 啟動函數
*
* @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();
}
}
再寫一個工具類,內容如下;
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 \n" + "Content-Type: text/html \n"
+ "Content-Length: " + contentLength + " \n" + "\r\n";
}
/**
* 為響應碼 404 提供請求頭信息(此處也包含了數據內容)
*
* @return 404 header info
*/
public static String getHttpHeader404() {
String str404 = "<h1>404 not found</h1>";
return "HTTP/1.1 404 NOT Found \n" + "Content-Type: text/html \n"
+ "Content-Length: " + str404.getBytes().length + " \n" + "\r\n" + str404;
}
}
啟動main方法:
使用IDEA訪問:
在瀏覽器訪問:
自此,我們的第二版本搞定。下面繼續(xù)第三個版本;
一個http協議的請求包含三部分:
比如
POST /index.html HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
Content-Length: 33
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
lastName=tian&firstName=JohnTian
簡單的解釋
類似于http協議的請求,響應也包含三個部分。
比如:
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 112
<html>
<head>
<title>HTTP Response Example</title> </head>
<body>
Welcome to Brainy Software
</body>
</html>
簡單解釋
代碼實現
創(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;
/**
* 提取了一些共用類和函數
*/
public class ResourceUtil {
/**
* 根據請求 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) {
if (written + byteSize > resourceSize) {
byteSize = (int) (resourceSize - written);
bytes = new byte[byteSize];
}
input.read(bytes);
output.write(bytes);
output.flush();
written += byteSize;
}
}
}
另外HttpProtocolUtil照樣用第二版本中。
再創(chuàng)建Request類,用來解析并存放請求相關參數。
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;
/**
* 請求信息輸入流 <br>
* 示例
* <pre>
* 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
* </pre>
*/
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");
// 讀取第一行數據,即:GET / HTTP/1.1
String firstLine = requestStringArray[0];
// 遍歷第一行數據按空格分隔
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進行小小調整:
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);
// 根據 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,內容很簡單:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello world</title>
</head>
<body>
<h2> you already succeed!</h2>
</body>
</html>
這一需要注意,index.html文件的存放路徑不放錯了,視本地路徑來定哈,放在classes文件夾下的。你可以debug試試,看看你應該放在那個目錄下。
啟動MyTomcat。
訪問http://localhost:8090/index.html
自此,我們針對于Http請求參數和相應參數做了一個簡單的解析以及封裝。
盡管其中還有很多問題,但是字少看起來有那點像樣了。我們繼續(xù)第四版,
用過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來實現Servlet。
代碼實現如下:
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 = "<h1> LoginServlet by GET method</h1>";
response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
}
@Override
public void doPost(Request request, Response response) throws Exception {
String repText = "<h1>LoginServlet by POST method</h1>";
response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() throws Exception {
}
}
大家是否還記得,我們在學習Servlet的時候,在resources目錄下面有個web.xml。我們這個版本也把這個xml文件給引入。
<?xml version="1.0" encoding="utf-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.tian.v4.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
</web-app>
既然引入了xml文件,那我們就需要去讀取這個xml文件,并解析器內容。所以這里我們需要引入兩個jar包。
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
萬事俱備,只欠東風了。這時候我們來吧MyTomcat這個類做一些調整即可。
下面有個很重要的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<String, HttpServlet> 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);
// 根據 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<Element> list = rootElement.selectNodes("//servlet");
for (Element element : list) {
// <servlet-name>show</servlet-name>
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
// <servlet-class>server.ShowServlet</servlet-class>
Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
String servletClass = servletclassElement.getStringValue();
// 根據 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:同步并阻塞,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
所以,大家在網上看到的手寫tomcat的,也有使用線程池來做的,這里希望大家能get到為什么使用線程池來實現。另外,其實在tomcat高版本中已經沒有使用BIO了。
而 HTTP/1.1默認使用的就是NIO了。
但這個只是通信方式,重點是我們要理解和掌握tomcat的整體實現。
另外,發(fā)現上面都是講配置文件解析,并將對應數據保存起來。熟悉這個套路后,大家是不是想到,我們很多配置項都是在server.xml中,還記得否?也是可以通過解析某個目錄下的server.xml文件,并把內容賦給java中相應的變量罷了。
比如:
1.server.xml中的端口配置,我們是在代碼里寫死的而已,改成MyTomcat啟動的時候去解析并獲取不久得了嗎?
2.我們通常是將我們項目的打成war,然后解壓到某個目錄下,最后還不是可以通過讀取這個解壓后的某個目錄中找到web.xml,然后用回到上面的web.xml解析了。
本文主要是分享如何從一個塑料版到黃金版、然后鉑金版,最后到磚石版。可以把加入線程池的版本稱之為星耀版,最后把相關server.xml解析,以及讀取我們放入到tomcat中項目解析可以稱之為王者版。
技術點:Socket編程、InputStream、OutputStream、線程池、xml文件解析、反射。更高級版本中NIO,AIO等。
不是為了裝逼而來搞這個tomcat,而是為了我們更深刻的理解tomcat的原理。
參考:http://ccx4.cn/Sr5yL
部分圖片來源網絡,侵刪!
聯系客服