很多人应该对 HTTP 的 multipart/form-data 不陌生,一般常用于上传文件,虽然大部分人可以很熟练的使用,但是对于 multipart/form-data 如何对请求编码的却知之甚少,这篇博客就来简单介绍下其中的编码过程以及 Netty 的实现。

首先我们使用 Postman 发送一个 HTTP multipart/form-data 类型的请求,通过抓包发现请求内容。

Request Header

Request Header:

Request Header

Request Body :

Request Header

Request Body 完成内容:

Request Header

从第二个图中可以看出,Reqeust Body 内容是由 Content-Type 中的 boundary 所分隔,内容中的 Content-Disposition 指定 form 的 key 和上传的文件名,Content-Type 指定文件类型,接着一个换行,后面就是文件的内容,同样,服务器在接受到请求后,也会按照这种格式来进行解码。首先来看看 Netty 是如何构造 multipart 请求内容的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// setup the factory: here using a mixed memory/disk based on size threshold
// Disk if MINSIZE exceed
HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
// setup an http request
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "http://127.0.0.1:1028/test-file");
// Use the PostBody encoder
HttpPostRequestEncoder bodyRequestEncoder = new HttpPostRequestEncoder(factory, request, true, HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738));
File file = new File("test.txt");
// add Form attribute
bodyRequestEncoder.addBodyFileUpload("file", file, "text/plain", true);
// finalize request
bodyRequestEncoder.finalizeRequest();
// send request
channel.write(request);

// test if request was chunked and if so, finish the write
if (bodyRequestEncoder.isChunked()) {
channel.write(bodyRequestEncoder);
}
channel.flush();

// Now no more use of file representation (and list of HttpData)
bodyRequestEncoder.cleanFiles();

其中最关键的地方是第 7 行 和 10 行代码。

HttpPostRequestEncoder 在初始化时会根据传入的 multipart 是否为 true 来构造对应的 RequestEncoder:

1
2
3
4
5
6
7
8
9
10
public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset, EncoderMode encoderMode) throws ErrorDataEncoderException {
// 省略部分无关代码
isMultipart = multipart;
multipartHttpDatas = new ArrayList<InterfaceHttpData>();
this.encoderMode = encoderMode;
if (isMultipart) {
// 生成 multipartDataBoundary
initDataMultipart();
}
}

当我们传入 multipart 为 true 时,HttpPostRequestEncoder 就会生成一个唯一的 multipart boundary,而这个 boundary 就是之前图中的分割界线。

接着来看 HttpPostRequestEncoder.addBodyFileUpload() 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void addBodyFileUpload(String name, File file, String contentType, boolean isText) throws ErrorDataEncoderException {
// 省略部分无关代码
FileUpload fileUpload = factory.createFileUpload(request, name, file.getName(), scontentType,
contentTransferEncoding, null, file.length());
try {
// 读取 file 中的内容,保存至 FileUpload 中
fileUpload.setContent(file);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
// fileUpload 添加到 bodyListDatas 中
addBodyHttpData(fileUpload);
}

通过 HttpDataFactory 构造 FileUpload 后,添加至 bodyListDatas 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException {

bodyListDatas.add(data);
/*
* Logic:
* if not Attribute:
* add Data to body list
* if (duringMixedMode)
* add endmixedmultipart delimiter
* currentFileUpload = null
* duringMixedMode = false;
* add multipart delimiter, multipart body header and Data to multipart list
* reset currentFileUpload, duringMixedMode
* if FileUpload: take care of multiple file for one field => mixed mode
* if (duringMixeMode)
* if (currentFileUpload.name == data.name)
* add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
* else
* add endmixedmultipart delimiter, multipart body header and Data to multipart list
* currentFileUpload = data
* duringMixedMode = false;
* else
* if (currentFileUpload.name == data.name)
* change multipart body header of previous file into multipart list to
* mixedmultipart start, mixedmultipart body header
* add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
* duringMixedMode = true
* else
* add multipart delimiter, multipart body header and Data to multipart list
* currentFileUpload = data
* duringMixedMode = false;
* Do not add last delimiter! Could be:
* if duringmixedmode: endmixedmultipart + endmultipart
* else only endmultipart
*/
if (data instanceof FileUpload) {
FileUpload fileUpload = (FileUpload) data;
InternalAttribute internal = new InternalAttribute(charset);
if (!multipartHttpDatas.isEmpty()) {
// previously a data field so CRLF
internal.addValue("\r\n");
}
// 添加 multipart boundary
internal.addValue("--" + multipartDataBoundary + "\r\n");
// Content-Disposition: form-data; name="files";
// filename="file1.txt"
// 这部分就是上文途中看到的 Content-Disposition
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
+ HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; "
+ HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
// Add Content-Length: xxx
internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
fileUpload.length() + "\r\n");
// Content-Type: image/gif
// Content-Type: text/plain; charset=ISO-8859-1
// Content-Transfer-Encoding: binary
internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType());
String contentTransferEncoding = fileUpload.getContentTransferEncoding();
if (contentTransferEncoding != null
&& contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": "
+ HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() + "\r\n\r\n");
} else if (fileUpload.getCharset() != null) {
internal.addValue("; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n");
} else {
internal.addValue("\r\n\r\n");
}
// 先讲 attribute 添加至 multipartHttpDatas 中
// 再将 fileUpload 添加址 multipartHttpDatas 中
// 主要是需要将 fileUpload 的内容保存在 boundary 和 fileUpload 的 attribute 之后
// 完整内容构造会在 finalizeRequest() 中生成
multipartHttpDatas.add(internal);
multipartHttpDatas.add(data);
globalBodySize += fileUpload.length() + internal.size();
}
}

只保留了一些关键代码,删除了 attribute 和 mix 相关代码。

根据 Netty 原有的注释以及我后面补充的注释,已经有了非常完整的 FileUpload 添加过程,接下来看 finalizeRequest() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
if (isMultipart) {
// 在 header 中设置 Content-Type
String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '='
+ multipartDataBoundary;
headers.add(HttpHeaderNames.CONTENT_TYPE, value);
}
if (isMultipart) {
// 获取 iterator ,会在 nextChunk 中使用
iterator = multipartHttpDatas.listIterator();
}

// 省略部分无关代码
}

finalizeRequest 除了设置 Content-Type 外还构造了 ListIterator\ iterator ,而实际的数据内容则是遍历 iterator 来读取的。

1
2
3
4
5
6
7
8
9
10
11
private HttpContent nextChunk() throws ErrorDataEncoderException {
while (size > 0 && iterator.hasNext()) {
currentData = iterator.next();
HttpContent chunk;
if (isMultipart) {
chunk = encodeNextChunkMultipart(size);
}
// NextChunk from data
return chunk;
}
}

在数据传输时,通过 nextChunk 中遍历 iterator 获取每一个 InterfaceHttpData 的内容并传输到对应的 server 中。

至此 HTTP multipart/form-data 及 Netty 中的实现基本分析完毕,错误之处欢迎指出。