美文网首页
Tomcat 写操作原理分析--下篇

Tomcat 写操作原理分析--下篇

作者: 绝尘驹 | 来源:发表于2020-02-06 17:15 被阅读0次

上篇分析了tomcat 响应内容时的流程,以及涉及的buffer和copy,阻塞原理等。
但主要分析了生成响应头的部分,body部分还没有分析完成,那这篇就是把写body相关分析完成,本文主要分析下面一个问题:

  • tomcat是怎么选择对内容编码的

我们通过 resp.getWriter().flush()时,开始刷新body的数据,代码如下:

 protected void doFlush(boolean realFlush) throws IOException {

    if (suspended) {
        return;
    }

    try {
        doFlush = true;
        if (initial) {
            //initial默认是true,会检查是否需要准备响应头,如果全面写数据小,没有导致bytebuffer刷新时,是从这里开始准备响应头的,如果已经准备过,这里会返回。
            coyoteResponse.sendHeaders();
            initial = false;
        }
        //通过write写的数据在char buffer里,需要把转换到byte buffer
        if (cb.remaining() > 0) {
            flushCharBuffer();
        }
        //通过outputStream写的字节,则把byteBuffer里的数据写到socket的writeBuffer
        if (bb.remaining() > 0) {
            flushByteBuffer();
        }
    } finally {
        doFlush = false;
    }

    if (realFlush) {
        coyoteResponse.action(ActionCode.CLIENT_FLUSH, null);
        // If some exception occurred earlier, or if some IOE occurred
        // here, notify the servlet with an IOE
        if (coyoteResponse.isExceptionPresent()) {
            throw new ClientAbortException(coyoteResponse.getErrorException());
        }
    }

flush方法就是把output buffer的两个buffer的数据写到socket buffer,body数据的关键在flushByteBuffer,这里会根据是否指定里content-length 来决定body的编码格式,flushByteBuffer方法会调用到
Http11OutputBuffer的doWrite方法,来选中一个编码的filter

  public int doWrite(ByteBuffer chunk) throws IOException {

    if (!response.isCommitted()) {
        // Send the connector a request for commit. The connector should
        // then validate the headers, send them (using sendHeaders) and
        // set the filters accordingly.
        response.action(ActionCode.COMMIT, null);
    }
    if (lastActiveFilter == -1) {
        return outputStreamOutputBuffer.doWrite(chunk);
    } else {
        return activeFilters[lastActiveFilter].doWrite(chunk);
    }
}

上面的代码主要是lastActiveFilter这个filter的索引,根据上篇文章分析的,如果是1.1,如果没有设置content-length,而且没有主动close,tomcat的activeFilters就是ChunkedOutputFilter,该filter就是对响应的body按chunk格式也就是分块发给客户端

Chunk 编码格式

每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束

比如我发送的是 0123456789 通过chunk的编码格式如下:

    'a\r\n'   #16进制的头
    '0123456789\r\n' #内容
    '0\r\n' # 结束快
    '\r\n' # 整个内容的结束

chunk编码是会增加传输的报文大小的,因为一个快就增加了一个头,会思考的有同学就会问,tomcat什么情况下要用chunk传输呢,其实一般情况下是不会用chunk编码发送的,只有在你不指定响应,而且在写数据的过程中,触发了bytebuffer写到socket的writebuffer,就需要准备响应头,因为最后一层writebuffer很定要先写响应头的内容。还有一个条件是不能主动调用flush,因为tomcat会在处理完后,走finshResponse的流程,因为到这里outputbuffer里的数据都确定里,tomcat会自动设置content-length的头。

说完chunk的格式后,我们看下tomcat的chunk编码实现:

public int doWrite(ByteBuffer chunk) throws IOException {

    int result = chunk.remaining();

    if (result <= 0) {
        return 0;
    }

    int pos = calculateChunkHeader(result);

    //写chunk的开始标记:读chunk 的长度 16进制 比如10\r\n
    chunkHeader.position(pos + 1).limit(chunkHeader.position() + 9 - pos);
    buffer.doWrite(chunkHeader);
    //chun的数据
    //buffer是
    buffer.doWrite(chunk);

    //一个chunk的结束标记:\r\n
    chunkHeader.position(8).limit(10);
    buffer.doWrite(chunkHeader);

    return result;
}

上面代码很简单,就是按chunk的格式,对chunk计算头的大小,并把数据写到buffer,这个buffer实际一个写到socket的writeBuffer的代理buffer,tomcat的output filter直接的关系说明下:

学会思考的同学一定会觉得奇怪,这里都flush里,咋不写chunk的结束标记,那chunk的结束标记是在那里写到buffer里的呢,答案是tomcat在执行完业务的代码后,会执行到CoyoteAdapter的下面的逻辑:

  if (!request.isAsync()) {
       request.finishRequest();
       response.finishResponse();
   }

response.finishResponse方法执行OutputBuffer的close方法,这个OutputBuffer是上篇一开始提到的

  public void close() throws IOException {

    if (closed) {
        return;
    }
    if (suspended) {
        return;
    }

    // If there are chars, flush all of them to the byte buffer now as bytes are used to
    // calculate the content-length (if everything fits into the byte buffer, of course).
    //如果没有主动刷新,这里会刷新。
    if (cb.remaining() > 0) {
        flushCharBuffer();
    }

    //如果没有发送响应头,而且没有设置content-length
    if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1)
            && !coyoteResponse.getRequest().method().equals("HEAD")) {
        // If this didn't cause a commit of the response, the final content
        // length can be calculated. Only do this if this is not a HEAD
        // request since in that case no body should have been written and
        // setting a value of zero here will result in an explicit content
        // length of zero being set on the response.
        if (!coyoteResponse.isCommitted()) {
            //到这里是第一次提交,那内容就是buffer的大小,这里设置后,在准备响应头会知道outout filter为identiy filter
            coyoteResponse.setContentLength(bb.remaining());
        }
    }

    if (coyoteResponse.getStatus() == HttpServletResponse.SC_SWITCHING_PROTOCOLS) {
        doFlush(true);
    } else {
        //这里不需要flush,后面通过ActionCode.CLOSE刷新
        doFlush(false);
    }
    closed = true;

    // The request should have been completely read by the time the response
    // is closed. Further reads of the input a) are pointless and b) really
    // confuse AJP (bug 50189) so close the input buffer to prevent them.
    Request req = (Request) coyoteResponse.getRequest().getNote(CoyoteAdapter.ADAPTER_NOTES);
    req.inputBuffer.close();
    //这里
    coyoteResponse.action(ActionCode.CLOSE, null);
}

