【Tomcat】手寫迷你版Tomcat
阿新 • • 發佈:2020-12-28
[Toc]
## 原始碼地址
https://github.com/CoderXiaohui/mini-tomcat
## 一,分析
### Mini版Tomcat需要實現的功能
作為一個伺服器軟體提供服務(通過瀏覽器客戶端傳送Http請求,它可以接收到請求進行處理,處理之後的結果返回瀏覽器客戶端)。
1. 提供服務,接收請求(socket通訊)
2. 請求資訊封裝成Request物件,封裝響應資訊Response物件
3. 客戶端請求資源,資源分為靜態資源(html)和動態資源(servlet)
4. 資源返回給客戶端瀏覽器
***Tomcat的入口就是一個main函式**
## 二,開發——準備工作
### 2.1 新建Maven工程
![image-20201227160427940](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227160427940.png)
![image-20201227161023307](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227161023307.png)
### 2.2 定義編譯級別
```xml
4.0.0
com.dxh
MiniCat
1.0-SNAPSHOT
org.apache.maven.plugins
maven-compiler-plugin
3.1
11
utf-8
```
### 2.3 新建主類編寫啟動入口和埠
這裡我們把socket監聽的埠號定義在主類中。
``` java
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方法主要就是監聽上面配置的埠,然後得到其輸出流,最後寫出。
``` java
/**
* 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();
}
}
```
**完整的程式碼:**
```java
/**
* 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/,可看到返回結果如下圖:
![image-20201227165334660](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227165334660.png)
**因為Http協議是一個應用層協議,其規定了請求頭、請求體、響應同樣,如果沒有這些東西的話瀏覽器無法正常顯示**。程式碼中直接把”Hello Minicat!“直接輸出了,
### 3.2 解決問題,修改程式碼:
1. 新建一個工具類,主要提供響應頭資訊
```java
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="resourceSize){ //剩餘未讀取大小不足一個1024長度,那就按照真實長度處理
byteSize= (int)(resourceSize-written); //剩餘的檔案內容長度
bytes=new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written+=byteSize;
}
}
```
把上述的第一步和第三步的方法封裝到一個類中:
```java
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 (writtenresourceSize){ //剩餘未讀取大小不足一個1024長度,那就按照真實長度處理
byteSize= (int)(resourceSize-written); //剩餘的檔案內容長度
bytes=new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
outputStream.flush();
written+=byteSize;
}
}
}
```
### 測試:
1. 修改**Bootstrap2.java**中的`start()`方法
```java
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();
}
}
```
2. 在專案的resources資料夾新建`index.html`檔案
```html
Static resource
Hello ~ Static resource
```
3. 執行main方法
4. 瀏覽器輸入:http://localhost:8080/index.html
5. 結果展現:
![image-20201227205755188](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227205755188.png)
## 五,開發——3.0版本
3.0版本就要定義Servlet了,大致分為以下幾步:
1. 定義servlet規範
2. 編寫Servlet
3. 載入解析Servlet配置
### 5.1 定義servlet規範
``` java
public interface Servlet {
void init() throws Exception;
void destroy() throws Exception;
void service(Request request,Response response) throws Exception;
}
```
定義一個抽象類,實現Servlet,並且增加兩個抽象方法`doGet` , `doPost`.
``` java
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方法
``` java
package server;
import java.io.IOException;
public class DxhServlet extends HttpServlet{
@Override
public void doGet(Request request, Response response) {
String content="
dxh
server.DxhServlet
dxh
/dxh
```
標準的配置Servlet的標籤。`servlet-class`改成自己寫的Servlet全限定類名,`url-pattern`為`/dxh`,一會請求http://localhost:8080/dxh,來訪問這個servlet
#### 5.3.2 解析配置檔案
**複製一份Bootstrap2.java,命名為Bootstrap3.java**
1. 載入解析相關的配置 ,web.xml
引入dom4j和jaxen的jar包
```xml
dom4j
dom4j
1.6.1
jaxen
jaxen
1.1.6
```
2. 在`Bootstrap3.java`中增加一個方法
```java
//用於下面儲存url-pattern以及其對應的servlet-class的例項化物件
private Map 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找到中與其匹配的
*/
List selectNodes = rootElement.selectNodes("//servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
/**
* 1, 找到所有的servlet標籤,找到servlet-name和servlet-class
*/
//dxh
Element servletNameElement =(Element)element.selectSingleNode("servlet-name");
String servletName = servletNameElement.getStringValue();
//server.DxhServlet
Element servletClassElement =(Element)element.selectSingleNode("servlet-class");
String servletClass = servletClassElement.getStringValue();
/**
* 2, 根據servlet-name找到中與其匹配的
*/
//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`,以及與其對應的``標籤下的`url-pattern`,然後存在Map中。注意,這裡Map的**Key是url-pattern**,**Value是servlet-class的例項化物件**。
### 5.4 接收請求,處理請求改造
![image-20201227230820862](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227230820862.png)
![image-20201227231617987](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227231617987.png)
這裡進行了判斷,判斷`servletMap`中是否存在url所對應的value,如果沒有,當作靜態資源訪問,如果有,取出並呼叫service方法,在HttpServlet的service方法中已經做了根據request判斷具體呼叫的是doGet還是doPost方法。
### 測試:
在瀏覽器中輸入:
http://localhost:8080/index.html,可以訪問靜態資源
![image-20201227231858786](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227231858786.png)
輸入:http://localhost:8080/dxh
![image-20201227231925246](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227231925246.png)
可以訪問【5.2中編寫的Servlet】動態資源~
**到此位置,一個簡單的Tomcat Demo已經完成。**
## 六,優化——多執行緒改造(不使用執行緒池)
### 6.1 問題分析
在現有的程式碼中,接收請求這部分它是一個IO模型——BIO,阻塞IO。
它存在一個問題,當一個請求還未處理完成時,再次訪問,會出現阻塞的情況。
**可以在`DxhServlet`的`doGet`方法中加入`Thread.sleep(10000);`然後訪問`http://localhost:8080/dxh`和`http://localhost:8080/index.html`做個測試**
那麼我們可以使用多執行緒對其進行改造。
![image-20201227233057004](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227233057004.png)
把上述程式碼放到一個新的執行緒中處理。
### 6.2 複製Bootstrap3
複製Bootstrap3,命名為Bootstrap4。把start()方法中上圖的部分(包括`socket.close()`)**剪下**到下面的**執行緒處理類的run方法**中:
### 6.3 定義一個執行緒處理類
```java
package server;
import java.io.InputStream;
import java.net.Socket;
import java.util.Map;
/**
* 執行緒處理類
*/
public class RequestProcessor extends Thread{
private Socket socket;
private Map servletMap;
public RequestProcessor(Socket socket, Map 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()方法
``` java
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()方法。執行緒池的使用不再贅述。程式碼如下:
``` java
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 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都需要做些什麼:
1. 定義一個入口類,需要監聽的埠號和入口方法——main方法
2. 定義servlet規範(介面),並實現它——HttpServlet
3. 編寫http協議工具類,主要提供響應頭資訊
4. 在main方法中呼叫start()方法用於啟動初始化和請求進來時的操作
5. 載入解析配置檔案(web.xml)
6. 當請求進來時,解析inputStream,並封裝為Request和Response物件。
7. 判斷請求資源的方式(動態資源還是靜態