美文网首页
Tomcat http Body 解析源码分析

Tomcat http Body 解析源码分析

作者: 绝尘驹 | 来源:发表于2017-10-18 19:46 被阅读0次

    前面2篇文章分析了tomcat 关于 http request line 和 header 的解析,本文是最后一篇关于body部分的,tomcat 对 http协议解析就算写完了。

    tomcat 解析http body 部分是通过InputFilter 来解析,不同的body 形式由不同的inputFilter来解析,下面我们先看下inputFilter 的初始化

    // Create and add the identity filters.
    // tomcat 通过filter来 读body,IdentityInputFilter 读非Chunk body
    inputBuffer.addFilter(new IdentityInputFilter(maxSwallowSize));outputBuffer.addFilter(new IdentityOutputFilter());
    
            // Create and add the chunked filters.
            inputBuffer.addFilter(new ChunkedInputFilter(maxTrailerSize, allowedTrailerHeaders,
                    maxExtensionSize, maxSwallowSize));
            outputBuffer.addFilter(new ChunkedOutputFilter());
    
            // Create and add the void filters.
            inputBuffer.addFilter(new VoidInputFilter());
            outputBuffer.addFilter(new VoidOutputFilter());
    
            // Create and add buffered input filter
            inputBuffer.addFilter(new BufferedInputFilter());
    

    根据body 形式的不同,解析的inputFilter 也不一样,普通的body 用IdentityInputFilter 来解析,chunked body 用ChunkedInputFilter 来解析。
    同理,没有body就用VoidInputFilter。

    这么多inputFilter 是由inputbuffer 管理起来的

    void addFilter(InputFilter filter) {
            if (filter == null) {
                throw new NullPointerException(sm.getString("iib.filter.npe"));
            }
    
            InputFilter[] newFilterLibrary = new InputFilter[filterLibrary.length + 1];
            for (int i = 0; i < filterLibrary.length; i++) {
                newFilterLibrary[i] = filterLibrary[i];
            }
            newFilterLibrary[filterLibrary.length] = filter;
            filterLibrary = newFilterLibrary;
            //activeFilters 是在读完header后,根据content length 来判断body是普通的还是chunked 形式的。来决定用那个inputFilter 来解释,并放到activeFilters
            activeFilters = new InputFilter[filterLibrary.length];
        }
    

    inputFilter 初始化后,tomcat 回在解析完header后,就开始做body解析的准备工作,具体选择那个inputFilter 来解析body 部分。

         try {
                //指定request body的读取filter,并没有真正读取
                prepareRequest();
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("http11processor.request.prepare"), t);
                }
                // 500 - Internal Server Error
                response.setStatus(500);
                setErrorState(ErrorState.CLOSE_CLEAN, t);
                getAdapter().log(request, response, 0);
            }
    
    连接重用KeepAlive

    prepareRequest 方法还有个比较重要的是根据http 协议的版本,决定是否支持keeplive 即链接持久化。

     if (protocolMB.equals(Constants.HTTP_11)) {
         http11 = true;
         protocolMB.setString(Constants.HTTP_11);
     } else if (protocolMB.equals(Constants.HTTP_10)) {
         http11 = false;
         keepAlive = false;
         protocolMB.setString(Constants.HTTP_10);
     } else if (protocolMB.equals("")) {
         // HTTP/0.9
         http09 = true;
         http11 = false;
         keepAlive = false;
     }
    

    可以看出只有http 1.1 才支持keepAlive,否则都是短连接,不能重用。

    和body相关的header 是transfer-encodingcontent-length,prepareRequest方法会根据这两个header 来判断body的解析

    tomcat是先检查是否有transfer-encoding, 需要注意的是,需要http1.1 才能支持chunk模式,代码如下:

    //需要http11
    if (http11) {
        MessageBytes transferEncodingValueMB = headers.getValue("transfer-encoding");
        if (transferEncodingValueMB != null) {
            String transferEncodingValue = transferEncodingValueMB.toString();
            // Parse the comma separated list. "identity" codings are ignored
            int startPos = 0;
            //transfer-encoding 可以指定多种编码方式,类似这样的transfer-encoding:aa,bb
            int commaPos = transferEncodingValue.indexOf(',');
            String encodingName = null;
            while (commaPos != -1) {
                //获取第一个encodding
                encodingName = transferEncodingValue.substring(startPos, commaPos);
                //根据encoding 选择对应的inputFilter
                addInputFilter(inputFilters, encodingName);
                startPos = commaPos + 1;
                commaPos = transferEncodingValue.indexOf(',', startPos);
            }
            encodingName = transferEncodingValue.substring(startPos);
            addInputFilter(inputFilters, encodingName);
        }
    }
    

    下面我们看addInputFilter 的实现

    private void addInputFilter(InputFilter[] inputFilters, String encodingName) {
    
        // Trim provided encoding name and convert to lower case since transfer
        // encoding names are case insensitive. (RFC2616, section 3.6)
        encodingName = encodingName.trim().toLowerCase(Locale.ENGLISH);
        //
        if (encodingName.equals("identity")) {
            // Skip 如果encoding 是identity 则忽略
        } else if (encodingName.equals("chunked")) {
            //如果是chunked,则指定inputFilter为ChunkedInputFilter,通过索引指定,inputFilter数组中依次为identity,chunked,buffer,void
            //通过addActiveFilter来设置activeFilter
            inputBuffer.addActiveFilter
                (inputFilters[Constants.CHUNKED_FILTER]);
            //并设置contentDelimitation 为true,说明已经确定,后面如果出现content-length时,会忽略。    
            contentDelimitation = true;
        } else {
            //如果不是identity,chunked,则根据encoding 匹配到具体的inputFilter
            for (int i = pluggableFilterIndex; i < inputFilters.length; i++) {
                if (inputFilters[i].getEncodingName().toString().equals(encodingName)) {
                    inputBuffer.addActiveFilter(inputFilters[i]);
                    return;
                }
            }
            // Unsupported transfer encoding
            // 501 - Unimplemented
            response.setStatus(501);
            setErrorState(ErrorState.CLOSE_CLEAN, null);
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("http11processor.request.prepare") +
                          " Unsupported transfer encoding [" + encodingName + "]");
            }
        }
    }
    
    

    addActiveFilter 方法如下:

    void addActiveFilter(InputFilter filter) {
    
        //lastActiveFilter 默认是-1,即第一个activeFilter时,指定filter的buffer
        if (lastActiveFilter == -1) {
            //inputStreamInputBuffer 为SocketInputBuffer,
            filter.setBuffer(inputStreamInputBuffer);
        } else {
            //不是第一个,则需要判断是否已经添加过。如果添加过,则忽略
            for (int i = 0; i <= lastActiveFilter; i++) {
                if (activeFilters[i] == filter)
                    return;
            }
            //前面已经有filter,该filter的buffer 为上个filter的buffer
            filter.setBuffer(activeFilters[lastActiveFilter]);
        }
        
        //添加到activeFilters
        activeFilters[++lastActiveFilter] = filter;
        //如果是普通的post请求,对应的filter 设置content-length
        filter.setRequest(request);
    }
    
    

    看完了transfer-encoding ,我们看下content-length的解析

    // Parse content-length header
    long contentLength = request.getContentLengthLong();
    if (contentLength >= 0) {
        从上面的代码可以看出,contentDelimitation为true,即代表出现transfer-encoding header,则忽略content-length
        if (contentDelimitation) {
            // contentDelimitation being true at this point indicates that
            // chunked encoding is being used but chunked encoding should
            // not be used with a content length. RFC 2616, section 4.4,
            // bullet 3 states Content-Length must be ignored in this case -
            // so remove it.
            headers.removeHeader("content-length");
            request.setContentLength(-1);
        } else {
            //没有出现chunked,则时普通的body,指定读取request body的filter为identityFilter解析
            inputBuffer.addActiveFilter
                    (inputFilters[Constants.IDENTITY_FILTER]);
            contentDelimitation = true;
        }
    }
    

    现在解析body的inputFilter 知道了,那在具体在什么时候用这个activeFilter 去做解析呢,答案是我们在第一次调用getParameter(String name ) 或者和parameter 相关的方法时,或者调用getInputStream()方法时会去解析body、而不时tomcat先解析好,等我们去具体获取参数时直接给我们的

    那现在我们先看下getParameter(String name)方法,这时我们写servlet时用的最熟悉的方法:

    我们调用servletRequest的getParameter(String name) 方法是有tomcat的connector下的的Request实现的。

    @Override
    public String getParameter(String name) {
      //parametersParsed 默认是false,解析万后会设置为true,防止重复读
       if (!parametersParsed) {
             parseParameters();
       }
       return coyoteRequest.getParameters().getParameter(name);
    
    }
    

    parseParameters 主要为解析主要做件如下几件事

    1 解析请求头url后面跟的参数,通过

    //解析url后面的参数
    parameters.handleQueryParameters(); 
    

    2 需要判断是否执行过getInputStream(),如果通过stream流的形式读了,则在getParameter是读不到body里的参数的。只能获取url后面的参数。

    //usingInputStream 为true,或者usingReader 为true则不直接返回
    if (usingInputStream || usingReader) {
           success = true;
           return;
    }
    

    3 检查header content-type,如果不是multipart/form-data 或者
    application/x-www-form-urlencoded 的,则不解析body

    if ("multipart/form-data".equals(contentType)) {
           parseParts(false);
           success = true;
           return;
    }
    
    if (!("application/x-www-form-urlencoded".equals(contentType))) {
            success = true;
            return;
    }
    

    4 上面3部分都符合要求后,则开始读body,解析参数,我们先看普通的body即存在 content-lenght header 请求post 请求

    5 body大小检查读body前先检查content-length 是否超过设置的最大值,如果超过则,抛postTooLarge

    //获取content-length,如果是chunked,则返回-1
    int len = getContentLength();
    //len 大于0则是普通的body
    if (len > 0) {
    int maxPostSize = connector.getMaxPostSize();
    //检查body的大小是否超过了maxPostSize 的大小,maxPostSize默认是2m
    if ((maxPostSize >= 0) && (len > maxPostSize)) {
        Context context = getContext();
        if (context != null && context.getLogger().isDebugEnabled()) {
            context.getLogger().debug(
                    sm.getString("coyoteRequest.postTooLarge"));
        }
        checkSwallowInput();
        parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
        return;
    }
    

    6 各项检查通过后,开始真正读body,这也是我们最关心的,总结起来就一句话:tomcat 是从底层inputbuffer把body的字节流copy到一个字节数组里,通过解析这个字节数组把里面的参数解析出来放到LinkedHashMap里,tomcat 会在这时做一次urlDecode,保证我们获取到的参数已经时解过码的,代码如下:

    //body 字节部分
    byte[] formData = null;
    //如果body的大小小于CACHED_POST_LEN,默认8192即8k,tomcat是用缓存的字节数组,不是重新创建一个,新建一个意味着向os申请一块连续的内存,如果不重用,则会出现频繁的申请向os.
    if (len < CACHED_POST_LEN) {
        if (postData == null) {
            postData = new byte[CACHED_POST_LEN];
        }
        formData = postData;
    } else {
        formData = new byte[len];
    }
    try {
        //这里是从inputbuffer里er读数据。我们分析了半天都没有看到前面提到的inputFilt到底在那里发挥作用的,接下来它就要等场了,这里是普通post的非chunk,则用的是IdentityInputFilter。
        if (readPostBody(formData, len) != len) {
    parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
            return;
        }
    } catch (IOException e) {
        // Client disconnect
        Context context = getContext();
        if (context != null && context.getLogger().isDebugEnabled()) {
            context.getLogger().debug(
                    sm.getString("coyoteRequest.parseParameters"),
                    e);
        }
        parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
        return;
    }
    //这里是读到具体内容后,解析出参数出来,这个都是一样的,无论是普通post请求还是chunked 请求,这个不是我们关注的点,不具体分析
    parameters.processParameters(formData, 0, len);
    

    readPostBody 的代码如下,tomcat的io buffer 比较多,如果不仔细分析,很难搞清楚是怎么一层层copy的,下面的getStream()是
    CoyoteInputStream,CoyoteInputStream 也是个代理,它有个实现了ApplicationBufferHandler 的 inputBuffer,这是tomcat应用层的buffer。CoyoteInputStream 把内容读到该inputbuffer里。

    protected int readPostBody(byte[] body, int len)
            throws IOException {
    
            int offset = 0;
            do {
                //这里最终是Http11InputBuffer 根据activeFilter 去读body
                int inputLen = getStream().read(body, offset, len - offset);
                if (inputLen <= 0) {
                    return offset;
                }
                offset += inputLen;
            } while ((len - offset) > 0);
            return len;
    
        }
    
    

    上面的read 会执行到这里,即Http11InputBuffer的doRead方法,handler是request的inputbuffer

    lastActiveFilter 如果为-1,则说明没有对应的inputFilter,直接通过SocketInputBuffer去Http11InputBuffer的inputbuffer里读。正常情况下
    lastActiveFilter是0,非chunked 请求是IdentityInputFilter。

    @Override
        public int doRead(ApplicationBufferHandler handler) throws IOException {
    
            if (lastActiveFilter == -1)
                return inputStreamInputBuffer.doRead(handler);
            else
                return activeFilters[lastActiveFilter].doRead(handler);
    
        }
    

    IdentityInputFilter 的doRead方法如下:

    @Override
        public int doRead(ApplicationBufferHandler handler) throws IOException {
    
            int result = -1;
           //contentLength ,remaining 是在filter设置request的时候确定的,
           if (contentLength >= 0) {
                if (remaining > 0) {
                   //这里又是buffer,估计你已经晕了,这个buffer是在设置activeFilter时确定的,一个filter的话,该buffer就是SocketInputBuffer,handler是ApplicationBufferHandler,
                  int nRead = buffer.doRead(handler);
                    if (nRead > remaining) {
                        // The chunk is longer than the number of bytes remaining
                        // in the body; changing the chunk length to the number
                        // of bytes remaining
                        handler.getByteBuffer().limit(handler.getByteBuffer().position() + (int) remaining);
                        result = (int) remaining;
                    } else {
                        result = nRead;
                    }
                    if (nRead > 0) {
                        remaining = remaining - nRead;
                    }
                } else {
                    // No more bytes left to be read : return -1 and clear the
                    // buffer
                    if (handler.getByteBuffer() != null) {
                        handler.getByteBuffer().position(0).limit(0);
                    }
                    result = -1;
                }
            }
    
            return result;
    
    

    分析到这里,我们需要搞清楚这些buffer这间那有copy,那里是公用底层buffer的,上面的 buffer.doRead(handler); 这里就是和Http11InputBuffer的inputBuffer 共享一份字节数组的,通过duplicate实现,这里就减少一次copy。

    SocketInputBuffer

    前面几次提到SocketInputBuffer ,有必要说明下,SocketInputBuffer 是tomcat Http11InputBuffer的内部类,它没有buffer,只是起到代理作用,通过SocketInputBuffer 去Http11InputBuffer的inputBuffer 中读数据,如果Http11InputBuffer的inputBuffer 没有可用的数据时,即position 大于或者等于limit时,tomcat 就尝试从OS层读,这里是一次copy。

    Http11InputBuffer

    Http11InputBuffer 在tomcat 序列读请求头时也分析过,这里再单独讲下,是因为Http11InputBuffer 是tomcat的最核心的buffer,它维持里一个byteBuffer,是真正存储字节的buffer。上面应用层的buffer 是和该buffer 共享一个底层字节数组,只是position,limit 各自独立。

    InputStream 通过流读取

    上面分析的是通过获取参数触发的body解析,我们有时候还可以通过getInputStream() 来直接获取body字节流。代码如下:

    @Override
        public ServletInputStream getInputStream() throws IOException {
    
            if (usingReader) {
                throw new IllegalStateException
                    (sm.getString("coyoteRequest.getInputStream.ise"));
            }
    
            usingInputStream = true;
            if (inputStream == null) {
                //如果你看懂了上面的部分,你应该会明白了这里的原理,直接通过CoyoteInputStream 来到inputBuffer,inputBuffer 和底层http11InputBuffer 共享字节数组。
                inputStream = new CoyoteInputStream(inputBuffer);
            }
            return inputStream;
    
        }
    
    

    通过流直接读取特别时候一些不需要解析参数的场景,比如我们正在做的网关系统,我们不需要关心具体内容,只需要把内容转发即可,所以不需要把参数解析出来,解析同时还要做decode操作。

    总结

    tomcat body 解析算是写完了80%,我们这个分析是通过我们通过servletRequest 获取参数时触发的body解析,通过上面的分析,我们知道getParamete 方法才会去真正做参数的读取,这个背后所隐藏的逻辑,字节的copy,我们在应用程序中获取body tomcat 默认需要三次次copy:

    • 第一次从连接的读缓冲区copy到堆外内存
    • 第二次是从堆外内存copy到 tomcat Http11InputBuffer的堆内byteBuffer
    • 第三次一次是从Http11InputBuffer的byteBuffer到应用的buffer,就是我们getOutputStream()的时候。

    再上一张图,更直观的体现下

    tomcat-io-buffer.png

    上图的end标记是一次请求的行和头结束的位置,tomcat每次读到的body都是从end那里开始写的,bytebuffer从0到end是需要保留的,end后面的空间是给body用的,并从os层读一个socket size大小的内容
    到bytebuffer,再从bytebuffer copy到postDataByte,下次还是从end那里开始写。

    这是使用堆内buffer默认情况下,有同学会问,为啥不jdk不能直接从os 缓冲区之间copy到堆内存呢,这是因为堆内存的地址有可能会变化(GC),而os操作需要一个固定的地址。同里写也是一样的,jdk都是线写到堆外内存,再从堆外内存写到OS的缓冲区。感兴趣的同学可以看jdk的源码sun.nio.ch.IOUtil的读和写方法,read 方法如下:

    static int read(FileDescriptor fd, ByteBuffer dst, long position,
                        NativeDispatcher nd, Object lock)
            throws IOException
        {
            if (dst.isReadOnly())
                throw new IllegalArgumentException("Read-only buffer");
            //如果是DirectBuffer 直接读
            if (dst instanceof DirectBuffer)
                return readIntoNativeBuffer(fd, dst, position, nd, lock);
    
            // Substitute a native buffer
            // 非堆外buffer,先临时读到bb,时堆外buffer,
            ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
            try {
                int n = readIntoNativeBuffer(fd, bb, position, nd, lock);
                bb.flip();
                if (n > 0)
                    //copy 在这里
                    dst.put(bb);
                return n;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(bb);
            }
        }
    
    

    这也是我们做网关系统为啥用netty做接入层,熟悉netty的同学都知道,netty 用堆外内存,就减少了两次copy,而是直接转发到后端。

    第一次从OS读缓冲区copy到堆外buffer,而且应用层就可以直接操作该buffer

    tomcat chunked 解析主要是根据chunk 协议 来解析,读字节流部分和上面分析的一样。后面单独写一篇文章来分析。

    相关文章

      网友评论

          本文标题:Tomcat http Body 解析源码分析

          本文链接:https://www.haomeiwen.com/subject/zffzyxtx.html