美文网首页
tomcat如何处理 chunked response

tomcat如何处理 chunked response

作者: WhiteBase | 来源:发表于2018-11-18 23:19 被阅读0次

    背景

    之前分析诡异的502问题的时候,还有一个疑点没有解释:为什么 tomcat 在接收到 response 的时候再次自行做分块处理呢?

    问题可以转化为:何时tomcat对 response 做分块?分块的条件是什么?

    何时准备输出 response

    处理分块应该在拿到 response 之后,这就需要再次追溯tomcat的请求处理流程,直接从 NioEndpoint 看起,Poller 线程取出 events() 之后进行事件的处理:org.apache.tomcat.util.net.NioEndpoint.Poller#processKey,这里是读写事件,进入 Socket 请求处理逻辑:org.apache.tomcat.util.net.NioEndpoint#processSocket(org.apache.tomcat.util.net.NioEndpoint.KeyAttachment, org.apache.tomcat.util.net.SocketStatus, boolean)

    org.apache.tomcat.util.net.NioEndpoint.SocketProcessor 本身就是一个 Runnable 任务,进入 process 方法:org.apache.coyote.http11.AbstractHttp11Processor#process

    看代码 tomcat 源码中将请求处理分成了不同的阶段,比如:org.apache.coyote.Constants#STAGE_PARSE, 这是一个很重要的线索,

        // Request states
        public static final int STAGE_NEW = 0;
        public static final int STAGE_PARSE = 1;
        public static final int STAGE_PREPARE = 2;
        public static final int STAGE_SERVICE = 3;
        public static final int STAGE_ENDINPUT = 4;
        public static final int STAGE_ENDOUTPUT = 5;
        public static final int STAGE_KEEPALIVE = 6;
        public static final int STAGE_ENDED = 7;
    

    结合 process() 方法的源码,可以猜测对于输出的处理应该在 EndInput -> EndOutput 之间。

                // Finish the handling of the request
                rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT);
    
                if (!isAsync() && !comet) {
                    if (getErrorState().isError()) {
                        // If we know we are closing the connection, don't drain
                        // input. This way uploading a 100GB file doesn't tie up the
                        // thread if the servlet has rejected it.
                        getInputBuffer().setSwallowInput(false);
                    } else {
                        // Need to check this again here in case the response was
                        // committed before the error that requires the connection
                        // to be closed occurred.
                        checkExpectationAndResponseStatus();
                    }
                    endRequest();
                }
    
                rp.setStage(org.apache.coyote.Constants.STAGE_ENDOUTPUT);
    
    

    重要的就是 endRequest() 这个方法。

        public void endRequest() {
    
            // Finish the handling of the request
            if (getErrorState().isIoAllowed()) {
                try {
                    getInputBuffer().endRequest();
                } catch (IOException e) {
                    setErrorState(ErrorState.CLOSE_NOW, e);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    // 500 - Internal Server Error
                    // Can't add a 500 to the access log since that has already been
                    // written in the Adapter.service method.
                    response.setStatus(500);
                    setErrorState(ErrorState.CLOSE_NOW, t);
                    getLog().error(sm.getString("http11processor.request.finish"), t);
                }
            }
            if (getErrorState().isIoAllowed()) {
                try {
                    getOutputBuffer().endRequest();
                } catch (IOException e) {
                    setErrorState(ErrorState.CLOSE_NOW, e);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    setErrorState(ErrorState.CLOSE_NOW, t);
                    getLog().error(sm.getString("http11processor.response.finish"), t);
                }
            }
        }
    

    做了两件事:getInputBuffer().endRequest();getOutputBuffer().endRequest();

    我们关心的是对输出的处理:

        public void endRequest() throws IOException {
    
            if (!committed) {
                // Send the connector a request for commit. The connector should
                // then validate the headers, send them (using sendHeader) and
                // set the filters accordingly.
                response.action(ActionCode.COMMIT, null);
            }
    
            if (finished)
                return;
    
            if (lastActiveFilter != -1)
                activeFilters[lastActiveFilter].end();
    
            flushBuffer(true);
    
            finished = true;
        }
    

    如果 response 没有 commit 则执行 commit,然后如果没有 finish 则进行 flushBuffer ,见名知意,flush才是最后提交到 OS 进行写出的步骤。

    而其中很重要的 action 方法就提示我们进入另一条很重要的源码分析线索:org.apache.coyote.http11.AbstractHttp11Processor#action ,这个方法根据不同的 code 让 connector 做不同的处理。看 commit 动作:

            case COMMIT: {
                // Commit current response
                if (response.isCommitted()) {
                    return;
                }
    
                // Validate and write response headers
                try {
                    prepareResponse();
                    getOutputBuffer().commit();
                } catch (IOException e) {
                    setErrorState(ErrorState.CLOSE_NOW, e);
                }
                break;
            }
    

    方法org.apache.coyote.http11.AbstractHttp11Processor#prepareResponse

    何时处理 response 分块?

    就是实际准备输出内容的地方,那如何处理 chunk?

    准备响应阶段
    //org.apache.coyote.http11.AbstractHttp11Processor#prepareResponse
    // 主要是校验 header / 状态码
            long contentLength = response.getContentLengthLong();
            boolean connectionClosePresent = false;
            if (contentLength != -1) {
                headers.setValue("Content-Length").setLong(contentLength);
                getOutputBuffer().addActiveFilter
                    (outputFilters[Constants.IDENTITY_FILTER]);
                contentDelimitation = true;
            } else {
                // If the response code supports an entity body and we're on
                // HTTP 1.1 then we chunk unless we have a Connection: close header
                connectionClosePresent = isConnectionClose(headers);
                if (entityBody && http11 && !connectionClosePresent) {
                    getOutputBuffer().addActiveFilter
                        (outputFilters[Constants.CHUNKED_FILTER]);
                    contentDelimitation = true;
                    headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
                } else {
                    getOutputBuffer().addActiveFilter
                        (outputFilters[Constants.IDENTITY_FILTER]);
                }
            }
    
    

    内容大意是:看是否有指定 content-lenght,如果有则不 chunk
    如果没有,则看条件:entityBody && http11 && !connectionClosePresent,都满足则进行 chunk 处理

    至此,tomcat 自动进行response分块的条件已经清晰了:body 有内容,http1.1 协议,连接不关闭,三个条件都满足才处理。

    复现502时的请求

    模拟当时的请求:

    $ curl -v "http://localhost:8080/index.jsp" --http1.0
    *   Trying ::1...
    * TCP_NODELAY set
    * Connected to localhost (::1) port 8080 (#0)
    > GET /index.jsp HTTP/1.0
    > Host: localhost:8080
    > User-Agent: curl/7.54.0
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    < Server: Apache-Coyote/1.1
    < Date: Sun, 18 Nov 2018 15:10:12 GMT
    < Connection: close
    <
    * Closing connection 0
    {'data': 'OK'}%                                                                                                                                               
    
    

    关键点就是:当时 tomcat 接收到的 local nginx 发来的请求是 http1.0 的,所以不满足分块响应的条件,也就不会自动分块,但是 response header 又提示了有分块,所以被认为是一个错误的响应,而被丢弃掉了。

    相关文章

      网友评论

          本文标题:tomcat如何处理 chunked response

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