原始碼地址
https://github.com/CoderXiaohui/mini-tomcat
一,分析
Mini版Tomcat需要實現的功能
作為一個伺服器軟體提供服務(通過瀏覽器客戶端傳送Http請求,它可以接收到請求進行處理,處理之後的結果返回瀏覽器客戶端)。
- 提供服務,接收請求(socket通訊)
- 請求資訊封裝成Request物件,封裝響應資訊Response物件
- 客戶端請求資源,資源分為靜態資源(html)和動態資源(servlet)
- 資源返回給客戶端瀏覽器
*Tomcat的入口就是一個main函式
二,開發——準備工作
2.1 新建Maven工程
2.2 定義編譯級別
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dxh</groupId>
<artifactId>MiniCat</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.3 新建主類編寫啟動入口和埠
這裡我們把socket監聽的埠號定義在主類中。
package server;
/**
* Minicat的主類
*/
public class Bootstrap {
/**
* 定義Socket監聽的埠號
*/
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* Minicat的啟動入口
* @param args
*/
public static void main(String[] args) {
}
}
三,開發——1.0版本
循序漸進,一點一點的完善,1.0版本我們需要的需求是:
- 瀏覽器請求http://localhost:8080,返回一個固定的字串到頁面“Hello Minicat”
3.1 編寫start方法以及遇到的問題
start方法主要就是監聽上面配置的埠,然後得到其輸出流,最後寫出。
/**
* MiniCat啟動需要初始化展開的一些操作
*/
public void start() throws IOException {
/*
完成Minicat 1.0版本
需求:瀏覽器請求http://localhost:8080,返回一個固定的字串到頁面“Hello Minicat!”
*/
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("========>>Minicat start on port:"+port);
while(true){
Socket socket = serverSocket.accept();
//有了socket,接收到請求,獲取輸出流
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello Minicat!".getBytes());
socket.close();
}
}
完整的程式碼:
/**
* Minicat的主類
*/
public class Bootstrap {
/**
* 定義Socket監聽的埠號
*/
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* Minicat的啟動入口
* @param args
*/
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
try {
//啟動Minicat
bootstrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* MiniCat啟動需要初始化展開的一些操作
*/
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("========>>Minicat start on port:"+port);
while(true){
Socket socket = serverSocket.accept();
//有了socket,接收到請求,獲取輸出流
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello Minicat!".getBytes());
socket.close();
}
}
}
此時,如果啟動專案,從瀏覽器中輸入http://localhost:8080/,能夠正常接收到請求嗎?
不能!
問題分析:
啟動專案,從瀏覽器中輸入http://localhost:8080/,可看到返回結果如下圖:
因為Http協議是一個應用層協議,其規定了請求頭、請求體、響應同樣,如果沒有這些東西的話瀏覽器無法正常顯示。程式碼中直接把”Hello Minicat!“直接輸出了,
3.2 解決問題,修改程式碼:
-
新建一個工具類,主要提供響應頭資訊
package server; /** * http協議工具類,主要提供響應頭資訊,這裡我們只提供200和404的情況 */ public class HttpProtocolUtil { /** * 為響應碼200提供請求頭資訊 */ 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提供請求頭資訊(也包含了資料內容) */ 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; } }
-
修改start方法
public void start() throws IOException { ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port:"+port); while(true){ Socket socket = serverSocket.accept(); OutputStream outputStream = socket.getOutputStream(); String data = "Hello Minicat!"; String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length)+data; outputStream.write(responseText.getBytes()); socket.close(); } }
-
訪問~
成功。
四,開發——2.0版本
需求:
- 封裝Request和Response物件
- 返回html靜態資原始檔
4.1 封裝前準備
新建一個類,Bootstrap2 (為了方便與1.0版本做對比)。獲得輸入流,並列印出來看看。
package server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Minicat的主類
*/
public class Bootstrap2 {
/**
* 定義Socket監聽的埠號
*/
private int port = 8080;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
/**
* Minicat的啟動入口
* @param args
*/
public static void main(String[] args) {
Bootstrap2 bootstrap = new Bootstrap2();
try {
//啟動Minicat
bootstrap.start();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* MiniCat啟動需要初始化展開的一些操作
*/
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("========>>Minicat start on port:"+port);
while (true){
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
//從輸入流中獲取請求資訊
int count = 0 ;
while (count==0){
count = inputStream.available();
}
byte[] bytes = new byte[count];
inputStream.read(bytes);
System.out.println("請求資訊=====>>"+new String(bytes));
socket.close();
}
}
}
列印出來的資訊:
這裡我們需要得到的是 請求方式(GET) 和 url (/) ,接下來封裝Request的時候也是隻封裝這兩個屬性
4.2封裝Request、Response物件
4.2.1 封裝Request
只封裝兩個引數——method和url
-
新建Request類
-
該類有三個屬性(
String method
、String url
、InputStream inputStream
)
method和url都是從input流中解析出來的。 -
GET SET方法
-
編寫有參構造
/** * 構造器 輸入流傳入 */ 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); String inputsStr = new String(bytes); //獲取第一行資料 String firstLineStr = inputsStr.split("\\n")[0]; //GET / HTTP/1.1 String[] strings = firstLineStr.split(" "); //把解析出來的資料賦值 this.method=strings[0]; this.url= strings[1]; System.out.println("method=====>>"+method); System.out.println("url=====>>"+url); }
-
無參構造
完整的Request.java:
package server;
import java.io.IOException;
import java.io.InputStream;
/**
* 把我們用到的請求資訊,封裝成Response物件 (根據inputSteam輸入流封裝)
*/
public class Request {
/**
* 請求方式 例如:GET/POST
*/
private String method;
/**
* / , /index.html
*/
private String url;
/**
* 其他的屬性都是通過inputStream解析出來的。
*/
private InputStream inputStream;
/**
* 構造器 輸入流傳入
*/
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);
String inputsStr = new String(bytes);
//獲取第一行資料
String firstLineStr = inputsStr.split("\\n")[0]; //GET / HTTP/1.1
String[] strings = firstLineStr.split(" ");
this.method=strings[0];
this.url= strings[1];
System.out.println("method=====>>"+method);
System.out.println("url=====>>"+url);
}
public Request() {
}
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;
}
a
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
}
4.2.2 封裝Response
package server;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* 封裝Response物件,需要依賴於OutputStream
*
*/
public class Response{
private OutputStream outputStream;
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
public Response() {
}
/**
* @param path 指的就是 Request中的url ,隨後要根據url來獲取到靜態資源的絕對路徑,進一步根據絕對路徑讀取該靜態資原始檔,最終通過輸出流輸出
*/
public void outputHtml(String path) throws IOException {
//獲取靜態資源的絕對路徑
String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
//輸出靜態資原始檔
File file = new File(absoluteResourcePath);
if (file.exists() && file.isFile()){
//讀取靜態資原始檔,輸出靜態資源
StaticResourceUtil.outputStaticResource(new FileInputStream(file),outputStream);
}else{
//輸出404
output(HttpProtocolUtil.getHttpHeader404());
}
}
//使用輸出流輸出指定字串
public void output(String context) throws IOException {
outputStream.write(context.getBytes());
}
}
2.0版本只考慮輸出靜態資原始檔
我們來分析一下outputHtml(String path)
這個方法
首先,path就指 Request中的url,我們要用這個url找到該資源的絕對路徑:
-
根據path,獲取靜態資源的絕對路徑
public static String getAbsolutePath(String path){ String absolutePath = StaticResourceUtil.class.getResource("/").getPath(); return absolutePath.replaceAll("\\\\","/")+path; }
-
判斷靜態資源是否存在
- 不存在:輸出404
-
存在:讀取靜態資原始檔,輸出靜態資源
public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException { int count = 0 ; while (count==0){ count=inputStream.available(); } //靜態資源長度 int resourceSize = count; //輸出Http請求頭 , 然後再輸出具體的內容 outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes()); //讀取內容輸出 long written = 0; //已經讀取的內容長度 int byteSize = 1024; //計劃每次緩衝的長度 byte[] bytes = new byte[byteSize]; while (written<resourceSize){ if (written+byteSize >resourceSize){ //剩餘未讀取大小不足一個1024長度,那就按照真實長度處理 byteSize= (int)(resourceSize-written); //剩餘的檔案內容長度 bytes=new byte[byteSize]; } inputStream.read(bytes); outputStream.write(bytes); outputStream.flush(); written+=byteSize; } }
把上述的第一步和第三步的方法封裝到一個類中:
package server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StaticResourceUtil {
/**
* 獲取靜態資源方法的絕對路徑
*/
public static String getAbsolutePath(String path){
String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\","/")+path;
}
/**
* 讀取靜態資原始檔輸入流,通過輸出流輸出
*/
public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
int count = 0 ;
while (count==0){
count=inputStream.available();
}
//靜態資源長度
int resourceSize = count;
//輸出Http請求頭 , 然後再輸出具體的內容
outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
//讀取內容輸出
long written = 0; //已經讀取的內容長度
int byteSize = 1024; //計劃每次緩衝的長度
byte[] bytes = new byte[byteSize];
while (written<resourceSize){
if (written+byteSize >resourceSize){ //剩餘未讀取大小不足一個1024長度,那就按照真實長度處理
byteSize= (int)(resourceSize-written); //剩餘的檔案內容長度
bytes=new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written+=byteSize;
}
}
}
測試:
-
修改Bootstrap2.java中的
start()
方法public void start() throws IOException { ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port:"+port); while (true){ Socket socket = serverSocket.accept(); InputStream inputStream = socket.getInputStream(); //封裝Resuest物件和Response物件 Request request = new Request(inputStream); Response response = new Response(socket.getOutputStream()); response.outputHtml(request.getUrl()); socket.close(); } }
-
在專案的resources資料夾新建
index.html
檔案<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Static resource </title> </head> <body> Hello ~ Static resource </body> </html>
-
執行main方法
-
結果展現:
五,開發——3.0版本
3.0版本就要定義Servlet了,大致分為以下幾步:
- 定義servlet規範
- 編寫Servlet
- 載入解析Servlet配置
5.1 定義servlet規範
public interface Servlet {
void init() throws Exception;
void destroy() throws Exception;
void service(Request request,Response response) throws Exception;
}
定義一個抽象類,實現Servlet,並且增加兩個抽象方法doGet
, doPost
.
public abstract class HttpServlet implements Servlet{
public abstract void doGet(Request request,Response response);
public abstract void doPost(Request request,Response response);
@Override
public void init() throws Exception {
}
@Override
public void destroy() throws Exception {
}
@Override
public void service(Request request, Response response) throws Exception {
if ("GET".equals(request.getMethod())){
doGet(request, response);
}else{
doPost(request, response);
}
}
}
5.2 編寫Servlet繼承HttpServlet
新建DxhServlet.java
,並繼承HttpServlet
重寫doGet和doPost方法
package server;
import java.io.IOException;
public class DxhServlet extends HttpServlet{
@Override
public void doGet(Request request, Response response) {
String content="<h1>DxhServlet get</h1>";
try {
response.output(HttpProtocolUtil.getHttpHeader200(content.getBytes().length)+content);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
String content="<h1>DxhServlet post</h1>";
try {
response.output(HttpProtocolUtil.getHttpHeader200(content.getBytes().length)+content);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void init() throws Exception {
super.init();
}
@Override
public void destroy() throws Exception {
super.destroy();
}
}
接下來要把DxhServlet
配置到一個配置檔案中,當MiniCat啟動時,載入進去。
5.3 載入解析Servlet配置
5.3.1 配置檔案
在resources目錄下,新建web.xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>dxh</servlet-name>
<servlet-class>server.DxhServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dxh</servlet-name>
<url-pattern>/dxh</url-pattern>
</servlet-mapping>
</web-app>
標準的配置Servlet的標籤。servlet-class
改成自己寫的Servlet全限定類名,url-pattern
為/dxh
,一會請求http://localhost:8080/dxh,來訪問這個servlet
5.3.2 解析配置檔案
複製一份Bootstrap2.java,命名為Bootstrap3.java
-
載入解析相關的配置 ,web.xml
引入dom4j和jaxen的jar包<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>
-
在
Bootstrap3.java
中增加一個方法//用於下面儲存url-pattern以及其對應的servlet-class的例項化物件 private Map<String,HttpServlet> servletMap = new HashMap<>(); private void loadServlet(){ InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml"); SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(resourceAsStream); //根元素 Element rootElement = document.getRootElement(); /** * 1, 找到所有的servlet標籤,找到servlet-name和servlet-class * 2, 根據servlet-name找到<servlet-mapping>中與其匹配的<url-pattern> */ List<Element> selectNodes = rootElement.selectNodes("//servlet"); for (int i = 0; i < selectNodes.size(); i++) { Element element = selectNodes.get(i); /** * 1, 找到所有的servlet標籤,找到servlet-name和servlet-class */ //<servlet-name>dxh</servlet-name> Element servletNameElement =(Element)element.selectSingleNode("servlet-name"); String servletName = servletNameElement.getStringValue(); //<servlet-class>server.DxhServlet</servlet-class> Element servletClassElement =(Element)element.selectSingleNode("servlet-class"); String servletClass = servletClassElement.getStringValue(); /** * 2, 根據servlet-name找到<servlet-mapping>中與其匹配的<url-pattern> */ //Xpath表示式:從/web-app/servlet-mapping下查詢,查詢出servlet-name=servletName的元素 Element servletMapping =(Element)rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']'"); // /dxh String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue(); servletMap.put(urlPattern,(HttpServlet) Class.forName(servletClass).newInstance()); } } catch (DocumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
這段程式碼的意思就是讀取web.xml轉換成Document,然後遍歷根元素內中的servlet標籤(servlet是可以配置多個的),通過XPath表示式獲得
servlet-name
、servlet-class
,以及與其對應的<servlet-mapping>
標籤下的url-pattern
,然後存在Map中。注意,這裡Map的Key是url-pattern,Value是servlet-class的例項化物件。
5.4 接收請求,處理請求改造
這裡進行了判斷,判斷servletMap
中是否存在url所對應的value,如果沒有,當作靜態資源訪問,如果有,取出並呼叫service方法,在HttpServlet的service方法中已經做了根據request判斷具體呼叫的是doGet還是doPost方法。
測試:
在瀏覽器中輸入:
http://localhost:8080/index.html,可以訪問靜態資源
可以訪問【5.2中編寫的Servlet】動態資源~
到此位置,一個簡單的Tomcat Demo已經完成。
六,優化——多執行緒改造(不使用執行緒池)
6.1 問題分析
在現有的程式碼中,接收請求這部分它是一個IO模型——BIO,阻塞IO。
它存在一個問題,當一個請求還未處理完成時,再次訪問,會出現阻塞的情況。
可以在DxhServlet
的doGet
方法中加入Thread.sleep(10000);
然後訪問http://localhost:8080/dxh
和http://localhost:8080/index.html
做個測試
那麼我們可以使用多執行緒對其進行改造。
把上述程式碼放到一個新的執行緒中處理。
6.2 複製Bootstrap3
複製Bootstrap3,命名為Bootstrap4。把start()方法中上圖的部分(包括socket.close()
)剪下到下面的執行緒處理類的run方法中:
6.3 定義一個執行緒處理類
package server;
import java.io.InputStream;
import java.net.Socket;
import java.util.Map;
/**
* 執行緒處理類
*/
public class RequestProcessor extends Thread{
private Socket socket;
private Map<String,HttpServlet> servletMap;
public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap) {
this.socket = socket;
this.servletMap = servletMap;
}
@Override
public void run() {
try{
InputStream inputStream = socket.getInputStream();
//封裝Resuest物件和Response物件
Request request = new Request(inputStream);
Response response = new Response(socket.getOutputStream());
String url = request.getUrl();
//靜態資源處理
if (servletMap.get(url)==null){
response.outputHtml(request.getUrl());
}else{
//動態資源處理
HttpServlet httpServlet = servletMap.get(url);
httpServlet.service(request,response);
}
socket.close();
}catch (Exception e){
}
}
}
6.4 修改Bootstrap4的start()方法
public void start() throws Exception {
//載入解析相關的配置 ,web.xml,把配置的servlet存入servletMap中
loadServlet();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("========>>Minicat start on port:"+port);
/**
* 可以請求動態資源
*/
while (true){
Socket socket = serverSocket.accept();
//使用多執行緒處理
RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap);
requestProcessor.start();
}
}
再次做6.1章節的測試, OK 沒有問題了。
七,優化——多執行緒改造(使用執行緒池)
這一步,我們使用執行緒池進行改造。
複製Bootstrap4,命名為Bootstrap5。
修改start()方法。執行緒池的使用不再贅述。程式碼如下:
public void start() throws Exception {
//載入解析相關的配置 ,web.xml,把配置的servlet存入servletMap中
loadServlet();
/**
* 定義執行緒池
*/
//基本大小
int corePoolSize = 10;
//最大
int maxPoolSize = 50;
//如果執行緒空閒的話,超過多久進行銷燬
long keepAliveTime = 100L;
//上面keepAliveTime的單位
TimeUnit unit = TimeUnit.SECONDS;
//請求佇列
BlockingQueue<Runnable> workerQueue = new ArrayBlockingQueue<>(50);
//執行緒工廠,使用預設的即可
ThreadFactory threadFactory = Executors.defaultThreadFactory();
//拒絕策略,如果任務太多處理不過來了,如何拒絕
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize
,maxPoolSize
,keepAliveTime
,unit
,workerQueue
,threadFactory
,handler);
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("========>>Minicat start on port(多執行緒):"+port);
/**
* 可以請求動態資源
*/
while (true){
Socket socket = serverSocket.accept();
RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap);
threadPoolExecutor.execute(requestProcessor);
}
}
OK ,再次測試,成功~
MINI版Tomcat到此完成。
八,總結
總結一下編寫一個MINI版本的Tomcat都需要做些什麼:
- 定義一個入口類,需要監聽的埠號和入口方法——main方法
- 定義servlet規範(介面),並實現它——HttpServlet
- 編寫http協議工具類,主要提供響應頭資訊
- 在main方法中呼叫start()方法用於啟動初始化和請求進來時的操作
- 載入解析配置檔案(web.xml)
- 當請求進來時,解析inputStream,並封裝為Request和Response物件。
- 判斷請求資源的方式(動態資源還是靜態資源)