跟我一塊兒動手實現Tomcat(三):解析Request請求參數、請求頭、cookie

前言

最近筆者讀了《深刻剖析tomcat》這本書(原做:《how tomcat works》),發現該書簡單易讀,每一個
章節按部就班的講解了tomcat的原理,在接下來的章節中,tomcat都是基於上一章新增功能並完善,
到最後造成一個簡易版tomcat的完成品。因此有興趣的同窗請按順序閱讀,本文爲記錄第三章的知識點
以及源碼實現(造輪子)。
複製代碼

內容回顧

跟我一塊兒動手實現Tomcat(二):實現簡單的Servlet容器

上一章咱們實現了一個簡單的Servlet容器,可以調用並執行用戶自定義實現Servlet接口的類。html

本章內容

  • 模塊模仿tomcat,實現Connector(鏈接器)、Bootstrap(啓動器)和核心模塊。
  • 可以執行繼承HttpServlet類的自定義Servlet(上一章是實現了Servlet接口)
  • 可以解析用戶請求參數(Parameters)/Cookie/請求頭(Header)

開始以前

  • 簡單介紹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

相關文章
相關標籤/搜索