tomcat上傳文件超過大小限制 tomcat修改上傳文件大小限制



文章插圖
tomcat上傳文件超過大小限制 tomcat修改上傳文件大小限制

文章插圖
前言
這兩天在另一個社區看到了一個關于 Tomcat 的提問,還挺有意思 。正好自己之前也沒思考過這個問題,今天就結合 Tomcat 機制來聊聊這個“為什么” 。
本文對 HTTP 協議中的文件上傳標準和 Tomcat 機制的分析內容較多,比較基礎,不需要的大佬們可以直接跳到文末 。
HTTP 協議中的文件上傳
眾所周知,HTTP 是一個文本協議,那文本協議如何傳輸文件呢?
直接傳……是的就這么簡單 。文本協議只是在應用層的角度,到了傳輸層都是數據都是字節,沒什么區別,并不用進行額外的編解碼 。
multipart/form-data 方式
HTTP 協議中規定了一種基于表單的文件上傳方式(Form-based File Upload) 。在 form 中定義一個 ENCTYPE 屬性,值為 multipart/form-data,然后增加一個 type 為 file 的 <input> 標簽 。
<FORM ENCTYPE="multipart/form-data" ACTION="_URL_" METHOD=POST>File to process: <INPUT NAME="userfile1" TYPE="file"><INPUT TYPE="submit" VALUE="http://www.mnbkw.com/jxjc/168188/Send File"> </FORM>這個 multipart/form-data 類型的表單和默認的 x-www-form-urlencoded 有些不同 。雖然都作為表單,可以上傳多個字段,但前者可以上傳文件,后者卻只能傳輸文本
現在來看看這個表單文件上傳方式的協議,下圖是一個簡單的 multipart/form-data 類型的請求報文:
從上圖可以看到,HTTP header 部分沒什么變化,只是 Content-Type 中增加了一段 boundary 標簽,但 payload 部分卻完全不同
boundary 在 multipart/form-data 中作用是分隔表單的多個字段,在 payload 部分中,首尾兩行各有一個 boundary,每個字段(part/item)之間也會有一個 boundary
Server 端在讀取時,只需要先從 Content-Type 中拿到 boundary,然后通過這個 boundary 去拆分 payload 部分就可以獲取所有的字段 。
每個字段的報文中,有一個 Content-Disposition字段,作為這個字段的 Header 部分 。其中記錄了當前字段名(name),如果是文件的話還會有一個 filename 屬性,同時再下一行會附帶一個 Content-Type 來標識文件的類型
雖然 x-www-form-urlencoded 和 multipart 兩種類型的表單都可以完成字段的傳輸,但 multipart 不僅可以傳輸文本字段,還可以傳輸文件 。而且這個 multipart 傳輸文件的方式也是“標準”的,各種 Server 都可以支持,直接讀取文件 。
而 x-www-form-urlencoded 只可以傳輸基礎的文本數據,不過你要是強行把文件當做文本,用這個類型傳也沒人能攔你,但作為文本傳輸時后端必然用字符串方式解析,byte -> str 時的編碼開銷完全沒必要,而且可能會導致編碼錯誤……
在 x-www-form-urlencoded 類型的報文中,并沒有 boundary,多個字段會通過 & 符號拼接,并且對key/value 都進行 urlencode 編碼
雖然 x-www-form-urlencoded 增加了一步編碼的過程,但不會給每個字段增加header,也沒有 boundary,報文體積相對 multipart 方式來說小了很多 。
除了這個 multipart,還有一種直接上傳文件的形式,不過不太常用
binary payload 方式
除了 multipart/form-data之外,還有一種 binary payload 的上傳方式 。這個 binary payload 是我自己起的名字……因為在 HTTP 協議中并沒有找到這種方式的說明(如果有找到的大佬評論區貼個連接),不過很多 HTTP 客戶端都支持 。
比如 Postman:
比如 OkHttp:
OkHttpClient client = new OkHttpClient().newBuilder().build();MediaType mediaType = MediaType.parse("image/png");RequestBody body = RequestBody.create(mediaType, "<file contents here>");Request request = new Request.Builder().url("localhost:8098/upload").method("POST", body).addHeader("Content-Type", "image/png").build();Response response = client.newCall(request).execute();這種方式非常簡單,就是將整個 payload 部分,都用來存放文件數據 。如下圖所示,整個 payload 部分都是文件內容:
這種方式雖然簡單,客戶端實現也簡單,但……服務端沒有很好的支持 。比如 Tomcat 中,并不會將這種 binary file 的形式作為文件處理,而是當做普通的報文處理 。
Tomcat 處理機制分析
Tomcat 在處理文本形式的報文時,會先讀取前面的 Header 部分,解析 Content-Length 來劃分報文邊界,剩下的 Payload 部分并不會一次性讀取,而是包裝了一個 InputStream,在內部調用 Socket read 進行讀取 RCV_BUF 的數據(完整報文大小大于 readBuf Size時)
對 HttpServletRequest 調用getParameter/getInputStream 等涉及 Payload 部分讀取操作時,就會進行InputStream 內部的 Socket RCV_BUF 的讀取,讀取 Payload 的數據 。
這種不一次性讀取所有數據暫存至內存中的方式,而包裝一個 InputStream 內部讀取 RCV_BUF 的方式,特點是不存儲數據,只是做一個包裝,應用層對 ServletRequest#inputStream 的 read 操作會轉發到對 Socket RCV_BUF 的read 。
不過如果應用層完整的讀取了 ServletRequest#inputStream,然后轉字符串,存儲至內存中的話,那這就和 Tomcat 沒什么關系了 。
對于 multipart 類型的請求,Tomcat 處理機制上比較特殊 。由于 multipart 是為了傳輸文件而設計的,所以在處理這種類型請求時,Tomcat 增加了一個暫存文件的概念,在解析報文時,將 multipart 中的數據寫入到了磁盤中 。
如下圖所示,Tomcat 對每一個字段都包裝為一個 DiskFileItem –org.apache.tomcat.util.http.fileupload.disk.DiskFileItem(這個 DiskFileItem 不區分是文件還是文本數據) 。DiskFileItem 內又分為 Header 部分和 Content 部分 。Content 中一部分存儲在內存,剩下的存儲至磁盤,通過一個 sizeThreshold 進行分割;不過這個值默認為0,也就是說默認會把內容部分全部存儲至磁盤 。
那既然存儲至磁盤,讀取時也肯定也是從磁盤讀取了……效率自然是比較低的 。所以如果只是文本型的報文,還是不要用 multipart 類型來傳輸了,這個類型會被轉存磁盤的 。
還有一個冷知識,Tomcat 在處理 multipart 類型的報文時,如果某個字段不是文件,會將這個字段的key/value 添加到 parameterMap 中,也就是說通過request.getParameter/getParameterMap 可以獲取到這些非文件的字段 。
//org.apache.catalina.connector.Request#parsePartsif (part.getSubmittedFileName() == null) {String name = part.getName();String value = http://www.mnbkw.com/jxjc/168188/null;try {value = part.getString(charset.name());} catch (UnsupportedEncodingException uee) {// Not possible}......parameters.addParameter(name, value);}要知道這個 getParameter 是只能獲取表單參數(FormParam)和查詢參數(QueryString)的,不過 multipart 也是 form,能獲取參數好像也沒啥毛病……
一個簡單的小結
Tomcat 對不同類型的請求處理方式:
如果參數是 GET queryString方式(url上拼參數),那么所有參數都在報文頭中,會一次性全部讀取至內存如果是 POST 類型的報文,Tomcat 只會對讀取 Header 部分,Payload 部分不會主動讀取,而是將 Socket 包裝成一個 InputStream 供應用層 readx-www-form-urlencoded 這種類型的報文,雖然不會主動讀取,但很多 Web 框架(比如 SpringMVC)會調用 getParameter,還是會出發 InputStream 的read,對 RCV_BUF 進行讀取上面提到的 binary payload也是一樣,Tomcat 并不會主動發起 read 操作,需要應用層調用 ServletRequest#InputStream 進行 read操作讀取 RCV_BUF 的數據multipart 類型的報文,一樣不會主動讀取,調用HttpServletRequest#getParts 才會觸發解析/讀??;同樣的,很多 Web 框架會調用 getParts,所以會觸發解析為什么要先寫入臨時文件,直接包裝 InputStream 交給應用層讀取不行嗎?
如果應用層不(及時)讀取 RCV_BUF,那么當收到的數據寫滿 RCV_BUF 時,就不會再返回 ACK 了,客戶端的的數據也會存儲在 SND_BUF 中,無法繼續發送數據,當 SND_BUF 被應用層寫滿時,這條連接就被阻塞了 。
【tomcat上傳文件超過大小限制 tomcat修改上傳文件大小限制】由于 multipart 一般是用于傳輸文件,但文件大小通常會遠大于 Socket Buffer 的容量 。所以,為了不阻塞 TCP 連接,Tomcat 會一次性讀取完整的 Payload 部分,然后將其中所有的 Part 存儲至磁盤(Header在內存中,內容在磁盤) 。
應用層只需要再從 Tomcat 提供的 DiskFileItem 讀取 Part 數據即可,這樣看起來雖然中轉了一層,但 RCV_BUF 中的數據卻可以被及時消費了 。
從效率上說,中轉+存磁盤這種操作,一定比不中轉要慢的多,不過可以及時消費 RCV_BUF,保證 TCP 連接不被阻塞 。
如果是在 HTTP2 的多路復用下,多個請求都使用同一個 TCP 連接,如果 RCV_BUF 沒有及時消費,那么還會導致所有的“邏輯 HTTP 連接”都阻塞
那為什么其他類型的報文不用暫存磁盤呢?
因為報文小啊,普通的請求報文不會太大的,常見的也就幾K 到幾十K,而且對于純文本報文來說,讀取操作一定也是及時的且一次性全部讀取的,而 multipart 這種形式的報文不同,它是文本+文件混合的方式,而且還可能是多文件 。
比如服務端在接收到文件后,還需要對文件進行轉存,轉存到某些云廠商的對象存儲服務中,那么此時有兩種轉存方式:
接收到完整文件數據,存儲至內存中,然后調用對象存儲的SDK用流的方式,一邊 read ServletRequest#InputStream,一邊 write 到 SDK 的 OutputStream 中
方式 1,雖然及時讀取了 RCV_BUF,但是內存占用過大,很容易把內存撐爆,非常不合理 方式 2,雖然內存占用很小(最多只有一個 Read Buffer 的大?。捎谑沁呑x邊寫,兩邊都是網絡,會導致 RCV_BUF 不能及時消費完成 。
而且不光是 Tomcat,連 Jetty 也是這么處理 multipart,其他 Web Server 雖然沒看,但我想應該都會這么處理 。