手寫一個(gè)簡易版的 Tomcat!
前言
使用tomcat的時(shí)候當(dāng)瀏覽器輸入url之后,開始發(fā)送http請求,這個(gè)請求發(fā)送到哪兒呢,Url解析的過程中
1 先通過域名解析請求得到ip 2 然后通過ip找到對應(yīng)的主機(jī) 3 再通過響應(yīng)的端口找到進(jìn)程 4 然后再去根據(jù)程序去處理這個(gè)請求,再到原路返回
思考
對于1,2步驟我們本地測試可以不用去扣這個(gè),明白這么回事兒就可以,本地localhost實(shí)際上對應(yīng)我們自己本機(jī)127.0.0.1
對于第三部,我們本地可以去通過一個(gè)socket去監(jiān)聽響應(yīng)的端口,去獲取到請求,然后再響應(yīng)給客戶端讓客戶端瀏覽器去解析我們返回的http報(bào)文,從而展示數(shù)據(jù);
具體如下步驟:
1)提供服務(wù),接收請求(可以使用Socket通信)
2)請求信息封裝成Request(Response)
3)客戶端請求資源,資源分為靜態(tài)資源(html)和動態(tài)資源(Servlet)
4)資源返回給客戶端瀏覽器
具體實(shí)現(xiàn)
首先新建maven工程

然后定義一個(gè)啟動類Bootstrap然后實(shí)現(xiàn)一個(gè)啟動方法start,在這個(gè)方法中啟動一個(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;
/**
?*?啟動類入庫
?*?用于啟動tomcat
?*/
public?class?Bootstrap?{
????/**
?????*?監(jiān)聽端口號
?????*?用于啟動socket監(jiān)聽的端口號
?????*/
????private?int?port?=?8080;
????/**
?????*?啟動方法
?????*/
????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
可以看到后臺代碼輸出信息

前臺瀏覽器顯示信息

返回信息瀏覽器不認(rèn),響應(yīng)無效,出現(xiàn)這種情況是瀏覽器只認(rèn)識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?=?"404?page...?
";
????/**
?????*?添加響應(yīng)頭信息
?????*?
?????*?http響應(yīng)體格式
?????*?
?????*?響應(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;
????}
}
然后再請求,可以看到成功返回信息

然后我們再去請求一個(gè)靜態(tài)頁面index.html
新建一個(gè)html頁面
html>
<html?lang="en">
<head>
????<meta?charset="UTF-8">
????<title>Titletitle>
head>
<body>
hello?tomcat....
body>
html>
這次前臺請求Url是http://localhost:8080/index.html
還是從Socket進(jìn)行監(jiān)聽8080端口
請求靜態(tài)的html,那如何去在后臺找到這個(gè)資源呢?
通過url也就是/index.html去找到這個(gè)文件,后臺文件我們?nèi)シ诺?code style="margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">resource下
那如何獲取url呢?
瀏覽器在請求后臺的時(shí)候發(fā)送的也是http請求,我們可以獲取http請求報(bào)文
可以看一下,請求報(bào)文

從請求頭中獲取到url以及method等
獲取輸入流
Socket?accept?=?socket.accept();
//獲取輸入流
InputStream?inputStream?=?accept.getInputStream();
然后對輸入流進(jìn)行解析,通過解析http請求頭第一行得到url和method封裝到Request對象中
/**
?*?封裝的請求實(shí)體類
?*/
public?class?Request?{
????/**
?????*?請求方式
?????*/
????private?String?method;
????/**
?????*?請求url
?????*/
????private?String?url;
????/**
?????*?輸入流
?????*/
????public?InputStream?inputStream;
????public?Request()?{
????}
????//構(gòu)造器輸入流
????public?Request(InputStream?inputStream)?throws?IOException?{
????????this.inputStream?=?inputStream;
????????//讀取請求信息,封裝屬性
????????int?count?=?0;
????????//讀取請求信息
????????while?(count?==?0)?{
????????????count?=?inputStream.available();
????????}
????????byte[]?b?=?new?byte[count];
????????inputStream.read(b);
????????String?reqStr?=?new?String(b);
????????System.out.println("請求信息?:?"?+?reqStr);
????????//根據(jù)http請求報(bào)文?換行符截取
????????String[]?split?=?reqStr.split("\\n");
????????//獲取第一行請求頭信息
????????String?s?=?split[0];
????????//根據(jù)空格進(jìn)行截取請求方式和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ù)請求的url從磁盤找到靜態(tài)資源讀取到然后以流的形式返回給瀏覽器
這里封裝返回對象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請求
????if?(("/favicon.ico").equals(url)){
????????return;
????}
????//獲取靜態(tài)資源的絕對路徑
????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?{
????????//獲取目錄的絕對路徑
????????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;
????????//讀取請求信息
????????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?????????????if?(written?+?1024?>?content)?{
????????????????byteSize?=?(int)?(content?-?written);
????????????????b?=?new?byte[byteSize];
????????????}
????????????inputStream.read(b);
????????????outputStream.write(b);
????????????outputStream.flush();
????????????written?+=?byteSize;
????????}
????}
}
socket中完整請求代碼
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();
????????//封裝請求和響應(yīng)對象
????????Request?request?=?new?Request(inputStream);
????????Response?response?=?new?Response(accept.getOutputStream());
????????response.outPutHtml(request.getUrl());
????}
}
瀏覽器測試,可以看到正確返回