coyoteResponse.action 的ActionCode.CLOSE 会调用Http11OutputBuffer的end方法()

public void end() throws IOException {
    if (responseFinished) {
        return;
    }

    if (lastActiveFilter == -1) {
        outputStreamOutputBuffer.end();
    } else {
        //ChunkedOutputFilter的end方法
        activeFilters[lastActiveFilter].end();
    }
    responseFinished = true;
}

在ChunkedOutputFilter 的end方法里把chunk的结束部分写到socket 的writeBuffer

public void end() throws IOException {

    Supplier<Map<String,String>> trailerFieldsSupplier = response.getTrailerFields();
    Map<String,String> trailerFields = null;

    if (trailerFieldsSupplier != null) {
        trailerFields = trailerFieldsSupplier.get();
    }

    if (trailerFields == null) {
        // Write end chunk 0\r\n\r\n
        buffer.doWrite(endChunk);
        endChunk.position(0).limit(endChunk.capacity());
    } else {
        buffer.doWrite(lastChunk);
        lastChunk.position(0).limit(lastChunk.capacity());

       ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
       OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.ISO_8859_1);
        for (Map.Entry<String,String> trailerField : trailerFields.entrySet()) {
            // Ignore disallowed headers
            if (disallowedTrailerFieldNames.contains(
                    trailerField.getKey().toLowerCase(Locale.ENGLISH))) {
                continue;
            }
            osw.write(trailerField.getKey());
            osw.write(':');
            osw.write(' ');
            osw.write(trailerField.getValue());
            osw.write("\r\n");
        }
        osw.close();
        buffer.doWrite(ByteBuffer.wrap(baos.toByteArray()));

        buffer.doWrite(crlfChunk);
        crlfChunk.position(0).limit(crlfChunk.capacity());
    }
    //最后在这里把writeBuffer数据写到网络层,buffer 为 
    SocketOutputBuffer
    buffer.end();
}

SocketOutputBuffer 是一个代理,调用socketWrapper的flush方法,

   public void end() throws IOException {
        socketWrapper.flush(true);
    }

socketWrapper 的flush方法上篇已经分析过了,就是把write buffer的数据写到网络上。

OutputFilter的关系

active filter是一个链,最后添加的最先执行,比如要对响应内容有压缩的,则是两个filter,一个是identify filter和zip filter,则先由zip filter压缩,压缩后,再传给后面的filter。有最后一个filter写到socket 的buffer发送到网络 如果lastActiveFilter为-1,说明是第一个output filter,这个filter的buffer为outputStreamOutputBuffer也就是最终写到socket的一个代理,lastActiveFilter 为最先执行filter的索引

总结

我们平时写完数据都会主动flush操作,如果是小的数据,可以不用flush,通过tomcat默认的flush来刷新出去,关键是不对内容进行分块传输

相关文章

网友评论

      本文标题:Tomcat 写操作原理分析--下篇

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