@[TOC]
手寫一個(gè)簡(jiǎn)易版的tomcat
前言
使用tomcat的時(shí)候當(dāng)瀏覽器輸入url之后,開始發(fā)送http請(qǐng)求,這個(gè)請(qǐng)求發(fā)送到哪兒呢,Url解析的過程中
- 1 先通過域名解析請(qǐng)求得到ip
- 2 然后通過ip找到對(duì)應(yīng)的主機(jī)
- 3 再通過響應(yīng)的端口找到進(jìn)程
- 4 然后再去根據(jù)程序去處理這個(gè)請(qǐng)求,再到原路返回
思考
對(duì)于1,2步驟我們本地測(cè)試可以不用去扣這個(gè),明白這么回事兒就可以,本地localhost實(shí)際上對(duì)應(yīng)我們自己本機(jī)127.0.0.1
對(duì)于第三部,我們本地可以去通過一個(gè)socket去監(jiān)聽響應(yīng)的端口,去獲取到請(qǐng)求,然后再響應(yīng)給客戶端讓客戶端瀏覽器去解析我們返回的http報(bào)文,從而展示數(shù)據(jù);
具體如下步驟:
1)提供服務(wù),接收請(qǐng)求(可以使用Socket通信)
2)請(qǐng)求信息封裝成Request(Response)
3)客戶端請(qǐng)求資源,資源分為靜態(tài)資源(html)和動(dòng)態(tài)資源(Servlet)
4)資源返回給客戶端瀏覽器
具體實(shí)現(xiàn)
首先新建maven工程
然后定義一個(gè)啟動(dòng)類Bootstrap然后實(shí)現(xiàn)一個(gè)啟動(dòng)方法start,在這個(gè)方法中啟動(dòng)一個(gè)socket監(jiān)聽8080端口
package com.udeam.v1;
import com.udeam.util.HttpUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 啟動(dòng)類入庫(kù)
* 用于啟動(dòng)tomcat
*/
public class Bootstrap {
/**
* 監(jiān)聽端口號(hào)
* 用于啟動(dòng)socket監(jiān)聽的端口號(hào)
*/
private int port = 8080;
/**
* 啟動(dòng)方法
*/
public void start() throws IOException {
//返回固定字符串到客戶端
ServerSocket socket = new ServerSocket(port);
System.out.println("--------- start port : " + port);
while (true) {
Socket accept = socket.accept();
//獲取輸入流
//InputStream inputStream = accept.getInputStream();
//輸出流
OutputStream outputStream = accept.getOutputStream();
System.out.println(" ------ 響應(yīng)返回內(nèi)容 : " + result);
outputStream.write("hello world ...".getBytes());
outputStream.flush();
outputStream.close();
socket.close();
}
}
public static void main(String[] args) {
try {
new Bootstrap().start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
通過這個(gè)Socket返回hello world...給客戶端
我們?yōu)g覽器輸入127
可以看到后臺(tái)代碼輸出信息
前臺(tái)瀏覽器顯示信息
返回信息瀏覽器不認(rèn),響應(yīng)無效,出現(xiàn)這種情況是瀏覽器只認(rèn)識(shí)http報(bào)文,故此需要包裝一個(gè)返回瀏覽器,然后瀏覽器才能解析
新建一個(gè)http包裝類HttpUtil包裝響應(yīng)信息給瀏覽器
這里我們只返回200和404狀態(tài)的
package com.udeam.util;
/**
* 封裝http響應(yīng)
*/
public class HttpUtil {
/**
* 404 page
*/
private static final String content = "<H2>404 page... </H2>";
/**
* 添加響應(yīng)頭信息
* <p>
* http響應(yīng)體格式
* <p>
* 響應(yīng)頭(多參數(shù)空格換行)
* 換行
* 響應(yīng)體
*/
public static String addHeadParam(int len) {
String head = "HTTP/1.1 200 OK \n";
head += "Content-Type: text/html; charset=UTF-8 \n";
head += "Content-Length: " + len + " \n" + "\r\n";
return head;
}
/**
* 4040響應(yīng)
*
* @return
*/
public static String resp_404() {
String head = "HTTP/1.1 404 not found \n";
head += "Content-Type: text/html; charset=UTF-8 \n";
head += "Content-Length: " + content.length() + " \n" + "\r\n";
return head + content;
}
/**
* 200響應(yīng)
*
* @param content 響應(yīng)內(nèi)容
* @return
*/
public static String resp_200(String content) {
return addHeadParam(content.length()) + content;
}
}
然后再請(qǐng)求,可以看到成功返回信息
然后我們?cè)偃フ?qǐng)求一個(gè)靜態(tài)頁(yè)面index.html
新建一個(gè)html頁(yè)面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
hello tomcat....
</body>
</html>
這次前臺(tái)請(qǐng)求Url是http://localhost:8080/index.html
還是從Socket進(jìn)行監(jiān)聽8080端口
請(qǐng)求靜態(tài)的html,那如何去在后臺(tái)找到這個(gè)資源呢?
通過url也就是/index.html去找到這個(gè)文件,后臺(tái)文件我們?nèi)シ诺?code>resource下
那如何獲取url呢?
瀏覽器在請(qǐng)求后臺(tái)的時(shí)候發(fā)送的也是http請(qǐng)求,我們可以獲取http請(qǐng)求報(bào)文
可以看一下,請(qǐng)求報(bào)文
從請(qǐng)求頭中獲取到url以及method等
獲取輸入流
Socket accept = socket.accept();
//獲取輸入流
InputStream inputStream = accept.getInputStream();
然后對(duì)輸入流進(jìn)行解析,通過解析http請(qǐng)求頭第一行得到url和method封裝到Request對(duì)象中
/**
* 封裝的請(qǐng)求實(shí)體類
*/
public class Request {
/**
* 請(qǐng)求方式
*/
private String method;
/**
* 請(qǐng)求url
*/
private String url;
/**
* 輸入流
*/
public InputStream inputStream;
public Request() {
}
//構(gòu)造器輸入流
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
//讀取請(qǐng)求信息,封裝屬性
int count = 0;
//讀取請(qǐng)求信息
while (count == 0) {
count = inputStream.available();
}
byte[] b = new byte[count];
inputStream.read(b);
String reqStr = new String(b);
System.out.println("請(qǐng)求信息 : " + reqStr);
//根據(jù)http請(qǐng)求報(bào)文 換行符截取
String[] split = reqStr.split("\\n");
//獲取第一行請(qǐng)求頭信息
String s = split[0];
//根據(jù)空格進(jìn)行截取請(qǐng)求方式和url
String[] s1 = s.split(" ");
System.out.println("method : " + s1[0]);
System.out.println("url : " + s1[1]);
this.method = s1[0];
this.url = s1[1];
}
//.... get set省略
}
然后根據(jù)請(qǐng)求的url從磁盤找到靜態(tài)資源讀取到然后以流的形式返回給瀏覽器
這里封裝返回對(duì)象Response
public class Response {
/**
* 響應(yīng)流
*/
private OutputStream outputStream;
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
//輸出指定字符串
public void outPutStr(String content) throws IOException {
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
}
}
根據(jù)url獲取靜態(tài)資源
public void outPutHtml(String url) throws IOException {
//排除瀏覽器的/favicon.ico請(qǐng)求
if (("/favicon.ico").equals(url)){
return;
}
//獲取靜態(tài)資源的絕對(duì)路徑
String abPath = ResourUtil.getStaticPath(url);
//查詢靜態(tài)資源是否存在
File file = new File(abPath);
if (file.exists()) {
//輸出靜態(tài)資源
ResourUtil.readFile(new FileInputStream(abPath), outputStream);
} else {
//404
try {
outPutStr(HttpUtil.resp_404());
} catch (IOException e) {
e.printStackTrace();
}
}
}
ResourUtil 工具類
封裝解析讀取靜態(tài)資源
/**
* 靜態(tài)資源工具類
*/
public class ResourUtil {
/**
* 獲取classes文件目錄
*/
private static URL url = ResourUtil.class.getClassLoader().getResource("\\\\");
/**
* 獲取靜態(tài)資源文件路徑
*
* @param path
* @return
*/
public static String getStaticPath(String path) throws UnsupportedEncodingException {
//獲取目錄的絕對(duì)路徑
try {
String decode = URLDecoder.decode(url.getPath(), "UTF-8");
String replace1 = decode.replace("\\", "/");
String replace2 = replace1.replace("http://", "");
replace2 = replace2.substring(0,replace2.lastIndexOf("/")) + path;
return replace2;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 讀取靜態(tài)資源文件輸入流
*
* @param inputStream
*/
public static void readFile(InputStream inputStream, OutputStream outputStream) throws IOException {
int count = 0;
//讀取請(qǐng)求信息
while (count == 0) {
count = inputStream.available();
}
int content = 0;
//讀取文件
content = count;
//輸出頭
outputStream.write(HttpUtil.addHeadParam(content).getBytes());
//輸出內(nèi)容
long written = 0;
int byteSize = 1024;
byte[] b = new byte[byteSize];
//讀取
while (written < content) {
if (written + 1024 > content) {
byteSize = (int) (content - written);
b = new byte[byteSize];
}
inputStream.read(b);
outputStream.write(b);
outputStream.flush();
written += byteSize;
}
}
}
socket中完整請(qǐng)求代碼
public void start() throws IOException {
//返回固定字符串到客戶端
ServerSocket socket = new ServerSocket(port);
System.out.println("--------- start port : " + port);
while (true) {
Socket accept = socket.accept();
//獲取輸入流
InputStream inputStream = accept.getInputStream();
//封裝請(qǐng)求和響應(yīng)對(duì)象
Request request = new Request(inputStream);
Response response = new Response(accept.getOutputStream());
response.outPutHtml(request.getUrl());
}
}
瀏覽器測(cè)試,可以看到正確返回
接下來實(shí)現(xiàn)定義請(qǐng)求動(dòng)態(tài)資源,具體實(shí)現(xiàn)在java web中處理一個(gè)請(qǐng)求是使用servlet請(qǐng)求
tomcat處理servlet請(qǐng)求需要實(shí)現(xiàn)servlet規(guī)范
什么是servlet規(guī)范呢?
簡(jiǎn)單來說就是http請(qǐng)求在接收到請(qǐng)求之后將請(qǐng)求交給Servlet容器來處理,Servlet容器通過Servlet接口來調(diào)用不同的業(yè)務(wù)類,這一整套稱作Servlet規(guī)范;
接口規(guī)范
/**
* 自定義servlet規(guī)范
*/
public interface Servlet {
void init() throws Exception;
void destory() throws Exception;
void service(Request request, Response response) throws Exception;
}
實(shí)現(xiàn)
/**
* 實(shí)現(xiàn)servlet規(guī)范
*/
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 service(Request request, Response response) throws Exception {
if ("GET".equalsIgnoreCase( request.getMethod()
)) {
doGet(request, response);
} else {
doPost(request, response);
}
}
}
業(yè)務(wù)請(qǐng)求servlet
/**
* 業(yè)務(wù)類servelt
*/
public class MyServlet extends HttpServlet {
@Override
public void init() throws Exception {
}
@Override
public void doGet(Request request, Response response) {
//動(dòng)態(tài)業(yè)務(wù)請(qǐng)求
String content = "<h2> GET 業(yè)務(wù)請(qǐng)求</h2>";
try {
response.outPutStr(HttpUtil.resp_200(content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request request, Response response) {
//動(dòng)態(tài)業(yè)務(wù)請(qǐng)求
String content = "<h2> Post 業(yè)務(wù)請(qǐng)求</h2>";
try {
response.outPutStr(HttpUtil.resp_200(content));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void destory() throws Exception {
}
}
定義完之后,如何請(qǐng)求呢,如何根據(jù)請(qǐng)求Ur去得到相應(yīng)的servlet
在Java web中我們是在web.xml中進(jìn)行配置,同樣新建web.xml,配置servlet
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<!-- v3版本 單線程 多個(gè)請(qǐng)求會(huì)阻塞 -->
<!-- <servlet>
<servlet-name>test</servlet-name>
<servlet-class>com.udeam.v3.service.MyServlet</servlet-class>
</servlet>-->
<!-- v4版本 多線程 不阻塞-->
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>com.udeam.v4.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
</web-app>
解析web.xml文件
講url和每一個(gè)servlet對(duì)應(yīng)起來存儲(chǔ)到map中
/**
* 加載解析web.xml,初始化Servlet
*/
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();
List<Element> selectNodes = rootElement.selectNodes("http://servlet");
for (int i = 0; i < selectNodes.size(); i++) {
Element element = selectNodes.get(i);
// <servlet-name>test</servlet-name>
Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
String servletName = servletnameElement.getStringValue();
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 + "']");
// /test
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();
}
}
啟動(dòng)方法
根據(jù)url找到servlet去執(zhí)行service方法
private static final Map<String, HttpServlet> servletMap = new HashMap<>();
public void start() throws IOException {
ServerSocket socket = new ServerSocket(port);
System.out.println("--------- start port : " + port);
while (true) {
Socket accept = socket.accept();
//獲取輸入流
InputStream inputStream = accept.getInputStream();
//封裝請(qǐng)求和響應(yīng)對(duì)象
Request request = new Request(inputStream);
Response response = new Response(accept.getOutputStream());
//靜態(tài)資源
if (request.getUrl().contains(".html")) {
response.outPutHtml(request.getUrl());
} else {
if (!servletMap.containsKey(request.getUrl())) {
response.outPutStr(HttpUtil.resp_200(request.getUrl() + " is not found ... "));
} else {
HttpServlet httpServlet = servletMap.get(request.getUrl());
try {
//處理請(qǐng)求
httpServlet.service(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
然后請(qǐng)求http://localhost:8080/test可以看到正確返回
這里在deget方法中增加睡眠停頓模擬業(yè)務(wù)請(qǐng)求時(shí)間
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
請(qǐng)求可以可以看到請(qǐng)求阻塞,這是因?yàn)橥粋€(gè)socket 當(dāng)前test這個(gè)沒有請(qǐng)求結(jié)束,第二個(gè)請(qǐng)求進(jìn)來然后阻塞;
必須等到第一個(gè)請(qǐng)求結(jié)束后才能處理請(qǐng)求
然后再請(qǐng)求index.html
發(fā)現(xiàn),并不是靜態(tài)資源并不是立即返回,需要等到test請(qǐng)求結(jié)束后才能返回
故此需要對(duì)這個(gè)進(jìn)行改造,讓彼此請(qǐng)求互不干擾
我們可以使用多線程來進(jìn)行解決,線程互不干擾,每個(gè)請(qǐng)求去執(zhí)行
在Socket中添加方法
//1 單線程處理
MyThread myThread = new MyThread(httpServlet, response, request);
new Thread(myThread).start();
線程是寶貴的資源,頻繁創(chuàng)建和銷毀線程對(duì)開銷很大,故此使用線程池來解決
/**
* 參數(shù)可以配置在xml里面
*/
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.HOURS, new ArrayBlockingQueue<>(500));
//2 線程池執(zhí)行
threadPoolExecutor.submit(myThread);
threadPoolExecutor.shutdown();
這樣子就可以立即返回響應(yīng),互不干擾;
簡(jiǎn)易版的tomcat實(shí)現(xiàn)就可以實(shí)現(xiàn)了,代碼的話沒有像tomcat那樣子可以將war包解析之類的..
而且代碼耦合性也大,tomcat和業(yè)務(wù)代碼在一個(gè)Maven中...
說明
分別在指定包下如v1,v2,v3,v4每個(gè)代表一個(gè)版本
- v1 簡(jiǎn)單的返回指定字符串
- v2 返回靜態(tài)頁(yè)面
- v3 單線程處理servelt請(qǐng)求(多個(gè)請(qǐng)求會(huì)阻塞)
- v4 多線程處理
其中需要用到解析xml依賴
<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>