接下來實(shí)現(xiàn)定義請求動態(tài)資源,具體實(shí)現(xiàn)在java web中處理一個(gè)請求是使用servlet請求
tomcat處理servlet請求需要實(shí)現(xiàn)servlet規(guī)范
什么是servlet規(guī)范呢?
簡單來說就是http請求在接收到請求之后將請求交給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ù)請求servlet
/**
?*?業(yè)務(wù)類servelt
?*/
public?class?MyServlet?extends?HttpServlet?{
????@Override
????public?void?init()?throws?Exception?{
????}
????@Override
????public?void?doGet(Request?request,?Response?response)?{
????????//動態(tài)業(yè)務(wù)請求
????????String?content?=?"?GET?業(yè)務(wù)請求
";
????????try?{
????????????response.outPutStr(HttpUtil.resp_200(content));
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}
????}
????@Override
????public?void?doPost(Request?request,?Response?response)?{
????????//動態(tài)業(yè)務(wù)請求
????????String?content?=?"?Post?業(yè)務(wù)請求
";
????????try?{
????????????response.outPutStr(HttpUtil.resp_200(content));
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}
????}
????@Override
????public?void?destory()?throws?Exception?{
????}
}
定義完之后,如何請求呢,如何根據(jù)請求Ur去得到相應(yīng)的servlet
在Java web中我們是在web.xml中進(jìn)行配置,同樣新建web.xml,配置servlet
<web-app>
????
????
????
????<servlet>
????????<servlet-name>testservlet-name>
????????<servlet-class>com.udeam.v4.MyServletservlet-class>
????servlet>
????<servlet-mapping>
????????<servlet-name>testservlet-name>
????????<url-pattern>/testurl-pattern>
????servlet-mapping>
web-app>
解析web.xml文件
講url和每一個(gè)servlet對應(yīng)起來存儲到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?selectNodes?=?rootElement.selectNodes("http://servlet");
????????for?(int?i?=?0;?i?????????????Element?element?=??selectNodes.get(i);
????????????//?test
????????????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();
????}
}
啟動方法
根據(jù)url找到servlet去執(zhí)行service方法
private?static?final?Map?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();
????????//封裝請求和響應(yīng)對象
????????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?{
????????????????????//處理請求
????????????????????httpServlet.service(request,?response);
????????????????}?catch?(Exception?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????}
????}
}
然后請求http://localhost:8080/test可以看到正確返回

這里在deget方法中增加睡眠停頓模擬業(yè)務(wù)請求時(shí)間
try?{
????Thread.sleep(10_000);
}?catch?(InterruptedException?e)?{
????e.printStackTrace();
}
請求可以可以看到請求阻塞,這是因?yàn)橥粋€(gè)socket 當(dāng)前test這個(gè)沒有請求結(jié)束,第二個(gè)請求進(jìn)來然后阻塞。
必須等到第一個(gè)請求結(jié)束后才能處理請求。

然后再請求index.html

發(fā)現(xiàn),并不是靜態(tài)資源并不是立即返回,需要等到test請求結(jié)束后才能返回
故此需要對這個(gè)進(jìn)行改造,讓彼此請求互不干擾
我們可以使用多線程來進(jìn)行解決,線程互不干擾,每個(gè)請求去執(zhí)行
在Socket中添加方法
//1?單線程處理
MyThread?myThread?=?new?MyThread(httpServlet,?response,?request);
new?Thread(myThread).start();
線程是寶貴的資源,頻繁創(chuàng)建和銷毀線程對開銷很大,故此使用線程池來解決
/**
*?參數(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),互不干擾;
簡易版的tomcat實(shí)現(xiàn)就可以實(shí)現(xiàn)了,代碼的話沒有像tomcat那樣子可以將war包解析之類的..
而且代碼耦合性也大,tomcat和業(yè)務(wù)代碼在一個(gè)Maven中...
說明
分別在指定包下如v1,v2,v3,v4每個(gè)代表一個(gè)版本
v1 簡單的返回指定字符串 v2 返回靜態(tài)頁面 v3 單線程處理servelt請求(多個(gè)請求會阻塞) v4 多線程處理
其中需要用到解析xml依賴
<dependencies>
????<dependency>
????????<groupId>dom4jgroupId>
????????<artifactId>dom4jartifactId>
????????<version>1.6.1version>
????dependency>
????<dependency>
????????<groupId>jaxengroupId>
????????<artifactId>jaxenartifactId>
????????<version>1.1.6version>
????dependency>
dependencies>
至此,一個(gè)簡單的 tomcat 就擼完了,如需本文源碼,請加我微信:xttblog2,免費(fèi)發(fā)送!
