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的参数。

相关文章
相关标签/搜索