tomcat request請求解析 原

一、請求頭解析

我們都知道,請求參數可以存在於請求頭中,也可以存在於請求體中,瀏覽器中發過來的請求通常是下面這個樣子的

我們都知道,請求參數可以存在於請求頭中,也可以存在於請求體中,瀏覽器中發過來的請求通常是下面這個樣子的

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-typeapplication/x-www.-form-urlencodedmultipart/form-data的參數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章