前面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-encoding
和 content-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 协议 来解析,读字节流部分和上面分析的一样。后面单独写一篇文章来分析。
网友评论