先行一步
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
來獲取 boundary
在 stream
中的結束索引,使用與之前相同的示例輸入
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
!