一、請求頭解析
我們都知道,請求參數可以存在於請求頭中,也可以存在於請求體中,瀏覽器中發過來的請求通常是下面這個樣子的
我們都知道,請求參數可以存在於請求頭中,也可以存在於請求體中,瀏覽器中發過來的請求通常是下面這個樣子的
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分開,然後多個參數用&拼接,這種參數就叫做在請求頭中。
而firstname和lastname這一行,和上面的用了一個空行進行了分割開,這段內容是存在方法體中 。
現在以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。
接下來來看真正接收到請求並處理的Http11Procossor的service方法。那麼對於對象頭參數的解析,也就是在這個方法調用中完成的。
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這個方法
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的值含義如下
1、前置空白字符
2、方法POST
3 中間空白字符
4 URI,http://localhost/examples/servlets/servlet/RequestParamExample
5、空白字符
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;
}
}
從上可以得出以下結論
1、 不管什麼類型的請求,均會調用parameters.handleQueryParameters();解析請求行中帶的參數 path?p1=value1&p2=value2
2、 根據請求的方法,如果請求不是POST方法,則不再往下解析,直接返回true
3、 判斷請求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的參數。