tomcat request請求解析

1、請求頭解析

咱們都知道,請求參數能夠存在於請求頭中,也能夠存在於請求體中,瀏覽器中發過來的請求一般是下面這個樣子的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的時候。以上囉囉嗦嗦的說了這麼多,那就看一下參數是具體怎麼解析的吧。

2、請求參數解析

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

相關文章
相關標籤/搜索