领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多距离 Spring Framework 5.3 发布已经有一段时间了。该版本的一个特性是对我们的响应式 Multipart 支持进行了重大改进。在这篇博客文章中,我们将分享一些在开发此功能过程中获得的知识。具体来说,我们将重点关注如何在字节缓冲区流中查找标记。
每当你上传文件时,你的浏览器会将其(以及表单中的其他字段)作为multipart/form-data
消息发送到服务器。这些消息的确切格式在RFC 7578中进行了描述。如果你提交一个简单的表单,其中包含一个名为foo
的单文本字段和一个名为file
的文件选择器,则multipart/form-data
消息看起来像这样:
POST / HTTP/1.1
Host: example.com
Content-Type: multipart/form-data;boundary="boundary" (1)
--boundary (2)
Content-Disposition: form-data; name="foo" (3)
bar
--boundary (4)
Content-Disposition: form-data; name="file"; filename="lorum.txt" (5)
Content-Type: text/plain
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam.
--boundary-- (6)
消息的Content-Type
头包含boundary
参数。
边界用于启动第一部分。它前面是--
。
第一部分包含文本字段foo
的值,这可以从部分头中看到。字段的值是bar
。
边界用于分隔第一部分和第二部分。它前面也是--
。
第二部分包含提交文件的內容,名为lorum.txt
。
消息的结尾由边界指示。它前面和后面都是--
。
multipart/form-data
消息中的边界非常重要。它被指定为Content-Type
头的参数。当前面有两个连字符 (--
) 时,边界表示新部分的开始。当后面也跟着--
时,边界表示消息的结尾。
在传入字节缓冲区流中查找边界是解析 multipart 消息的关键。这样做看起来很简单
private int indexOf(DataBuffer source, byte[] target) {
int max = source.readableByteCount() - target.length + 1;
for (int i = 0; i < max; i++) {
boolean found = true;
for (int j = 0; j < target.length; j++) {
if (source.getByte(i + j) != target[j]) {
found = false;
break;
}
}
if (found) {
return i;
}
}
return -1;
}
但是,有一个复杂之处:边界可能跨越两个缓冲区,而这些缓冲区在响应式环境中可能不会同时到达。例如,给定前面显示的示例 multipart 消息,第一个缓冲区可能包含以下内容:
POST / HTTP/1.1
Host: example.com
Content-Type: multipart/form-data;boundary="boundary"
--boundary
Content-Disposition: form-data; name="foo"
bar
--bou
而下一个缓冲区包含剩余部分:
ndary
Content-Disposition: form-data; name="file"; filename="lorum.txt"
Content-Type: text/plain
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam.
--boundary--
如果我们一次检查一个缓冲区,我们就无法找到这样的分割边界。相反,我们需要跨多个缓冲区查找边界。
解决此问题的一种方法是等到所有缓冲区都接收完毕,将它们连接起来,然后之后再定位边界。下面的示例使用示例流和前面定义的indexOf
方法来做到这一点:
Flux<DataBuffer> stream = Flux.just("foo", "bar", "--boun", "dary", "baz")
.map(s -> factory.wrap(s.getBytes(UTF_8)));
byte[] boundary = "--boundary".getBytes(UTF_8);
Mono<Integer> result = DataBufferUtils.join(stream)
.map(joined -> indexOf(joined, boundary));
StepVerifier.create(result)
.expectNext(6)
.verifyComplete();
使用 Reactor 的StepVerifier
,我们看到边界从索引 6 开始。
这种方法有一个主要缺点:将多个缓冲区连接成一个缓冲区实际上会将整个 multipart 消息存储在内存中。Multipart 消息主要用于上传(大型)文件,因此这不是一个可行的方案。相反,我们需要一种更智能的方法来定位边界。
幸运的是,这种方法以Knuth-Morris-Pratt 算法的形式存在。该算法的主要思想是,如果我们已经匹配了边界的几个字节,但下一个字节不匹配,我们不需要从头开始重新匹配。为此,该算法维护状态,以预计算表中的位置的形式,该表包含不匹配后可以跳过的字节数。
在 Spring Framework 中,我们在Matcher
接口中实现了 Knuth-Morris-Pratt 算法,你可以通过DataBufferUtils::matcher
获取其实例。你也可以查看源代码。
在这里,我们使用Matcher
为我们提供stream
中boundary
的结束索引,使用与前面相同的示例输入:
Flux<DataBuffer> stream = Flux.just("foo", "bar", "--boun", "dary", "baz")
.map(s -> factory.wrap(s.getBytes(UTF_8)));
byte[] boundary = "--boundary".getBytes(UTF_8);
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(boundary);
Flux<Integer> result = stream.map(matcher::match);
StepVerifier.create(result)
.expectNext(-1)
.expectNext(-1)
.expectNext(-1)
.expectNext(3)
.expectNext(-1)
.verifyComplete();
请注意,Knuth-Morris-Pratt 算法给出边界的**结束**索引,这解释了测试结果:边界直到倒数第二个缓冲区的索引 3 处才结束。
正如预期的那样,Spring Framework 的MultipartParser
大量使用了Matcher
,用于:
如果你需要在一个字节缓冲区流中查找一系列字节,请尝试使用Matcher
!