tomcat如何处理 chunked response

背景

之前分析诡异的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)