咱們都知道,請求參數能夠存在於請求頭中,也能夠存在於請求體中,瀏覽器中發過來的請求一般是下面這個樣子的java
咱們都知道,請求參數能夠存在於請求頭中,也能夠存在於請求體中,瀏覽器中發過來的請求一般是下面這個樣子的spring
POST http://localhost/examples/servlets/servlet/RequestParamExample?version=1 HTTP/1.1 cache-control: no-cache Postman-Token: 3fdd5c34-9b40-4750-8ea1-c868bc9479a7 Content-Type: application/x-www-form-urlencoded User-Agent: PostmanRuntime/6.3.2 Accept: */* Host: localhost accept-encoding: gzip, deflate content-length: 27 Connection: keep-alive firstname=fds&lastname=fdas
在這個請求中,傳遞了三個參數,version,firstname和lastname,可是參數的地方是不一樣的,version參數在請求的url的後面,用「?」和url分開,而後多個參數用&拼接,這種參數就叫作在請求頭中。apache
而firstname和lastname這一行,和上面的用了一個空行進行了分割開,這段內容是存在方法體中 。json
如今以http請求爲例,分析一下針對於存在於方法頭中的參數,tomcat是怎麼解析的。首先tomcat接收一個socket請求,這個socket會被封裝成一個SocketWrapper對象,而後會NioEndpoint類的一個內部類SocketProcessore的dorun方法會被調用,這個方法中會由AbstractProtocol根據不一樣的協議,交由不一樣的processor進行處理,針對http,則由AbstractProcessorLight類的process進行處理,這個類是一個輕量級的抽象解析類,提供了upgrade接口,以供協議升級。若是未升級協議,則會調用其子類的service方法,進行處理請求。http請求中,會調用Http11processor的service方法,其接受一個封裝了socket的SocketWrapperBase參數。到此,對socket的請求正式開始。以前的流程能夠用以下時序圖表示。瀏覽器
http11Processor繼承自AbstractProcessor,它的建立實在Abstractprotocolde process方法時,判斷有沒有相應的processor實例,若是沒有的話,則調用createProcess方法進行建立出來的。而createProcessor方法是一個抽象方法,由AbstractHttp1Protocol類實現,建立完成之後會進行緩存,下次使用的時候不用再次建立。AbstractProcess在實例化的時候,會new出來一個Request和Response對象,而後做爲它的一個屬性,這個地方也就是org.apache.coyote.Request的誕生地。做爲承載用戶請求數據的一個重要角色,此時的coyote.Request的Request開始了它的首次登場。緩存
public AbstractProcessor(AbstractEndpoint<?> endpoint) { this(endpoint, new Request(), new Response()); } protected AbstractProcessor(AbstractEndpoint<?> endpoint, Request coyoteRequest, Response coyoteResponse) { this.endpoint = endpoint; asyncStateMachine = new AsyncStateMachine(this); request = coyoteRequest; response = coyoteResponse; response.setHook(this); request.setResponse(response); request.setHook(this); }
經過從http11processor的初始化中,能夠看出對於請求結果和返回結果以前的過濾器的增長,也就是在初始化的時候增長到這個解析器裏面的,而且設置inputBuffer。tomcat
接下來來看真正接收到請求並處理的Http11Procossor的service方法。那麼對於對象頭參數的解析,也就是在這個方法調用中完成的。mvc
public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException { RequestInfo rp = request.getRequestProcessor(); rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); // Setting up the I/O setSocketWrapper(socketWrapper); //初始化inputBuffer和outputBuffer inputBuffer.init(socketWrapper); outputBuffer.init(socketWrapper); // 設置一些標示 keepAlive = true; openSocket = false; readComplete = true; boolean keptAlive = false; SendfileState sendfileState = SendfileState.DONE; //在如下狀況下一直自旋 //狀態正常,鏈接存活,方法同步,未升級協議,sendfileState == SendfileState.DONE 而且endpoint非暫停狀態 while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null && sendfileState == SendfileState.DONE && !endpoint.isPaused()) { // 解析請求頭 ,主要是parseRequestLine方法 try { //按行解析請求行 if (!inputBuffer.parseRequestLine(keptAlive)) { if (inputBuffer.getParsingRequestLinePhase() == -1) { return SocketState.UPGRADING; } else if (handleIncompleteRequestLineRead()) { break; } } ... }
service方法主要是經過http1InputBuffer的parseRequestLine方法解析請求行首部,經過parseHeaders方法解析請求頭,而後封裝request對象,發送給tomcat的容器(servlet),通過容器處理之後,返回給客戶端相應的結果。其中本節關注就是如何解析請求頭的,tomcat在這方面作的仍是挺好玩的,我以前想的就是讀取一行,而後對一行進行字符串分割,而後tomcat中並無這麼讀,而是一個字符一個字符的讀。能夠看一下parseRequestLine這個方法app
parseRequestLine這個方法主要是爲了處理這樣一行數據框架
POST http://localhost/examples/servlets/servlet/RequestParamExample HTTP/1.1
方法比較長,我給剪切一下
boolean parseRequestLine(boolean keptAlive) throws IOException { // check state if (!parsingRequestLine) { return true; } // // Skipping blank lines 跳過空行 // if (parsingRequestLinePhase < 2) { byte chr = 0; do { } while ((chr == Constants.CR) || (chr == Constants.LF)); byteBuffer.position(byteBuffer.position() - 1); parsingRequestLineStart = byteBuffer.position(); parsingRequestLinePhase = 2; //狀態變動 } if (parsingRequestLinePhase == 2) { // // 讀取請求方法名稱,這裏面讀取出來是POST // Method name is a token // boolean space = false; while (!space) { } request.method().setBytes(byteBuffer.array(), parsingRequestLineStart, pos - parsingRequestLineStart); //把解析出來的方法,設置爲request的method parsingRequestLinePhase = 3; //狀態變動 } if (parsingRequestLinePhase == 3) { //讀取空白字符,就是請求方法和url中間的空白字符 parsingRequestLinePhase = 4; //狀態變動 } if (parsingRequestLinePhase == 4) { // 讀取URI boolean space = false; while (!space) { // Read new bytes if needed if (byteBuffer.position() >= byteBuffer.limit()) { if (!fill(false)) // request line parsing return false; } int pos = byteBuffer.position(); byte chr = byteBuffer.get(); if (chr == Constants.SP || chr == Constants.HT) { space = true; end = pos; } else if (chr == Constants.CR || chr == Constants.LF) { // HTTP/0.9 style request parsingRequestLineEol = true; space = true; end = pos; } else if (chr == Constants.QUESTION && parsingRequestLineQPos == -1) { parsingRequestLineQPos = pos; } else if (HttpParser.isNotRequestTarget(chr)) { throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget")); } } request.queryString().setBytes(byteBuffer.array(), parsingRequestLineQPos + 1, end - parsingRequestLineQPos - 1); request.requestURI().setBytes(byteBuffer.array(), parsingRequestLineStart, parsingRequestLineQPos - parsingRequestLineStart); //設置request的URI的屬性值和queryString的屬性值 parsingRequestLinePhase = 5; //狀態變動 } if (parsingRequestLinePhase == 5) { // 繼續讀取空白字符,URI和http協議之間的空白字符 parsingRequestLinePhase = 6; //裝填變動 } if (parsingRequestLinePhase == 6) { // // 解析協議 // Protocol is always "HTTP/" DIGIT "." DIGIT // request.protocol().setBytes(byteBuffer.array(), parsingRequestLineStart, end - parsingRequestLineStart); //設置request的請求協議屬性值 return true; //全部都解析完成之後進行返回 } }
(吐槽一下編輯器,一直沒有找到好的編輯器,在代碼裏面我也能把相關的字體加粗顯示,以表示哪些是重點,而且這種黑底白字的我也不喜歡,看來找機會自建博客吧)
parseRequestLine 中使用parsingRequestLinePhase變量來記錄如今已經讀取到第幾種類型的元素了、初始化爲0,當parsingRequestLinePhase爲2時,表明讀取到了請求方法,parsingRequestLinePhase的值含義以下
一、前置空白字符
二、方法POST
3 中間空白字符
4 URI,http://localhost/examples/servlets/servlet/RequestParamExample
五、空白字符
6 協議HTTP/1.1
而後會讀取到回車字符,第一行內容的讀取就此結束。
這樣的話請求行中第一行的解析也就結束了。上面的數字對應的是parsingRequestLinePhase值,以及它具體解析出來的東西。
請求行解析完成之後,接下來就要解析請求頭了。請求頭的格式以下:
cache-control: no-cache
Postman-Token: 3fdd5c34-9b40-4750-8ea1-c868bc9479a7
Content-Type: application/x-www-form-urlencoded
User-Agent: PostmanRuntime/6.3.2
Accept: */*
Host: localhost
accept-encoding: gzip, deflate
content-length: 27
Connection: keep-alive
在上面已經貼出,爲了避免讓你再往上翻頁,或者你忘了,在這再貼出來一下。
解析請求頭使用的是Http11InputBuffer類的parseHeaders方法,這是一個批量解析的方法
boolean parseHeaders() throws IOException {
if (!parsingHeader) {
throw new IllegalStateException(sm.getString("iib.parseheaders.ise.error"));
}
HeaderParseStatus status = HeaderParseStatus.HAVE_MORE_HEADERS;
do {
status = parseHeader();
} while (status == HeaderParseStatus.HAVE_MORE_HEADERS);
if (status == HeaderParseStatus.DONE) {
parsingHeader = false;
end = byteBuffer.position();
return true;
} else {
return false;
}
}
能夠看到,這個裏面用一個HeaderPraseStatus來標示解析的進度,看是否已經解析完成,還有沒有未解析的頭,若是有的話,則調用parseHeader進行解析,沒有的話,HeaderParseStatus狀態變動,則循環結束。看一下如何對單行的請求頭進行解析的。
private HeaderParseStatus parseHeader() throws IOException { byte chr = 0; while (headerParsePos == HeaderParsePosition.HEADER_START) { // Read new bytes if needed if (byteBuffer.position() >= byteBuffer.limit()) { if (!fill(false)) {// parse header headerParsePos = HeaderParsePosition.HEADER_START; return HeaderParseStatus.NEED_MORE_DATA; } } chr = byteBuffer.get(); //當取到的字符是\r 則忽略,繼續讀取下一個字符,當\n的下一個字符是\n 是,則更改解析頭的狀態爲DONE,解析頭的工做結束 if (chr == Constants.CR) { // Skip } else if (chr == Constants.LF) { return HeaderParseStatus.DONE; } else { byteBuffer.position(byteBuffer.position() - 1); break; } } if (headerParsePos == HeaderParsePosition.HEADER_START) { // Mark the current buffer position headerData.start = byteBuffer.position(); //移動讀取的位置,而後設置當前讀取的標示爲HEADER_NAME headerParsePos = HeaderParsePosition.HEADER_NAME; } // // 開始讀取HEADER_NAME,就是header 冒號 :以前的部分 // Header name is 老是 US-ASCII while (headerParsePos == HeaderParsePosition.HEADER_NAME) { int pos = byteBuffer.position(); chr = byteBuffer.get(); if (chr == Constants.COLON) { //一直一個字符一個字符的讀取,當讀取到的字符是冒號 ":"時,header name結束,更改狀態爲HEADER_VALUE_START (開始讀取頭的value) headerParsePos = HeaderParsePosition.HEADER_VALUE_START; //設置header的name headerData.headerValue = headers.addValue(byteBuffer.array(), headerData.start, pos - headerData.start); } } // Skip the line and ignore the header if (headerParsePos == HeaderParsePosition.HEADER_SKIPLINE) { return skipLine(); } // 讀取請求頭的Value (可能會跨越多行) while (headerParsePos == HeaderParsePosition.HEADER_VALUE_START || headerParsePos == HeaderParsePosition.HEADER_VALUE || headerParsePos == HeaderParsePosition.HEADER_MULTI_LINE) { //處理value } //設置 header value headerData.headerValue.setBytes(byteBuffer.array(), headerData.start, headerData.lastSignificantChar - headerData.start); headerData.recycle(); return HeaderParseStatus.HAVE_MORE_HEADERS; //默認還有更多的頭部信息 }
經過parseHeaders()和parseHeader()方法,解析請求的頭部,而後分別把請求頭部的name值和value值存儲到HeaderParseData中,解析的過程當中,遇到 \r\n 則對頭部解析結束
關於請求行的解析,在service方法中就這麼多了,剩下的就是關於協議升級和錯誤處理的相關邏輯,在這本篇中暫時就不去深究。
讀到此時,作了哪些事兒呢,就是解析請求行和請求頭,尚未看到請求參數解析的影子,那咱們就接着往下走,而後數據被流轉到了CyoteAdapter類的service方法中。這是一個適配器,是一個分界嶺,tomcat的兩大核心的東西 Connector和Container,就是在此時用這個類鏈接起來的。在這個方法中涉及到以下內容
request/request 轉換:tomcat中的cyote.request轉換爲servlert.Request
Connectainer設置:設置valve以及須要被處理的容器相關。設置完以後就開始調用容器的valve的invoke,從container,而後engineer,context一直到wrapper,經過一系列的管道和過濾器,終於帶着request和response對象,走到了httpServlet的service方法中。Request對象中有一個屬性叫作parametersParsed,這個參數的意思是請求參數是否已經解析,到了這時候這個參數的值依然是false,也就是說tomcat對請求參數尚未解析。實際上在tomcat1.4以及以前的版本中,參數的解析是在Connector中的,爲了提升效率,有時候不須要關注請求的參數,因此在以後的tomcat版本中,參數的解析就放在了真正使用的時候。也就是咱們調用request.getParameter的時候。以上囉囉嗦嗦的說了這麼多,那就看一下參數是具體怎麼解析的吧。
咱們在本身的servlet中的request,其實是個門面request
這個requestFacade持有Connector.request對象,也就是真正的request,調用getParameter方法時,首先會判斷參數是否已經解析,若已經解析,則直接從緩存中拿取數據,若未解析,則調用parseParameter()方法進行解析參數。對於要解析請求行中的參數,解析是在Parameters的handlerQueryParameters函數。
因此對參數的解析,在當前的tomcat版本中均已經放到了Parameter類中。(Request類中parseParameters()處理contentType)
對參數解析的核心方法是
private void processParameters(byte bytes[], int start, int len, Charset charset)
方法雖長,可是思路很簡單,就是經過標記start和end逐字節的解析取字符,參數形式爲name=value&name2=value2,碰到 = 號,就把前面的記做name,而後後面的記做value,根據&分割參數。而後對一些非標準的參數列表進行處理,形如 && 和&=value&,而後針對解析出來的name和value,調用addParameter方法,把它增長到參數列表中,參數列表實際上也就是個ArrayList。tomcat對請求頭的解析採用逐字節讀取的方式,對請求參數的解析也採用此種方式,這種寫的是如此底層,不知道爲何不直接採用拿到字符串,而後採用字符串分割的方式進行解析呢,難道是由於這種方式執行效率更高是麼?也許是吧,畢竟我也沒有通過度量,之後有機會試試。
在Request中解析參數,有如下的須要注意的邏輯:
protected void parseParameters() { parametersParsed = true; Parameters parameters = coyoteRequest.getParameters(); boolean success = false; boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); parameters.setCharset(charset); if (useBodyEncodingForURI) { parameters.setQueryStringCharset(charset); } parameters.handleQueryParameters(); if( !getConnector().isParseBodyMethod(getMethod()) ) { success = true; return; } String contentType = getContentType(); if (contentType == null) { contentType = ""; } int semicolon = contentType.indexOf(';'); if (semicolon >= 0) { contentType = contentType.substring(0, semicolon).trim(); } else { contentType = contentType.trim(); } if ("multipart/form-data".equals(contentType)) { parseParts(false); success = true; return; } if (!("application/x-www-form-urlencoded".equals(contentType))) { success = true; return; } }
從上能夠得出如下結論
一、 無論什麼類型的請求,均會調用parameters.handleQueryParameters();解析請求行中帶的參數 path?p1=value1&p2=value2
二、 根據請求的方法,若是請求不是POST方法,則再也不往下解析,直接返回true
三、 判斷請求content-type類型,從從這段代碼中能夠看出,實際上tomcat只會主動處理兩種content-type的請求,
multipart/form-data和application/x-www-form-urlencoded
multipart/form-data處理文件上傳的操做,tomcat爲咱們進行了封裝,使咱們沒必要要直接和inputStream打交道,只須要獲取文件的part便可,這也就是爲何咱們在上傳文件時必需要求form的enctype=」multipart/form-data」,而且請求方法是post了。
application/x-www-form-urlencoded處理表單提交的數據,解析表單提交的參數。有時候咱們用POSTMAN模擬發送請求,後臺參數接收不到,這時候就須要關注一下content-type使用的是什麼了。雖然咱們常常能夠看到content-type=application/json或者其餘的,可是實際上tomcat對這種傳遞過來的參數是不作處理的,咱們只能用request.getInputStream本身處理。使用springmvc之類的框架可能對這個有封裝。可是必定要謹記的是tomcat只會處理表單的content-type爲application/x-www.-form-urlencoded和multipart/form-data的參數。