前面提到过,netty解析http协议,会将解析后的数据,分为以下部分:
- 一个HttpMessage
- 一个或者多个HttpContent
- 一个 LastHttpContent
Post 协议分为两种,一种是普通post协议,一种是 multipart/form-data类型。
具体官网例子:https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/upload
HttpPostRequestDecoder
类定义:
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;
private final InterfaceHttpPostRequestDecoder decoder;
}
HttpPostRequestDecoder 为netty中处理post请求类,里面有 一个成员变量 decoder,有两种实现:
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
ObjectUtil.checkNotNull(factory, "factory");
ObjectUtil.checkNotNull(request, "request");
ObjectUtil.checkNotNull(charset, "charset");
// Fill default values
if (isMultipart(request)) {
decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
} else {
decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
}
}
即content-type 为 multipart开头的post请求,以及普通标准的post请求。
对于http消息体,主要是放进 HttpContent 中,而主要就是将传入的ByteBuf存放在HttpContent中。
二者的解析,主要是在 方法offer中。
HttpPostStandardRequestDecoder
HttpPostStandardRequestDecoder
的 offer
方法:
@Override
public HttpPostStandardRequestDecoder offer(HttpContent content) {
checkDestroyed();
// Maybe we should better not copy here for performance reasons but this will need
// more care by the caller to release the content in a correct manner later
// So maybe something to optimize on a later stage
ByteBuf buf = content.content();
if (undecodedChunk == null) {
undecodedChunk = buf.copy();
} else {
undecodedChunk.writeBytes(buf);
}
if (content instanceof LastHttpContent) {
isLastChunk = true;
}
parseBody();
if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
undecodedChunk.discardReadBytes();
}
return this;
}
- 将HttpContent内容,拷贝到undecodedChunk中。
- 调用
parseBody()
方法对数据进行解析。
HttpPostStandardRequestDecoder
的parseBody
方法
private void parseBody() {
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
if (isLastChunk) {
currentStatus = MultiPartStatus.EPILOGUE;
}
return;
}
parseBodyAttributes();
}
- 设置当前状态,如果是最后一个chunk,则将当前状态设置为
MultiPartStatus.EPILOGUE
- 执行
parseBodyAttributes
解析参数。
而解析的方式,则是通过LR
、CR
、&
、=
来进行解析的,简而言之,就是通过换行或者&来分割http属性,而通过=来进行值赋值。
所以浏览器会进行http解码,以防影响http协议解析。
HttpPostStandardRequestDecoder
的 parseBodyAttributes
部分方法:
loop: while (sao.pos < sao.limit) {
char read = (char) (sao.bytes[sao.pos++] & 0xFF);
currentpos++;
switch (currentStatus) {
case DISPOSITION:// search '='
if (read == '=') {
currentStatus = MultiPartStatus.FIELD;
equalpos = currentpos - 1;
String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
charset);
currentAttribute = factory.createAttribute(request, key);
firstpos = currentpos;
} else if (read == '&') { // special empty FIELD
currentStatus = MultiPartStatus.DISPOSITION;
ampersandpos = currentpos - 1;
String key = decodeAttribute(
undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
currentAttribute = factory.createAttribute(request, key);
currentAttribute.setValue(""); // empty
addHttpData(currentAttribute);
currentAttribute = null;
firstpos = currentpos;
contRead = true;
}
break;
case FIELD:// search '&' or end of line
if (read == '&') {
currentStatus = MultiPartStatus.DISPOSITION;
ampersandpos = currentpos - 1;
setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
firstpos = currentpos;
contRead = true;
} else if (read == HttpConstants.CR) {
if (sao.pos < sao.limit) {
read = (char) (sao.bytes[sao.pos++] & 0xFF);
currentpos++;
if (read == HttpConstants.LF) {
currentStatus = MultiPartStatus.PREEPILOGUE;
ampersandpos = currentpos - 2;
sao.setReadPosition(0);
setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
firstpos = currentpos;
contRead = false;
break loop;
} else {
// Error
sao.setReadPosition(0);
throw new ErrorDataDecoderException("Bad end of line");
}
} else {
if (sao.limit > 0) {
currentpos--;
}
}
} else if (read == HttpConstants.LF) {
currentStatus = MultiPartStatus.PREEPILOGUE;
ampersandpos = currentpos - 1;
sao.setReadPosition(0);
setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
firstpos = currentpos;
contRead = false;
break loop;
}
break;
default:
// just stop
sao.setReadPosition(0);
contRead = false;
break loop;
}
}
如果解析到正确的一个键值对数据,会调用 addHttpData(currentAttribute)
方法将属性缓存起来。
protected void addHttpData(InterfaceHttpData data) {
if (data == null) {
return;
}
List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
if (datas == null) {
datas = new ArrayList<InterfaceHttpData>(1);
bodyMapHttpData.put(data.getName(), datas);
}
datas.add(data);
bodyListHttpData.add(data);
}
将数据加入到 bodyListHttpData
中。
content-type: application/x-www-form-urlencoded, 说明是url解码
http请求体为&为分隔符,且用=来赋值。
HttpPostMultipartRequestDecoder
处理 content-type 为 multipart 类型的post请求时,采用 HttpPostMultipartRequestDecoder 来解析处理。
前面处理都一致,核心处理方法 parseBody
:
HttpPostMultiparRequestDecoder
的 parseBody
方法:
private void parseBody() {
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
if (isLastChunk) {
currentStatus = MultiPartStatus.EPILOGUE;
}
return;
}
parseBodyMultipart();
}
具体解析body部分:
private void parseBodyMultipart() {
if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) {
// nothing to decode
return;
}
InterfaceHttpData data = decodeMultipart(currentStatus);
while (data != null) {
addHttpData(data);
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
break;
}
data = decodeMultipart(currentStatus);
}
}
整个解析过程较为复杂,结合http请求报文具体看:
content-type
为multipart/form-data; boundary=----WebKitFormBoundaryy1rBvHvbc0IdjF9h
boundary 代表分隔符,具体看下面请求报文即可。- 接下来是form类型参数
Content-Disposition: form-data; name="getform"
有 分隔符 + 一个回车换行在属性之间区分。
- 如果上传文件,http报文形式为:
如上图,仍然是以 boundary进行分割,有第二行进行说明新的 content-type。
总结
本文分析了netty解析post方法两个类:
- HttpPostStandardRequestDecoder:用于解析标准的post请求体,主要用&进行分割,=进行赋值。
- HttpPostMultipartRequestDecoder:用于解析content-type为multipart开头的http的post协议,后面必须带boundary进行分割。
关注博主公众号: 六点A君。
哈哈哈,一起研究Netty: