文章目录
- HttpPostRequestDecoder
-
- 属性
- 构造方法
-
- isMultipart(HttpRequest)
-
- [getMultipartDataBoundary(String contentType)](#getMultipartDataBoundary(String contentType))
- MultiPartStatus
- HttpPostStandardRequestDecoder
- HttpPostMultipartRequestDecoder
HttpPostRequestDecoder
属性
java
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;
// 内部维护了一个 InterfaceHttpPostRequestDecoder
private final InterfaceHttpPostRequestDecoder decoder;
// ...
}
构造方法
java
public HttpPostRequestDecoder(HttpRequest request) {
this(
new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), // 16KB
request,
HttpConstants.DEFAULT_CHARSET // UTF-8
);
}
java
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) {
this(
factory,
request,
HttpConstants.DEFAULT_CHARSET // UTF-8
);
}
java
public HttpPostRequestDecoder(HttpDataFactory factory,
HttpRequest request,
Charset charset) {
ObjectUtil.checkNotNull(factory, "factory");
ObjectUtil.checkNotNull(request, "request");
ObjectUtil.checkNotNull(charset, "charset");
// 根据content-type请求头,判断是否是文件上传请求
if (isMultipart(request)) {
// 使用文件上传的解码器
decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
} else {
// 使用标准的解码器
decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
}
}
isMultipart(HttpRequest)
java
public static boolean isMultipart(HttpRequest request) {
// 获取content-type请求头对应的值
String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
// 如果content-type请求头的值以multipart/form-data开始
if (mimeType != null &&
mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
// 如果content-type请求头的值符合文件上传的条件,则返回true
return getMultipartDataBoundary(mimeType) != null;
}
// 不是文件上传请求
return false;
}
getMultipartDataBoundary(String contentType)
java
protected static String[] getMultipartDataBoundary(String contentType) {
// Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
// 调用splitHeaderContentType方法将contentType分割成多个部分,存储在headerContentType数组中。假设分割后的数组有三个部分:第一个是媒体类型(如multipart/form-data),第二个和第三个是参数(如boundary和charset)。
String[] headerContentType = splitHeaderContentType(contentType);
// 定义multiPartHeader为"multipart/form-data"
final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString();
// 检查headerContentType[0]是否以不区分大小写的方式匹配multiPartHeader。如果匹配,说明是multipart/form-data类型的请求。
if (headerContentType[0].regionMatches(true,
0, multiPartHeader,
0 , multiPartHeader.length())) {
int mrank;
int crank;
// 定义boundaryHeader为"boundary"
final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString();
// 检查headerContentType[1]是否以不区分大小写的方式匹配boundaryHeader
if (headerContentType[1].regionMatches(true,
0, boundaryHeader,
0, boundaryHeader.length())) {
// boundary在数组中的索引
mrank = 1;
// charset在数组中的索引
crank = 2;
}
// 检查headerContentType[2]是否以不区分大小写的方式匹配boundaryHeader
else if (headerContentType[2].regionMatches(true,
0, boundaryHeader,
0, boundaryHeader.length())) {
// boundary在数组中的索引
mrank = 2;
// charset在数组中的索引
crank = 1;
}
else {
// 如果都没有找到boundary,则返回null
return null;
}
// 从headerContentType[mrank]中提取boundary的值(通过等号'='分割后的部分)
String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
// 如果boundary为null,则抛出异常。
if (boundary == null) {
throw new ErrorDataDecoderException("Needs a boundary value");
}
// 检查boundary的第一个字符是否是双引号,如果是,则去掉首尾的双引号
if (boundary.charAt(0) == '"') {
String bound = boundary.trim();
int index = bound.length() - 1;
if (bound.charAt(index) == '"') {
boundary = bound.substring(1, index);
}
}
// 定义charsetHeader为"charset"
final String charsetHeader = HttpHeaderValues.CHARSET.toString();
// 检查headerContentType[crank]是否以不区分大小写的方式匹配charsetHeader。
if (headerContentType[crank].regionMatches(true,
0, charsetHeader,
0, charsetHeader.length())) {
// 如果匹配,则提取charset的值(通过等号'='分割后的部分)。
String charset = StringUtil.substringAfter(headerContentType[crank], '=');
// 如果charset不为null,则返回一个包含两个元素的字符串数组:第一个是"--" + boundary(注意,boundary前面加两个减号),第二个是charset。
if (charset != null) {
return new String[] {"--" + boundary, charset};
}
}
// 如果没有charset,则返回一个只包含"--" + boundary的数组
return new String[] {"--" + boundary};
}
// 如果不是multipart/form-data类型,则返回null
return null;
}
splitHeaderContentType
java
private static String[] splitHeaderContentType(String sb) {
int aStart;
int aEnd;
int bStart;
int bEnd;
int cStart;
int cEnd;
// 找到第一个非空白字符
aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
// 找到 ";" 分号
aEnd = sb.indexOf(';');
// 如果 没有找到 ";" 分号
if (aEnd == -1) {
// 返回 sb和2个空字符串
return new String[] { sb, "", "" };
}
// 说明找到了 ";" 分号
// 从 ";" 分号后的下一个字符开始找到非空白字符
bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
// 如果 ";" 分号前1个字符是空格,则aEnd索引减1
if (sb.charAt(aEnd - 1) == ' ') {
aEnd--;
}
// 从bStart开始找下一个 ";" 分号
bEnd = sb.indexOf(';', bStart);
// 如果没有 ";" 分号
if (bEnd == -1) {
// 从sb后面往前找第一个非空白字符的索引
bEnd = HttpPostBodyUtil.findEndOfString(sb);
// 返回 2个有效元素 和 1个空字符串
return new String[] { sb.substring(aStart, aEnd),
sb.substring(bStart, bEnd),
""
};
}
// 说明又找到了 ";" 分号
// 从 ";" 分号后的下一个字符开始找到非空白字符
cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
// 如果 ";" 分号前1个字符是空格,则bEnd索引减1
if (sb.charAt(bEnd - 1) == ' ') {
bEnd--;
}
// 从sb后面往前找第一个非空白字符的索引
cEnd = HttpPostBodyUtil.findEndOfString(sb);
// 3个部分都有,都找到了
return new String[] { sb.substring(aStart, aEnd),
sb.substring(bStart, bEnd),
sb.substring(cStart, cEnd)
};
}
MultiPartStatus
这个枚举MultiPartStatus定义了多部分表单数据(multipart/form-data)解析过程中的状态。
java
protected enum MultiPartStatus {
NOTSTARTED, // 初始状态,尚未开始解析。
PREAMBLE, // 前言部分,在实际的分隔符之前的内容,通常忽略。
HEADERDELIMITER, // 已经读取到一个分隔符(即--boundary),接下来应该是一个部分的头部。
DISPOSITION, // 正在解析一个部分的Content-Disposition头部,这个头部通常包含字段名或文件名。
FIELD, // 表示当前部分是一个普通的表单字段,接下来是字段的值。
FILEUPLOAD, // 表示当前部分是一个文件上传,接下来是文件内容
MIXEDPREAMBLE, // 当某个部分本身又是multipart/mixed类型时,该部分的前言。
MIXEDDELIMITER, // 在multipart/mixed部分中,读取到内部边界分隔符。
MIXEDDISPOSITION, // 在multipart/mixed部分中,解析内部部分的Content-Disposition头部。
MIXEDFILEUPLOAD, // 在multipart/mixed部分中,表示内部部分是一个文件上传。
MIXEDCLOSEDELIMITER, // 在multipart/mixed部分中,读取到内部边界的结束分隔符(即--boundary--)
CLOSEDELIMITER, // 读取到外层multipart/form-data的结束分隔符
PREEPILOGUE, // 在结束分隔符之后,实际结束之前的状态
EPILOGUE // 结束状态,所有数据解析完成
}
- 从NOTSTARTED开始,然后进入PREAMBLE(如果有的话)。
- 接下来是零次或多次的普通部分(字段或文件):
- HEADERDELIMITER:读取到分隔符。
- DISPOSITION:解析该部分的头部,得到字段名或文件名。
- 然后根据类型,进入FIELD(普通字段)或FILEUPLOAD(文件上传)。
- 可能有嵌套的multipart/mixed部分(用于一个字段对应多个文件的情况):
- HEADERDELIMITER:读取到分隔符。
- DISPOSITION:解析外层部分的头部。
- MIXEDPREAMBLE:进入混合部分的前言。
- 然后重复多次内部部分(MIXEDDELIMITER, MIXEDDISPOSITION, MIXEDFILEUPLOAD)。
- 最后以MIXEDCLOSEDELIMITER结束内部混合部分。
- 整个multipart/form-data以CLOSEDELIMITER(即结束分隔符)结束。
- 然后进入EPILOGUE(可能有一些尾部内容,通常忽略)。
HttpPostStandardRequestDecoder
报文示例
POST /abc HTTP/1.1
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Cache-Control: no-cache
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
cookie: SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm;SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
content-length: 17
name=zzhua&age=18
java
public class HttpPostSdDecTest {
public static void main(String[] args) {
DefaultHttpDataFactory httpDataFactory = new DefaultHttpDataFactory();
DefaultFullHttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
HttpMethod.POST,
"/test"
);
request.content()
.writeBytes("name=zzhua&age=18".getBytes(StandardCharsets.UTF_8));
HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(
httpDataFactory,
request
);
decoder.offer(request);
for (InterfaceHttpData bodyHttpData : decoder.getBodyHttpDatas()) {
System.out.println(bodyHttpData);
}
}
}
属性
java
// 用于创建InterfaceHttpData对象(例如Attribute和FileUpload)
final HttpDataFactory factory;
// 要解码的请求
final HttpRequest request;
// 默认字符集
final Charset charset;
// 是否已经接收到最后一个块
boolean isLastChunk;
// 存储解析出的所有数据
final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
// 按照名字(忽略大小写)存储数据
final Map<String, List<InterfaceHttpData>> bodyMapHttpData
= new TreeMap<String, List<InterfaceHttpData>>(CaseIgnoringComparator.INSTANCE);
// 存储未解码的数据
ByteBuf undecodedChunk;
// 当前在bodyListHttpData中读取的位置,用于迭代
int bodyListHttpDataRank;
// 当前解析状态,注意这里虽然枚举名为MultiPartStatus,但用于非multipart的解析状态。
MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
// 当前正在解码的属性
Attribute currentAttribute;
// 解码器是否已被销毁
boolean destroyed;
// 丢弃阈值,用于控制内存使用
int discardThreshold = 10 * 1024 * 1024;
构造方法
java
public HttpPostStandardRequestDecoder(HttpRequest request) {
this(
// 16KB,超过此大小,则使用磁盘
new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
// 请求对象
request,
// UTF-8
HttpConstants.DEFAULT_CHARSET
);
}
java
public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) {
this(factory,
request,
// UTF-8
HttpConstants.DEFAULT_CHARSET);
}
java
public HttpPostStandardRequestDecoder(HttpDataFactory factory,
HttpRequest request,
Charset charset) {
this.request = checkNotNull(request, "request");
this.charset = checkNotNull(charset, "charset");
this.factory = checkNotNull(factory, "factory");
try {
// 构造器中会根据
if (request instanceof HttpContent) {
// offer中会调用parseBody() 方法
offer((HttpContent) request);
}
else {
parseBody();
}
} catch (Throwable e) {
destroy();
PlatformDependent.throwException(e);
}
}
offer(HttpContent)
该方法用于接收新的HTTP内容块(HttpContent),并对其进行解码,以解析HTTP POST请求的主体部分。
Netty可以逐步接收HTTP请求体,并逐步解析,而不需要等待整个请求体完全接收完毕。这对于大文件上传尤其重要,可以避免内存溢出。
java
public HttpPostStandardRequestDecoder offer(HttpContent content) {
// 首先,检查解码器(destroyed属性)是否已经被销毁,如果已经销毁,则会抛出异常。
checkDestroyed();
// 判断接收到的content是否是LastHttpContent,如果是,则将isLastChunk标志设置为true。
if (content instanceof LastHttpContent) {
isLastChunk = true;
}
// 获取content中的ByteBuf(即实际的数据)
ByteBuf buf = content.content();
// 如果undecodedChunk为null,说明这是第一个数据块,那么我们需要创建一个新的缓冲区来存储数据。
if (undecodedChunk == null) {
// 这里使用buf.alloc().buffer(buf.readableBytes())来分配一个与当前可读字节数相同大小的缓冲区,并将buf的数据写入。
// 注意:这里没有使用buf.copy(),因为copy()可能会设置一个maxCapacity,而我们后续可能会添加更多数据,超过这个容量。
undecodedChunk = buf.alloc().buffer(buf.readableBytes()).writeBytes(buf);
} else {
// 如果undecodedChunk不为null,说明已经有数据了,直接将新数据写入到已有的undecodedChunk中。
undecodedChunk.writeBytes(buf);
}
// 调用parseBody()方法来解析已经存储的数据。这个方法会解析HTTP POST请求的主体,包括表单字段和文件上传等。
// 【这个方法是实际解析HTTP POST请求体的核心】
parseBody();
// 解析完成后,检查undecodedChunk是否不为null且其写索引(writerIndex)大于discardThreshold(表示缓冲区中已经写入了很多数据,我们希望通过丢弃已读数据来释放空间。)。
if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
// 如果undecodedChunk的引用计数为1(即只有当前解码器引用这个缓冲区),那么可以安全地调用discardReadBytes()来丢弃已读取的数据,从而释放空间。
if (undecodedChunk.refCnt() == 1) {
undecodedChunk.discardReadBytes();
}
else {
// 否则,说明有多个引用,为了避免影响其他引用,我们创建一个新的缓冲区,只复制未读取的数据,然后释放原来的undecodedChunk,并将新的缓冲区赋值给undecodedChunk。
ByteBuf buffer = undecodedChunk.alloc()
.buffer(undecodedChunk.readableBytes());
buffer.writeBytes(undecodedChunk);
undecodedChunk.release();
undecodedChunk = buffer;
}
}
return this;
}
parseBody()
java
private void parseBody() {
// 这里首先检查当前解析状态是否为 PREEPILOGUE 或 EPILOGUE。这两个状态都表示解析已经进入尾声。
// PREEPILOGUE:表示已经解析到了结束边界,但可能还有尾随数据(epilogue)需要处理。
// EPILOGUE:表示整个解析过程已经完成。
if (currentStatus == MultiPartStatus.PREEPILOGUE
|| currentStatus == MultiPartStatus.EPILOGUE) {
// 如果当前状态已经是上面这两个状态之一
// 如果当前收到的数据块是最后一个(isLastChunk 为 true),则将状态设置为 EPILOGUE,表示解析结束。
if (isLastChunk) {
currentStatus = MultiPartStatus.EPILOGUE;
}
// 然后直接返回,不再执行后续的解析。
return;
}
// 如果当前状态不是 PREEPILOGUE 或 EPILOGUE,则调用 parseBodyAttributes() 方法进行实际的解析。
parseBodyAttributes();
}
parseBodyAttributes()
HttpPostStandardRequestDecoder 中解析普通表单数据(application/x-www-form-urlencoded)的方法。它负责解析像 key1=value1&key2=value2 这样的表单数据。此外,它也处理一些边界情况,比如空值(key1=&key2=value2)以及数据末尾的回车换行。
java
private void parseBodyAttributes() {
// 如果 undecodedChunk 为 null,则直接返回。
if (undecodedChunk == null) {
return;
}
// 如果缓冲区不是基于数组的(例如,可能是直接缓冲区),
if (!undecodedChunk.hasArray()) {
// 则调用另一个方法 parseBodyAttributesStandard() 来处理,并返回。
parseBodyAttributesStandard();
return;
}
// 走到这里,说明缓冲区是基于数组的
// 使用 SeekAheadOptimize 类来优化读取操作,它通过直接访问字节数组来避免多次边界检查,提高性能。
// 代码实现目的与 parseBodyAttributesStandard() 方法一致,直接看上面的这个方法就行了
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
int firstpos = undecodedChunk.readerIndex();
int currentpos = firstpos;
int equalpos;
int ampersandpos;
if (currentStatus == MultiPartStatus.NOTSTARTED) {
currentStatus = MultiPartStatus.DISPOSITION;
}
boolean contRead = true;
try {
loop: while (sao.pos < sao.limit) {
char read = (char) (sao.bytes[sao.pos++] & 0xFF);
currentpos++;
switch (currentStatus) {
case DISPOSITION:
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 == '&') {
currentStatus = MultiPartStatus.DISPOSITION;
ampersandpos = currentpos - 1;
String key = decodeAttribute(
undecodedChunk.toString(firstpos,
ampersandpos - firstpos,
charset),
charset
);
currentAttribute = factory.createAttribute(request, key);
currentAttribute.setValue("");
addHttpData(currentAttribute);
currentAttribute = null;
firstpos = currentpos;
contRead = true;
}
break;
case FIELD:
if (read == '&') {
currentStatus = MultiPartStatus.DISPOSITION;
ampersandpos = currentpos - 1;
setFinalBuffer(
undecodedChunk.retainedSlice(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.retainedSlice(firstpos,
ampersandpos - firstpos)
);
firstpos = currentpos;
contRead = false;
break loop;
} else {
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.retainedSlice(firstpos,
ampersandpos - firstpos)
);
firstpos = currentpos;
contRead = false;
break loop;
}
break;
default:
sao.setReadPosition(0);
contRead = false;
break loop;
}
}
if (isLastChunk && currentAttribute != null) {
ampersandpos = currentpos;
if (ampersandpos > firstpos) {
setFinalBuffer(
undecodedChunk.retainedSlice(firstpos,
ampersandpos - firstpos)
);
} else if (!currentAttribute.isCompleted()) {
setFinalBuffer(Unpooled.EMPTY_BUFFER);
}
firstpos = currentpos;
currentStatus = MultiPartStatus.EPILOGUE;
} else if (contRead
&& currentAttribute != null
&& currentStatus == MultiPartStatus.FIELD) {
currentAttribute.addContent(
undecodedChunk.retainedSlice(firstpos,
currentpos - firstpos),
false
);
firstpos = currentpos;
}
undecodedChunk.readerIndex(firstpos);
} catch (ErrorDataDecoderException e) {
undecodedChunk.readerIndex(firstpos);
throw e;
} catch (IOException e) {
undecodedChunk.readerIndex(firstpos);
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
undecodedChunk.readerIndex(firstpos);
throw new ErrorDataDecoderException(e);
}
}
parseBodyAttributesStandard
java
private void parseBodyAttributesStandard() {
// 记录 待解码的数据缓冲区 的 读索引
int firstpos = undecodedChunk.readerIndex();
// 从 待解码的数据缓冲区 的 读索引 作为当前位置 开始
int currentpos = firstpos;
// 查找 "=" 的索引位置
int equalpos;
// 查找 "&" 的索引位置
int ampersandpos;
// 如果当前状态为未开始,会设置为DISPOSITION。而状态初始化时默认就是 未开始。
if (currentStatus == MultiPartStatus.NOTSTARTED) {
currentStatus = MultiPartStatus.DISPOSITION;
}
// 是否要继续读
boolean contRead = true;
try {
// 如果 待解码的数据缓冲区 有内容 并且 要继续读,则进入循环
while (undecodedChunk.isReadable() && contRead) {
// 读取1个字节
char read = (char) undecodedChunk.readUnsignedByte();
// 当前索引 + 1
currentpos++;
switch (currentStatus) {
// 如果 当前状态 currentStatus 是 DISPOSITION 或 FIELD,就接着在循环里面处理
// 如果 当前状态 currentStatus 不是 DISPOSITION 或 FIELD,就跳出循环
// 如果 当前状态 currentStatus是 DISPOSITION
case DISPOSITION:
// 如果遇到了 "="
if (read == '=') {
// 将 当前状态 currentStatus 切换为 FIELD,表示读完key之后,就要去读key对应的值
currentStatus = MultiPartStatus.FIELD;
// "=" 是在 当前索引 的前1个位置
equalpos = currentpos - 1;
// 对key作 URL 解码
String key = decodeAttribute(
// 获取到 "=" 前面的key
undecodedChunk.toString(firstpos,
equalpos - firstpos,
charset),
charset
);
// 使用HttpDataFactory创建Attribute对象,并将它作为当前处理的Attribute对象
currentAttribute = factory.createAttribute(request, key);
// currentpos 当前索引 即为 "=" 的下1个位置
// 也就是firstPos更新为 "=" 的下1个位置
firstpos = currentpos;
} else if (read == '&') { // 有可能是 abc& 或 直接 &开始
// 当前状态切换为 DISPOSITION
currentStatus = MultiPartStatus.DISPOSITION;
// ampersandpos 更新为 当前位置 - 1,因为 当前位置 已经加过1了
ampersandpos = currentpos - 1;
// 对key作 URL 解码
String key = decodeAttribute(
// 获取 & 前面的key
undecodedChunk.toString(firstpos,
ampersandpos - firstpos,
charset),
charset
);
// 使用HttpDataFactory创建Attribute对象,并将它作为当前处理的Attribute对象
currentAttribute = factory.createAttribute(request, key);
// 显然,值为空字符串
currentAttribute.setValue(""); // empty
// 添加httpData
addHttpData(currentAttribute);
// 将 当前Attribute 重置为null
currentAttribute = null;
// firstPos 更新为 currentpos
firstpos = currentpos;
// 表示继续循环处理
contRead = true;
}
// 如果没遇到 "=" 或 "&",就一直跳过,直到遇到它们,才进入处理
break;
// 如果 当前状态 currentStatus是 FIELD
case FIELD:
// 如果遇到了 &,此时&前面的就是值了
if (read == '&') {
// 将 当前状态 currentStatus 切换为 DISPOSITION,可以下一个key了
currentStatus = MultiPartStatus.DISPOSITION;
// 获取到 & 所在索引位置
ampersandpos = currentpos - 1;
// 将解析的值设置到 当前Attribute对象 中
setFinalBuffer(
// 获取到 key对应的值
undecodedChunk.retainedSlice(firstpos,
ampersandpos - firstpos)
);
// firstPos 更新为 currentpos
firstpos = currentpos;
// 需要继续 往下读
contRead = true;
} else if (read == HttpConstants.CR) { // 如果遇到了回车符
// 如果 当前正在处理的缓冲区中 后面还有内容
if (undecodedChunk.isReadable()) {
// 再去读取下一个字节
read = (char) undecodedChunk.readUnsignedByte();
// 当前位置索引 + 1
currentpos++;
// 如果下一个是换行符
if (read == HttpConstants.LF) {
// 当前状态 切换为 PREEPILOGUE
currentStatus = MultiPartStatus.PREEPILOGUE;
// CR + LF 共2个字节,因此 ampersandpos 值改为 当前位置 - 2
ampersandpos = currentpos - 2;
// 将值设置给当前Attribute对象
setFinalBuffer(
undecodedChunk.retainedSlice(firstpos,
ampersandpos - firstpos)
);
// firstPos 更新为 currentpos
firstpos = currentpos;
// 已经结束了,不需要再循环处理了,因此,将该contRead标记置为false
contRead = false;
}
// 如果下一个字符不是换行符,则会抛出异常
else {
throw new ErrorDataDecoderException("Bad end of line");
}
}
else {
// 如果 当前正在处理的缓冲区中 后面没有内容了,这可能是后面的LF还没到,
// 因此将currentpos - 1,等下次LF到了,再从CR开始处理
currentpos--;
}
} else if (read == HttpConstants.LF) { // 如果遇到了换行符
// 当前状态 切换为 PREEPILOGUE
currentStatus = MultiPartStatus.PREEPILOGUE;
// ampersandpos 值改为 当前位置 - 1
ampersandpos = currentpos - 1;
// 将值设置给当前Attribute对象
setFinalBuffer(
undecodedChunk.retainedSlice(firstpos,
ampersandpos - firstpos)
);
// firstPos 更新为 currentpos
firstpos = currentpos;
// 已经结束了,不需要再循环处理了,因此,将该contRead标记置为false
contRead = false;
}
// 如果没遇到 "&" 或 CR回车符 或 LF换行符,就一直跳过,直到遇到它们,才进入处理
break;
default:
// currentStatus不是 DISPOSITION 或 FIELD,就跳出循环
contRead = false;
}
}
// 结束循环处理后,可能是还有数据未到达,也可能是所有数据都处理完了
// 当这是最后一个数据块(isLastChunk为true)且当前还有未完成的属性(currentAttribute不为null)时
// (当前方法中,只有在遇到&时,才会把currentAttribute设置为null,这里的currentAttribute不为null,就表示还没遇到&,但是已经是最后一个分块了,没办法只能作结束处理了)
if (isLastChunk && currentAttribute != null) {
// 将ampersandpos设置为currentpos(当前读取的位置),即整个数据块的结束位置。
ampersandpos = currentpos;
// 如果ampersandpos大于firstpos,说明从firstpos到ampersandpos之间有数据(即最后一个键值对的值部分),
// 我们就将这些数据设置为最终缓冲区(通过setFinalBuffer方法,该方法会将数据设置到currentAttribute中,并标记 当前Attribute对象 为完成)。
if (ampersandpos > firstpos) {
// 将值设置给 当前Attribute对象,并且将 currentAttribute属性 置为null
setFinalBuffer(
undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos)
);
}
// 已经是最后的分块了,但是未设定值,此时ampersandpos等于firstpos,比如值为: "name="
// 如果currentAttribute还没有完成(!currentAttribute.isCompleted()),但是,
// 当前已经是最后1个分块了,后面没有数据了,因此这里只能被迫结束了
else if (!currentAttribute.isCompleted()) {
// 我们就将一个空的缓冲区(Unpooled.EMPTY_BUFFER)设置为最终缓冲区。
setFinalBuffer(Unpooled.EMPTY_BUFFER);
}
// 将firstpos更新为currentpos
firstpos = currentpos;
// 将状态设置为EPILOGUE(结束状态)
currentStatus = MultiPartStatus.EPILOGUE;
}
// 如果还没有添加最后1个分块,
// 并且contRead仍然是继续读,
// 并且当前Attribute不是null,
// 并且当前正在解析值
else if (contRead
&& currentAttribute != null
&& currentStatus == MultiPartStatus.FIELD) {
// 将当前获取到的内容添加到 当前Attribute对象 中
// (这里也就是在说在未完全获取到值时,也会把部分已经获取到的值,添加到当前Attribute对象中)
currentAttribute.addContent(
undecodedChunk.retainedSlice(firstpos, currentpos - firstpos),
// 表示还没完全接收完
false
);
// 将firstPos 更新为 currentPos
firstpos = currentpos;
}
// 此时,再移动 分块的读索引指针 至 firstpos
// (表示从firstpos索引位置开始往后都未处理)
undecodedChunk.readerIndex(firstpos);
} catch (ErrorDataDecoderException e) {
undecodedChunk.readerIndex(firstpos);
throw e;
} catch (IOException e) {
undecodedChunk.readerIndex(firstpos);
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
undecodedChunk.readerIndex(firstpos);
throw new ErrorDataDecoderException(e);
}
}
setFinalBuffer(ByteBuf)
java
private void setFinalBuffer(ByteBuf buffer) throws IOException {
// 将此值添加到 当前Attribute对象 中,并且内容就是完整的
currentAttribute.addContent(buffer, true);
// URL编码中,特殊字符(如空格、非ASCII字符等)会被编码为%后跟两个十六进制数字。这个方法的作用就是将这些编码的字符解码回原始字符
ByteBuf decodedBuf = decodeAttribute(currentAttribute.getByteBuf(), charset);
// 将URL解码的值设置给 当前Attribute对象
if (decodedBuf != null) {
currentAttribute.setContent(decodedBuf);
}
// 添加httpData
addHttpData(currentAttribute);
// 当前Attribute对象 已解析完毕,重置为null
currentAttribute = null;
}
addHttpData(InterfaceHttpData)
java
protected void addHttpData(InterfaceHttpData data) {
// 将 httpData添加到 bodyMapHttpData 和 bodyListHttpData 中
if (data == null) {
return;
}
List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
if (datas == null) {
datas = new ArrayList<InterfaceHttpData>(1);
// 相同name的 httpData会组成一个列表,放入 bodyMapHttpData
bodyMapHttpData.put(data.getName(), datas);
}
datas.add(data);
// 所有的httpData都会放入 bodyListHttpData
bodyListHttpData.add(data);
}
HttpPostMultipartRequestDecoder
普通multipart报文示例
POST /abc HTTP/1.1
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Cache-Control: no-cache
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
cookie: SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm;SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=--------------------------209491242636242047311642
content-length: 268
----------------------------209491242636242047311642
Content-Disposition: form-data; name="name"
zzhua
----------------------------209491242636242047311642
Content-Disposition: form-data; name="age"
18
----------------------------209491242636242047311642--
java
public class HttpPostMultipartDecTest01 {
public static void main(String[] args) {
DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/multipart1");
request.headers().add("Content-Type", "multipart/form-data; boundary=--------------------------209491242636242047311642");
DefaultHttpDataFactory httpDataFactory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
HttpPostMultipartRequestDecoder multipartDecoder = new HttpPostMultipartRequestDecoder(httpDataFactory, request);
DefaultLastHttpContent lastHttpContent = new DefaultLastHttpContent();
lastHttpContent.content().writeBytes(
("----------------------------209491242636242047311642\r\n" +
"Content-Disposition: form-data; name=\"name\"\r\n" +
"\r\n" +
"zzhua\r\n" +
"----------------------------209491242636242047311642\r\n" +
"Content-Disposition: form-data; name=\"age\"\r\n" +
"\r\n" +
"18\r\n" +
"----------------------------209491242636242047311642--\r\n"
).getBytes(StandardCharsets.UTF_8)
);
multipartDecoder.offer(lastHttpContent);
for (InterfaceHttpData bodyHttpData : multipartDecoder.getBodyHttpDatas()) {
System.out.println("---------------解析请求---------------START");
System.out.println(bodyHttpData.getName());
System.out.println(bodyHttpData.getHttpDataType());
if (bodyHttpData instanceof Attribute) {
try {
System.out.println(((Attribute) bodyHttpData).getValue());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.out.println("---------------解析请求---------------END");
}
}
}
文件上传multipart报文示例
DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
POST /abc HTTP/1.1
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Cache-Control: no-cache
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
cookie: SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm;SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=--------------------------489111531315023450709694
content-length: 563
----------------------------489111531315023450709694
Content-Disposition: form-data; name="name"
zzhua
----------------------------489111531315023450709694
Content-Disposition: form-data; name="age"
18
----------------------------489111531315023450709694
Content-Disposition: form-data; name="myFile"; filename="multipart报文.txt"; filename*=UTF-8''multipart%E6%8A%A5%E6%96%87.txt
Content-Type: text/plain
共3行报文,报文行1
第3行报文,报文行2
第3行报文,报文行3
----------------------------489111531315023450709694--
java
public class HttpPostMultipartDecTest02 {
public static void main(String[] args) {
DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/multipart1");
request.headers().add("Content-Type", "multipart/form-data; boundary=--------------------------489111531315023450709694");
DefaultHttpDataFactory httpDataFactory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
HttpPostMultipartRequestDecoder multipartDecoder = new HttpPostMultipartRequestDecoder(httpDataFactory, request);
DefaultLastHttpContent lastHttpContent = new DefaultLastHttpContent();
lastHttpContent.content().writeBytes(
("----------------------------489111531315023450709694\r\n" +
"Content-Disposition: form-data; name=\"name\"\r\n" +
"\r\n" +
"zzhua\r\n" +
"----------------------------489111531315023450709694\r\n" +
"Content-Disposition: form-data; name=\"age\"\r\n" +
"\r\n" +
"18\r\n" +
"----------------------------489111531315023450709694\r\n" +
"Content-Disposition: form-data; name=\"myFile\"; filename=\"multipart报文.txt\"; filename*=UTF-8''multipart%E6%8A%A5%E6%96%87.txt\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"共3行报文,报文行1\r\n" +
"第3行报文,报文行2\r\n" +
"第3行报文,报文行3\r\n" +
"----------------------------489111531315023450709694--\r\n"
).getBytes(StandardCharsets.UTF_8));
multipartDecoder.offer(lastHttpContent);
for (InterfaceHttpData bodyHttpData : multipartDecoder.getBodyHttpDatas()) {
System.out.println("---------------解析请求---------------START");
System.out.println(bodyHttpData.getName());
System.out.println(bodyHttpData.getHttpDataType());
if (bodyHttpData instanceof Attribute) {
try {
System.out.println(((Attribute) bodyHttpData).getValue());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
else if (bodyHttpData instanceof FileUpload) {
FileUpload fileUpload = (FileUpload) bodyHttpData;
System.out.println(fileUpload.getFilename());
System.out.println(fileUpload.getContentType());
System.out.println(fileUpload.getContentTransferEncoding());
try {
System.out.println(
new String(
fileUpload.get(),
fileUpload.getCharset()
)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.out.println("---------------解析请求---------------END");
}
}
}
属性
java
// 用于创建InterfaceHttpData对象(例如Attribute和FileUpload)
final HttpDataFactory factory;
// 要解码的请求
final HttpRequest request;
// 默认字符集
final Charset charset;
// 是否已经接收到最后一个块
boolean isLastChunk;
// 存储解析出的所有数据
final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
// 按照名字(忽略大小写)存储数据
final Map<String, List<InterfaceHttpData>> bodyMapHttpData
= new TreeMap<String, List<InterfaceHttpData>>(CaseIgnoringComparator.INSTANCE);
// 存储未解码的数据
ByteBuf undecodedChunk;
// 当前在bodyListHttpData中读取的位置,用于迭代
int bodyListHttpDataRank;
// ???
final String multipartDataBoundary;
// ???
String multipartMixedBoundary;
// 当前解析状态
MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
// 当前正在解码的属性
Attribute currentAttribute;
// 解码器是否已被销毁
boolean destroyed;
// 丢弃阈值,用于控制内存使用
int discardThreshold = 10 * 1024 * 1024;
构造方法
java
public HttpPostMultipartRequestDecoder(HttpRequest request) {
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
request,
HttpConstants.DEFAULT_CHARSET);
}
java
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request) {
this(factory, request, HttpConstants.DEFAULT_CHARSET);
}
java
public HttpPostMultipartRequestDecoder(HttpDataFactory factory,
HttpRequest request,
Charset charset) {
this.request = checkNotNull(request, "request");
this.charset = checkNotNull(charset, "charset");
this.factory = checkNotNull(factory, "factory");
String contentTypeValue = this.request.headers().get(HttpHeaderNames.CONTENT_TYPE);
if (contentTypeValue == null) {
throw new ErrorDataDecoderException("No content-type header present.");
}
String[] dataBoundary = HttpPostRequestDecoder.getMultipartDataBoundary(
contentTypeValue
);
if (dataBoundary != null) {
multipartDataBoundary = dataBoundary[0];
if (dataBoundary.length > 1 && dataBoundary[1] != null) {
try {
this.charset = Charset.forName(dataBoundary[1]);
} catch (IllegalCharsetNameException e) {
throw new ErrorDataDecoderException(e);
}
}
} else {
multipartDataBoundary = null;
}
currentStatus = MultiPartStatus.HEADERDELIMITER;
try {
if (request instanceof HttpContent) {
offer((HttpContent) request);
} else {
parseBody();
}
} catch (Throwable e) {
destroy();
PlatformDependent.throwException(e);
}
}