最近筆者讀了《深刻剖析tomcat》這本書(原做:《how tomcat works》),發現該書簡單易讀,每一個
章節按部就班的講解了tomcat的原理,在接下來的章節中,tomcat都是基於上一章新增功能並完善,
到最後造成一個簡易版tomcat的完成品。因此有興趣的同窗請按順序閱讀,本文爲記錄第三章的知識點
以及源碼實現(造輪子)。
複製代碼
跟我一塊兒動手實現Tomcat(二):實現簡單的Servlet容器
上一章咱們實現了一個簡單的Servlet容器,可以調用並執行用戶自定義實現Servlet接口的類。html
簡單介紹Connector(鏈接器)java
對Tomcat比較熟悉的朋友對這個詞應該不陌生,後面的篇幅會繼續比較詳細介紹,在這裏不熟悉的朋友能夠理解爲:鏈接器只是負責接收請求,而後將請求丟給Container(容器)去執行相應的請求。git
javax.servlet.http.HttpServlet類github
上一章咱們自定義的Servlet是實現了Servlet接口,實例化Servlet的時候咱們是將解析的Request/Response(分別實現了ServletRequest/ServletResponse接口)傳入對應的service()方法中完成執行。
那咱們來回顧一下剛學Servlet開發的時候,大部分教程都按順序講解實現Servlet接口、繼承GenericServlet類、繼承HttpServlet類這三種方式去寫本身的Servlet,那麼後面推薦的仍然是最後一種,重寫其中的doGet()/doPost()方法便可,那麼咱們來看看上一章咱們的tomcat能不能支持繼承HttpServlet類的Servlet呢:web
//HttpServlet源代碼片斷
public abstract class HttpServlet extends GenericServlet {
...
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException{
HttpServletRequest request;
HttpServletResponse response;
//若是傳進來的request/response對象不是Http類型的則拋異常
if (!(req instanceof HttpServletRequest &&
res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
service(request, response);
}
...
}
複製代碼
如上所示源碼,再來看看咱們上一章的ServletProcess調用Servlet源碼:數組
servlet.service(new RequestFacade(request), new ResponseFacade(response));
複製代碼
很明顯上一章的request/response在HttpServlet時會拋出異常,因此本章咱們會將Request/Response以及它們的外觀類都實現HttpServletRequest/HttpServletResponse接口。瀏覽器
在代碼實現前咱們看看總體模塊以及流程執行圖(看不清能夠點擊放大):tomcat
1. Bootstrap模塊服務器
啓動模塊目前咱們沒有多大工做,只是啓動HttpConnector:cookie
public final class Bootstrap {
public static void main(String[] args){
new HttpConnector().start();
}
}
複製代碼
2. HttpConnector模塊(鏈接器)
鏈接器模塊和下面的核心模塊的前身其實就是上一章的HttpServer類,咱們把它按功能拆分紅了
等待和創建鏈接(HttpConnector)/處理鏈接(HttpProcess)2個模塊。
複製代碼
鏈接器功能是等待請求並將請求丟給相應執行器去執行:
public class HttpConnector implements Runnable {
public void start(){
new Thread(this).start();
}
@Override
public void run() {
ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
while (true) {
Socket accept = serverSocket.accept();
HttpProcess process = new HttpProcess(this);
process.process(accept);
}
serverSocket.close();
}
}
複製代碼
3. 核心模塊(執行器)
上面也有說到,執行器也是上一章HttpServer類的前身,只不過這章咱們修改瞭解析請求信息的方式。
複製代碼
public class HttpProcess {
private HttpRequest request;
private HttpResponse response;
public void process(Socket socket) {
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
//初始化request以及response
request = new HttpRequest(input);
response = new HttpResponse(output, request);
//解析request請求行和請求頭
this.parseRequestLine(input);
this.parseHeaders(input);
//調用對應的處理器處理
if (request.getRequestURI().startsWith(SERVLET_URI_START_WITH)) {
new ServletProcess().process(request, response);
} else {
new StaticResourceProcess().process(request, response);
}
}
}
複製代碼
看了上面的實現可能不少人對有些對象有點陌生,下面一一介紹:
1. HttpRequest/HttpResponse變量就是上一章的Request/Response對象,由於實現了
HttpServletReuqest/HttpServletResponse也就順便改了個名,將會在下面介紹;
2. 每個請求都對應了一個HttpProcess對象,因此這裏request/response是成員變量;
3. 解析請求行和解析請求頭的方法也在下面介紹。
複製代碼
parseRequestLine、parseHeaders方法
讓咱們先看看一個原始的HTTP請求字符串,看看如何去解析請求行和請求頭:
GET /index.html?utm_source=aaa HTTP/1.1\r\n
Host: www.baidu.com\r\n
Connection: keep-alive\r\n
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\r\n
Accept: text/html\r\n
Accept-Encoding: gzip, deflate, sdch, br\r\n
Accept-Language: zh-CN,zh;q=0.8\r\n
Cookie: BAIDUID=462A9AC35EE6158AA7DFCD27AF:FG=1; BIDUPSID=462A9AC35EE6158AA7DF027AF; PSTM=1506310304; BD_CK_SAM=1; PSINO=7; BD_HOME=1; H_PS_PSSID=1459_24885_21115_25436; BD_UPN=12314353; sug=3; sugstore=0; ORIGIN=2; bdime=0\r\n
複製代碼
你們能夠發現,其實咱們使用socket讀取HTTP請求時候,發現每一行都會有'\r\n'這個回車換行符,只不過在咱們瀏覽器按F12時被瀏覽器自動解析成了換行而已,咱們分析上面的這個請求信息得出如下規律:
- 每一行結尾字符都是\r\n
- 請求行(第一行)的HTTP請求方法、URI、請求協議中間都有個空格
- 第二行開始(請求頭)key和value的內容都是以':'和一個' '字符隔開
- Cookie的鍵值對是以'='分割,以';'和' '區分先後鍵值對
複製代碼
接下來咱們分別去解析如下ISO-8859-1編碼狀況下上面字符對應的值,並創建一個常量類:
public class HttpConstant {
/* 回車 \r */
public static final byte CARRIAGE_RETURN = 13;
/* 換行 \n */
public static final byte LINE_FEED = 10;
/* 空格 */
public static final byte SPACE = 32;
/* 冒號 : */
public static final byte COLON = 58;
}
複製代碼
1.parseRequestLine方法
根據上面的思路,咱們就能夠輕鬆地解析請求行的數據:
StringBuilder temp = new StringBuilder();
int cache;
while ((cache = requestStream.read()) != -1) {
//讀取到第一個\r\n時則說明讀取請求行完畢
if (HttpConstant.CARRIAGE_RETURN == cache && HttpConstant.LINE_FEED == requestStream.read()) {
break;
}
temp.append((char)cache);
}
String[] requestLineArray = temp.toString().split(" ");
複製代碼
最後分割空格使用數組裝着請求行(若是你有更好的方案也能夠在評論區說一說哈)
接下來判斷URI有沒有使用"?"傳遞參數,若是有就截取並丟到HttpRequest的QueryString變量中,
最後截取URI便可。
String uri = requestLineArray[1];
int question = uri.indexOf("?");
if (question >= 0) {
request.setQueryString(uri.substring(question+1,uri.length()));
uri = uri.substring(0,question);
}
複製代碼
判斷是否是從?傳遞jsessionid過來,若是是就賦值到request對象中
複製代碼
String match = ";jsessionid=";
int semicolon = uri.indexOf(match);
if (semicolon >= 0) {
String rest = uri.substring(semicolon + match.length());
int semicolon2 = rest.indexOf(';');
if (semicolon2 >= 0) {
request.setRequestedSessionId(rest.substring(0, semicolon2));
rest = rest.substring(semicolon2);
} else {
request.setRequestedSessionId(rest);
rest = "";
}
request.setRequestedSessionURL(true);
uri = uri.substring(0, semicolon) + rest;
} else {
request.setRequestedSessionId(null);
request.setRequestedSessionURL(false);
}
複製代碼
這裏調用了一個校驗URI合法性的方法,若是URI不合法(例如包含'.//'之類跳轉目錄的危險字符)
則拋異常,不然就將上面解析到的內容丟到request中去。
String normalizedUri = this.normalize(uri);
if (normalizedUri == null) {
throw new ServletException("Invalid URI: " + uri + "'");
}
request.setRequestURI(normalizedUri);
request.setMethod(requestLineArray[0]);
request.setProtocol(requestLineArray[2]);
複製代碼
就這樣,請求行的信息就被咱們讀取完畢,那咱們再來看看讀取請求頭的代碼:
parseHeaders方法
這裏有個坑:Socket的read()方法讀取完畢時最後一個字節不是-1,而是阻塞等待Socket客戶端發送-1過來結束讀取,可是咱們的Socket客戶端是瀏覽器,瀏覽器不會發送-1以表示結束髮送,因此咱們結合InputStream#available()方法(返回實際還能夠讀取的字節數)來判斷是否讀取完畢便可。
public void parseHeader() {
StringBuilder sb = new StringBuilder();
int cache;
while (input.available() > 0 && (cache = input.read()) > -1) {
sb.append((char)cache);
}
....看下文
}
複製代碼
讀取完畢效果如圖:
若是是POST請求,那麼表單參數會在空行後面:
也頗有規律,請求頭都用\r\n隔開,而且若是是POST請求提交表單,那麼表單參數會在一個空行後面(兩個\r\n)
//使用\r\n分割請求頭
Queue<String> headers = Stream.of(sb.toString().split("\r\n")).collect(toCollection(LinkedList::new));
while (!headers.isEmpty()) {
//獲取一個請求頭
String headerString = headers.poll();
//讀取到空行則說明請求頭已讀取完畢
if (StringUtil.isBlank(headerString)) {
break;
}
//分割請求頭的key和value
String[] headerKeyValue = headerString.split(": ");
request.addHeader(headerKeyValue[0], headerKeyValue[1]);
}
//若是在讀取到空行後還有數據,說明是POST請求的表單參數
if(!headers.isEmpty()){
request.setPostParams(headers.poll());
}
複製代碼
大體流程:
最後咱們對一些特殊的請求頭信息設置到Request對象中(cookie、content-type、content-length);
String contentLength = request.getHeader("content-length");
if(contentLength!=null){
request.setContentLength(Integer.parseInt(contentLength));
}
request.setContentType(request.getHeader("content-type"));
Cookie[] cookies = parseCookieHeader( request.getHeader("cookie"));
Stream.of(cookies).forEach(cookie -> request.addCookie(cookie));
//若是sessionid不是從cookie中獲取的,則優先使用cookie中的sessionid
if (!request.isRequestedSessionIdFromCookie()) {
Stream.of(cookies)
.filter(cookie -> "jsessionid".equals(cookie.getName()))
.findFirst().
ifPresent(cookie -> {
//設置cookie的值
request.setRequestedSessionId(cookie.getValue());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
});
}
複製代碼
讀取cookie的方法也很簡單:
private Cookie[] parseCookieHeader(String cookieListString) {
return Stream.of(cookieListString.split("; "))
.map(cookieStr -> {
String[] cookieArray = cookieStr.split("=");
return new Cookie(cookieArray[0], cookieArray[1]);
}).toArray(Cookie[]::new);
}
複製代碼
不熟悉JDK8語法的小夥伴們可能看不太懂幹了什麼,不要緊來張圖解釋一下上面那段代碼內容:
到這裏,HttpProcess處理請求的邏輯就搞定啦,(是否是以爲代碼有點多),細心的客官們必定發現了,request怎麼能夠設置那麼多屬性呢?上一章的request好像沒有那麼多功能吧?是的,咱們這一章也對request/response作了手腳,請看下文分析:
HttpRequest(上一章的Request對象)
沒錯,在文章的開頭咱們已經說了要把Request升級一下,那麼怎麼升級呢?也就是實現HttpServletRequest接口啦:
public class HttpRequest implements HttpServletRequest {
private String contentType;
private int contentLength;
private InputStream input;
private String method;
private String protocol;
private String queryString;
private String postParams;
private String requestURI;
private boolean requestedSessionCookie;
private String requestedSessionId;
private boolean requestedSessionURL;
protected ArrayList<Cookie> cookies = new ArrayList<>();
protected HashMap<String, ArrayList<String>> headers = new HashMap<>();
protected ParameterMap parameters;
...
}
複製代碼
哈哈沒有看錯,多了一堆參數,可是細心的客官們應該能夠看到,這些參數都是很是眼熟,並且上面已經對大部分參數設值過了,眼生的可能就是下面的那個ParameterMap,那麼等下咱們慢慢分析:(那些get、set方法就不分析了)
請求頭(header)操做:
public void addHeader(String name, String value) {
name = name.toLowerCase();
//若是key對應的value不存在則new一個ArrayList
ArrayList<String> values = headers.computeIfAbsent(name, k -> new ArrayList<>());
values.add(value);
}
public ArrayList getHeaders(String name) {
name = name.toLowerCase();
return headers.get(name);
}
public String getHeader(String name) {
name = name.toLowerCase();
ArrayList<String> values = headers.get(name);
if (values != null) {
return values.get(0);
} else {
return null;
}
}
public ArrayList getHeaderNames() {
return new ArrayList(headers.keySet());
}
複製代碼
你們能夠看到請求頭是是個Map,key是請求頭的名字,value則是請求頭的內容數組(一個請求頭能夠有多個內容),因此也就是對這個Map作操做而已~
Cookie操做:
public Cookie[] getCookies() {
return cookies.toArray(new Cookie[cookies.size()]);
}
public void addCookie(Cookie cookie) {
cookies.add(cookie);
}
複製代碼
好像也沒什麼好說的,對List\作常規操做。
Parameters操做:
這是咱們最經常使用的一個操做啦,那麼ParameterMap是個什麼東西呢,咱們先來看看:
public final class ParameterMap extends HashMap<String,String[]> {
private boolean locked = false;
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
public String[] put(String key, String[] value) {
if (locked) {
throw new IllegalStateException("error");
}
return (super.put(key, value));
}
...
}
複製代碼
好吧其實它就是在HashMap基礎上加了一個locked對象(若是已經解析參數完畢了則將這個對象設置爲true禁止更改),key是參數名,value是參數值數組(可有多個)
例如:127.0.0.1:8080/servlet/QueryServlet?name=geoffrey&name=yip
那麼咱們來看看對parameter這個map的操做有:
public String getParameter(String name) {
parseParameters();
String[] values = parameters.get(name);
return Optional.ofNullable(values).map(arr -> arr[0]).orElse(null);
}
public Map getParameterMap() {
parseParameters();
return this.parameters;
}
public ArrayList<String> getParameterNames() {
parseParameters();
return new ArrayList<>(parameters.keySet());
}
public String[] getParameterValues(String name) {
parseParameters();
return parameters.get(name);
}
複製代碼
代碼都很簡單,可是這個parseParameters()是什麼呢,對,它是去解析請求的參數了(懶加載),由於咱們不知道用戶使用Servlet會不會用到請求參數這個功能,並且解析它的開銷比解析其餘數據大,因此咱們會在用戶真正使用參數的時候纔會去解析,提升總體的響應速度,大概的代碼以下:
protected void parseParameters() {
if (parsed) {
//已經解析過則中止解析
return;
}
ParameterMap results = parameters;
if (results == null) {
results = new ParameterMap();
}
results.setLocked(false);
String encoding = getCharacterEncoding();
if (encoding == null) {
encoding = StringUtil.ISO_8859_1;
}
// 解析URI攜帶的請求參數
String queryString = getQueryString();
this.parseParameters(results, queryString, encoding);
// 初始化Content-Type的值
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
//解析POST請求的表單參數
if (HTTPMethodEnum.POST.name().equals(getMethod()) && getContentLength() > 0
&& "application/x-www-form-urlencoded".equals(contentType)) {
this.parseParameters(results, getPostParams(), encoding);
}
//解析完畢就鎖定
results.setLocked(true);
parsed = true;
parameters = results;
}
/**
* 解析請求參數
* @param map Request對象中的參數map
* @param params 解析前的參數
* @param encoding 編碼
*/
public void parseParameters(ParameterMap map, String params, String encoding) {
String[] paramArray = params.split("&");
Stream.of(paramArray).forEach(param -> {
String[] splitParam = param.split("=");
String name = splitParam[0];
String value = splitParam[1];
//此處是將key和value使用URLDecode解碼並添加進map中
putMapEntry(map, urlDecode(name, encoding), urlDecode(value, encoding));
});
}
複製代碼
大概內容就是根據以前HttpProcess解析請求行的queryString參數以及若是是POST請求的表單數據放入ParameterMap中,而且鎖定Map。
HttpResponse(上一章的Response對象)
HttpResponse對象也跟隨者實現了HttpServletResponse接口,可是本章沒有實現具體的內容,因此此處略過。
public class HttpResponse implements HttpServletResponse {
...
}
複製代碼
ServletProcess
ServletProcess具體只須要將request和response的外觀類跟着升級實現對應的接口便可:
public void process(HttpRequest request, HttpResponse response) throws IOException {
...
servlet.service(new HttpRequestFacade(request), new HttpResponseFacade(response));
...
}
public class HttpRequestFacade implements HttpServletRequest {
private HttpRequest request;
...
}
public class HttpResponseFacade implements HttpServletResponse {
private HttpResponse response;
...
}
複製代碼
咱們先編寫一個Servlet:
/**
* 測試註冊Servlet
*/
public class RegisterServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) {
//打印表單參數
String name = req.getParameter("name");
String password = req.getParameter("password");
if (StringUtil.isBlank(name) || StringUtil.isBlank(password)) {
try {
resp.getWriter().println("帳號/密碼不能爲空!");
} finally {
return;
}
}
//打印請求行
System.out.println("Parse user register method:" + req.getMethod());
//打印Cookie
System.out.println("Parse user register cookies:");
Optional.ofNullable(req.getCookies())
.ifPresent(cookies ->
Stream.of(cookies)
.forEach(cookie ->System.out.println(cookie.getName() + ":" + cookie.getValue()
)));
//打印請求頭
System.out.println("Parse http headers:");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ":" + req.getHeader(headerName));
}
System.out.println("Parse User register name :" + name);
System.out.println("Parse User register password :" + password);
try {
resp.getWriter().println("註冊成功!");
} finally {
return;
}
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) {
this.doGet(req, resp);
}
}
複製代碼
編寫一個HTML:
<html>
<head>
<title>註冊</title>
</head>
<body>
<form method="post" action="/servlet/RegisterServlet">
帳號:<input type="text" name="name"><br>
密碼:<input type="password" name="password"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
複製代碼
打開瀏覽器測試:
控制檯輸出:
到這裏,我們的Tomcat 3.0 web服務器就已經開發完成啦(滑稽臉),已經能夠實現簡單的自定義Servlet調用,以及請求行/請求頭/請求參數/cookie等信息的解析,待完善的地方還有不少:
- HTTPProcess一次性只能處理一個請求,其餘請求只能堵塞,不具有服務器使用性。
- 每一次請求就new一次Servlet,Servlet應該在初始化項目時就應該初始化,是單例的。
- 並未遵循Servlet規範實現相應的生命週期,例如init()/destory()方法咱們均未調用。
- HttpServletRequest/HttpServletResponse接口的大部分方法咱們仍未實現
- 架構/包結構和tomcat相差太多
- 其餘未實現的功能
複製代碼
PS:本章源碼已上傳github: SimpleTomcat