上篇分析了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来刷新出去,关键是不对内容进行分块传输
网友评论