一. 大文件上傳基礎描述:
各種WEB框架中,對於瀏覽器上傳文件的請求,都有自己的處理對象負責對Http MultiPart協議內容進行解析,並供開發人員調用請求的表單內容。
比如:
Spring 框架中使用類似CommonsMultipartFile對象處理表二進制文件信息。
而.NET 中使用HtmlInputFile/ HttpPostedFile對象處理二進制文件信息。
優點:使用框架內置對象可以很方便的處理來自瀏覽器的MultiPart二進制信息請求,協議分析操作不用開發人員參與。
缺點:其接收數據包過程完全被封閉在框架內置對象中,直到本次請求信息處理(接收)完畢后,才允許開發人員從接口調取表單及文件內容。上傳過程中的進度信息無法訪問,無法上傳大尺寸文件(比如幾百兆以上的大文件二進制信息)。
目標:我們要在JAVA WEB框架中,依靠Filter過濾器的能力,實現不依靠框架內置對象,從瀏覽器請求字節流中解析MultiPart協議,取得本次用戶請求的所有信息,包括多二進制文件信息及其他表單項信息。用戶上傳的文件尺寸將不受限制。而且在傳輸過程中,我們可以實時獲得當前傳輸進度信息。
注:.NET框架中可依靠IHttpModule接口對象達到JAVA框架中Filter的能力,本文不做描述。
本文最終完成圖:
1.1 普通Post請求協議及MultiPart協議
普通POST請求協議,見圖:
Content-Length為請求信息內容的字節長度
最下方紅圈內為本次表單請求信息
MultiPart請求協議,見圖:
Content-Length 為本次請求的內容長度字節,本例729366
Content-Type 為multipart/form-data,二進制多段表單
Boundary為多段表單信息的分隔符,這里為-----------------------------------7dflaxxxxxxxxxxx
最后一段信息中,name="file1",為本文件表單的單元名稱,filename="untitled2.png"為該文件名,content-type: image/png為內容區文件格式
最下方的紅框中為該文件的二進制信息。
由以上兩圖可見,MultiPart與普通的POST在協議結構上有明顯區別,所以我們接下來的工作就是按字節流的方式接收MultiPart請求數據包,並對其進行分析。
1.2 可實時獲取當前傳輸進度信息
由於我們可以從上述的Http頭中獲取本次請求內容區長度,即字節總量。由於我們可以從Filter中按字節單位接收來自瀏覽器的數據包,所以我們也能實時的獲得當前接收字節量。因此我們可以實時的獲得當前傳輸進度百分比,用當前接收的字節量除以接收時間即可獲得當前傳輸率(字節/秒)。
由此,我們可獲得以下傳輸過程信息:
· 本次數據包總字節數
· 當前已接收的字節數
· 本次請求發起時間
· 當前進度節點時間
· 當前進度狀態(初始狀態,接收數據中,接收數據完畢等)
接下來,我們只需把這些進度信息以進度Id做標識(progId),在SERVER端放入Java框架中的一個公有內存區即可,在瀏覽器中我們可使用JS以一定時間間隔訪問SERVER中的某一URL,以進度Id為標識,從SERVER的公有內存區獲得當前請求的進度信息。取得信息后,即可實時操控進度條運行。
在Java框架中,公有內存區為ServletContext對象(例,使用setAttribute方法,以鍵值對的形式將單個用戶進度信息存入HashMap對象)。在.NET框架中,公有內存區為HttpApplicationState對象。
注:向公有內存區(HashMap對象)寫操作時要進行同步鎖控制(synchronized),因為公有內存區可能會產生多用戶(多線程)並發操作的現象。
二. 問題點分析:
2.1 分段接收:
因為一次傳輸的大文件MultiPart數據包,字節數可能會很大(1G甚至以上),為了獲取實時進度信息,以及內存開銷控制,我們需要將接收過程分成多段處理,即將數據包分段循環接收(例:每次循環只接收64K數據,期間即可更新當前的進度信息)。
2.2 完整數據包解析?/部分數據包實時解析?
普通的解析協議方式是,將數據包全部接收后,再進行解析。以下有兩種方式實現。
數據包全部加載入內存:對於大文件的MultiPart數據量來說,這種方式會占用大量內存(比如一個用戶正在上傳1G的數據,那么內存區必須接收到全部1G數據后才能進行解析,如果多用戶同時操作會導致服務器崩潰),這種方式不可用。
數據包全部寫入文件后再加載入內存:只能解決在接收過程中開啟小內存並分段寫入文件,當數據全部寫入文件后,還需要加載入內存中進行整體協議分析,也會突發性導致內存開銷過大,導致服務器崩潰,這種方式也不可取。
我們這里采用的是分段接收,分段解析,分段寫文件的處理方式。當數據包全部接收完畢后我們的整個分析過程也即終止,並得到用戶上傳的文件及其他表單信息結果。這樣我們每次只需要很小的內存區(比如64K)即可完成任務。
但這種方式會面臨本次接收的分段信息內含有多個表單項信息及剩余的不完整表單信息,或本次接收的分段信息實際上不包含任何表單信息,僅僅是大文件二進制信息的一個片段。所以,這種方式在編碼上會帶來一定的復雜度。
情況1:
情況2:
情況3:
三. 源碼解析
3.1 項目構成要點
本次我們采用Spring框架來實現“大文件傳輸”功能,要點設計結構圖如下:
Filter對象:
用於負責接收MultiPart原始數據的Filter,用以在Spring內置對象之前接收用戶請求。需要在Web.xml中進行配置,Web啟動后,該Filter即啟動,當用戶請求到來時需要判斷該MultiPart數據信息是否合法,接收並進行解析。
ServletInputStream/BufferedInputStream對象:
使用以上兩對象,可對本次請求進行按字節流接收。在此可創建比較小的接收緩沖區,依靠BufferedInputStream的read進行分段循環接收。
getBoundarySectFromBuf()函數:
自定義函數,我們需要該函數從分段緩沖區中分析可能包含的多個Form表單信息,或者部分表單信息,或者二進制文件片段信息。對於表單信息分析后填充表單數據結構,對於二進制文件信息需要寫文件。該函數需要完成邊接收邊解析邊寫文件的重要工作。
ProgressInfo對象:
進度信息類,描述了一次上傳請求的進度信息。該對象會用來被客戶端輪詢請求,以獲得當前傳輸大文件過程中的進度信息。
FormPart對象及listFormPart集合:
FormPart對於單個Form表單的描述。listFormPart為本次請求的全部表單描述集合。即供后續代碼調用的全部表單項內容。
Controller層getProgInfo()處理函數:
該函數將接受來自瀏覽器的“獲得進度信息請求”,並從當前ServletContext公共內存區中找到與Progesss ID對應的進度信息對象ProgressInfo,以XML的形式返回給瀏覽器。該函數會被客戶端輪詢請求。
multi-form.jsp頁面:
本次表單的顯示頁面,包含多種表單項(Input,Textarea,File等)。該頁面還將顯示用於本次傳輸的進度條,傳輸狀態,傳輸率等信息。頁面中進度信息將使用js向服務器進行周期性輪詢請求,獲得及顯示。
upload-result.jsp頁面:
用來顯示本次請求的所有表單項信息,包括普通Input表單,及File表單信息。
3.2 重點模塊解析
3.2.1 服務器端:
3.2.2 瀏覽器端:
(本節可參考示例代碼中注釋)
四. 擴展及相關
4.1斷點續傳:
一般常說的斷點續傳是指文件下載的斷點續傳。 即利用HTTP協議中的Content-Range關鍵字(在HTTP Header中),向服務器發請求,服務器接收請求后,查看Content-Range屬性的文件偏移量,從而發送后續文件二進制信息給瀏覽器。比如網絡螞蟻類的下載軟件,即開啟多線程利用Content-Range關鍵字將某個網絡資源分布接收,最終整合保存在本地。
而在WEB中我們所使用的上傳文件斷點續傳功能,大多是需要下載ActiveX控件來實現。即相當於在本地下載了一個應用程序,同服務器間文件傳輸協議也不用使用HTTP協議,可自定義協議完成。
利用存粹的HTTP協議進行上傳文件的斷點續傳目前還比較少,據說利用Ajax 中的Slice方法把本地文件分成多個HTTP包POST給服務器,而服務器需要將這些包接收后並整合來實現。操作方式比較復雜,本人沒嘗試過,有感興趣的朋友可深入探討。
4.2本項目待完善要點:
由於時間倉促,本項目目前只完成了大文件上傳及進度顯示的主要功能。在瀏覽器前端進度信息的動態顯示上,前端使用的JS框架(Ext JS, JQuery)等都需要更深入的支持。
在服務器端,也可以依靠對Filter的配置信息,對文件上傳信息進行核查或過濾,比如不能上傳某些擴展名的文件,文件上傳尺寸控制,另存后的文件名唯一性控制等也都需要更細致的描述。
附件文件列表:
MultiData.txt :一次截獲的全部MultiPart數據包信息
multi-form.jsp:多文件上傳顯示頁面,包括獲取進度信息JS腳本
upload_result.jsp:用於顯示上傳結果的表單項集合頁面
MultiForm.java:主過濾器,Filter。用來處理全部上傳過程。
UploadProgInfo.java:Controller層的Spring Bean對象,用來獲取當前的進度信息。
作者自述:
本人從事十六年WINDOWS應用/游戲/設備/WEB/APP等開發,目前從事Linux,IaaS/PaaS/Docker及CAAS雲平台架構設計及開發。
基於全球開源共享理念,本人會分享更多原創及譯文,讓更多的IT人從中受益,與大家一起進步!
尋找對雲計算,雲平台,容器技術感興趣的伙伴,讓計算資源像水一樣在世界流動~
相關配置參考:http://blog.ncmem.com/wordpress/2019/08/12/javahtml5%e5%ae%9e%e7%8e%b0%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/