周煜瑩,崔巖松,王丹志,陳科良
(北京郵電大學(xué) 電子工程學(xué)院,北京 100876)
在計(jì)算機(jī)網(wǎng)絡(luò)技術(shù)迅猛發(fā)展的今天,文件的上傳是一個(gè)重要的應(yīng)用交互場(chǎng)景. 對(duì)于普通的圖片或word文檔等幾十KB 或者幾MB 的文件上傳,使用Web 的組件即可完成流暢的上傳功能. 通常的文件上傳是一次性獲取整個(gè)文件上傳,傳輸過程簡(jiǎn)單: 第1 步獲取本地文件,第2 步將文件轉(zhuǎn)換成字節(jié)傳輸,第3 步后端接收按順序接收字節(jié)到內(nèi)存中,最后將接收完的字節(jié)保存為文件[1]. 但在大文件傳輸?shù)膽?yīng)用中,例如在郵箱管理系統(tǒng)中上傳大容量的資源壓縮包,在網(wǎng)絡(luò)視頻發(fā)布系統(tǒng)中上傳視頻文件,在線制作電子相冊(cè)時(shí)需要上傳高清圖片,網(wǎng)絡(luò)硬盤服務(wù)系統(tǒng),局域網(wǎng)文件交換系統(tǒng)等[2]. 這些業(yè)務(wù)場(chǎng)景下的大文件傳輸很容易占據(jù)較大的帶寬資源,造成網(wǎng)頁訪問速度降低,也可能導(dǎo)致后端服務(wù)器響應(yīng)超時(shí),前端頁面長(zhǎng)時(shí)間無響應(yīng),甚至卡頓而導(dǎo)致頁面崩潰. 即便能夠上傳成功,用戶需要較長(zhǎng)的等待時(shí)間,在此期間不能刷新頁面,只能等待請(qǐng)求完成. 這些問題嚴(yán)重降低了用戶體驗(yàn),因而大文件上傳一直是Web 應(yīng)用系統(tǒng)的一大痛點(diǎn).
針對(duì)以上問題,本文基于Node.js 邊讀邊寫的流模式傳輸,采用HTML5 的File API 對(duì)上傳的大文件進(jìn)行分片處理,通過上傳速率動(dòng)態(tài)調(diào)整分片大小,同時(shí)充分利用帶寬,結(jié)合多并發(fā)上傳進(jìn)一步縮短上傳時(shí)間,在服務(wù)端檢驗(yàn)所有分片文件上傳完整后,再進(jìn)行文件的合并,有效的提高了大文件的上傳速率,減少了用戶的等待時(shí)長(zhǎng).
目前,基于HTTP 協(xié)議的文件上傳方式有以下幾種:
(1)表單上傳
這是Web 開發(fā)中最常見的上傳方式,使用Form表單的input[type=“input”]打開文件選擇界面,通過POST 方法向指定資源提交表單數(shù)據(jù)[3]. 上傳的文件使用multipart 格式,編碼類型為“multipart/form-data”[4].
(2)無刷新的Ajax 上傳
區(qū)別于表單上傳,使用Ajax 的異步上傳,在提交表單數(shù)據(jù)不需要刷新和跳轉(zhuǎn)頁面. 提交數(shù)據(jù)時(shí),可以使用FormData 對(duì)象模擬表單提交,發(fā)送表單的二進(jìn)制文件內(nèi)容,通過XMLHttpRequest 實(shí)例將參數(shù)提交至服務(wù)端[5].
(3)Flash 上傳
在傳統(tǒng)表單的上傳功能基礎(chǔ)上,Flash 上傳方式在不刷新網(wǎng)頁的條件下,支持多個(gè)文件批量上傳以及顯示上傳進(jìn)度等功能. 它采用Flash 作為中間代理層與服務(wù)端進(jìn)行通信,以此為基礎(chǔ)的SWFUpload、Plupload及Uploadify 等文件上傳插件被廣泛應(yīng)用[6].
(4)第三方組件上傳/插件上傳
插件技術(shù)主要包括ActiveX、Applet 等,雖然可能受限于瀏覽器的安全性設(shè)置,但在學(xué)校及企業(yè)內(nèi)部網(wǎng)站環(huán)境中有一定的使用價(jià)值[7]. 例如ActiveX 組件,在VB 6.0 運(yùn)行環(huán)境下,使用關(guān)鍵的Winsock 控件來建立與服務(wù)端之間的通信,通過Socket 連接發(fā)送文件數(shù)據(jù).文獻(xiàn)[8]對(duì)FileUpload,SWFUpload 及SlickUpload 三種組件的特性進(jìn)行了分析和評(píng)估. FileUpload 控件使用簡(jiǎn)單,但默認(rèn)對(duì)上傳組件的大小有限制,因而需要通過修改配置文件中響應(yīng)時(shí)間和大小的限制實(shí)現(xiàn)大文件的上傳. SWFUpload 作為一個(gè)開源的JavaScript 和Flash庫(kù),它結(jié)合了二者的功能,可以實(shí)現(xiàn)交互性更好的界面展示. Slickupload 是來自國(guó)外的商業(yè)組件,其在局域網(wǎng)的文件上傳中具有良好的表現(xiàn)[8].
Node.js 基于事件驅(qū)動(dòng)的非阻塞I/O 模型,旨在支持能夠管理大量并發(fā)請(qǐng)求的輕量級(jí)服務(wù)器的簡(jiǎn)單而快速的開發(fā)[9]. 受益于V8,Node.js 性能優(yōu)越,運(yùn)行速度快,可以在服務(wù)端運(yùn)行,匿名函數(shù)和閉包的使用使其在語言層面具備了異步、事件編程的特性[10]. 在處理二進(jìn)制數(shù)據(jù)流時(shí),常用的有stream 合并與buffer 合并兩種方式. Node.js 中使用buffer 庫(kù)實(shí)現(xiàn)原始數(shù)據(jù)的存儲(chǔ)方法,數(shù)據(jù)被保存在buffer 的實(shí)例中. Node.js 中的stream流是處理流式數(shù)據(jù)的抽象接口,在處理較大數(shù)據(jù)量的文件時(shí),采用stream 合并比buffer 合并更有優(yōu)勢(shì). Buffer需要一次性將數(shù)據(jù)全部放入內(nèi)存,如果數(shù)據(jù)流較大容易導(dǎo)致速度慢,內(nèi)存爆滿. 流模式合并數(shù)據(jù)則是一邊讀取數(shù)據(jù)一邊進(jìn)行操作,在空間上只占用當(dāng)前處理數(shù)據(jù)區(qū)域的內(nèi)存大小,有效地降低了內(nèi)存的開銷[11]. 同時(shí),對(duì)于傳輸過程中的加密及壓縮處理,stream 流具有更高的擴(kuò)展性. 因此,本文選擇流合并,使用Node.js 的可讀流與可寫流,實(shí)現(xiàn)讀取和寫入同步,提高合并效率.
在HTML5 中提供了一種通過File API 規(guī)范與本地文件進(jìn)行交互的標(biāo)準(zhǔn)方法,它的主要作用是將本地文件以文件對(duì)象的形式提供給 Web 應(yīng)用程序進(jìn)行訪問,為瀏覽器端應(yīng)用程序的開發(fā)提供了無限可能[12].File API 提供了前端處理本地文件的能力,讓圖片預(yù)覽、分塊上傳、拖拽上傳等操作變?yōu)榭赡? 以下是本文所用到的對(duì)象簡(jiǎn)介.
(1)FileList 是一個(gè)由File 對(duì)象組成的類數(shù)組對(duì)象.
(2)File 是FileList 中的一個(gè)對(duì)象,包含文件名稱(name)、大小(size)、類別(type)、修改時(shí)間(lastModified-Date)等基本信息.
(3)FileReader 用來讀取文件的API,將文件讀取到內(nèi)存中,提供將文件讀取為文本、base64 圖片編碼、Buffer 數(shù)據(jù)類型、二進(jìn)制字符串等方法,可以實(shí)現(xiàn)預(yù)覽圖片、計(jì)算MD5 等等操作.
(4)Blob 是一個(gè)二進(jìn)制數(shù)據(jù),File 對(duì)象就繼承自Blob 對(duì)象. 通過slice 方法,可以使二進(jìn)制數(shù)據(jù)按照字節(jié)分塊,返回的對(duì)象中包含了源 Blob 對(duì)象中指定范圍內(nèi)的數(shù)據(jù)[13].
對(duì)分片文件的標(biāo)識(shí)也是整個(gè)文件處理過程中必不可少的一部分. 異步提交的數(shù)據(jù)中必須包含文件的唯一標(biāo)識(shí)來確認(rèn)文件分片的順序,驗(yàn)證是否上傳完畢[14].MD5 生成的hash 碼不可逆,可以作為文件上傳的有效標(biāo)識(shí),這也是實(shí)現(xiàn)文件秒傳的基礎(chǔ). Spark-md5 是基于Javascript 的前端類庫(kù),它基于文件的內(nèi)容生成相應(yīng)hash 值,利用File API 對(duì)文件進(jìn)行分塊之后再進(jìn)行MD5計(jì)算,與傳統(tǒng)的MD5 計(jì)算相比,它的傳輸效率很高,不容易引起瀏覽器卡頓、崩潰等問題.
Node.js 和JavaScript 都是單線程編程模型,HTML5的新特性Web worker 為瀏覽器實(shí)現(xiàn)多線程操作提供了支持. 在文件上傳過程中,多線程操作顯然比單線程更具有優(yōu)勢(shì),且不容易造成阻塞. Web worker 允許在Web 程序中并發(fā)執(zhí)行多個(gè)JavaScript 腳本,每個(gè)腳本執(zhí)行過程都作為一個(gè)線程,各個(gè)線程之間彼此獨(dú)立,由JavaScript 引擎負(fù)責(zé)管理[15]. 線程一旦被創(chuàng)建,可以在主線程調(diào)用worker 線程,通過將消息發(fā)布到代碼指定的事件處理程序.
基于對(duì)大文件上傳常用方法與關(guān)鍵技術(shù)的研究,本文設(shè)計(jì)并實(shí)現(xiàn)了完整的前后端大文件上傳系統(tǒng). 該系統(tǒng)基于HTTP 協(xié)議,利用HTML5 的File API 對(duì)需要上傳的目標(biāo)大文件進(jìn)行分片處理. 同時(shí),充分發(fā)揮CPU多核的性能,創(chuàng)建Web worker 線程計(jì)算和處理分片的文件,避免主線程阻塞. 通過對(duì)分片文件的MD5 校驗(yàn)及標(biāo)記,增加文件傳輸?shù)陌踩? 在此基礎(chǔ)上,通過自適應(yīng)分片結(jié)合多并發(fā)上傳進(jìn)行優(yōu)化,提高了傳輸速率.在服務(wù)端,服務(wù)器接收前端傳輸?shù)姆制募?按分片順序依次存儲(chǔ),當(dāng)收到前端的合并請(qǐng)求,服務(wù)端使用流模式將收到的所有文件切片進(jìn)行合并. 此外,在上傳過的切片列表中進(jìn)行查詢比對(duì),對(duì)已經(jīng)上傳過的相同文件無需再傳,避免重復(fù)上傳. 整個(gè)系統(tǒng)的流程示意圖如圖1 所示.
圖1 系統(tǒng)流程圖
3.2.1 Hash 計(jì)算
為了使服務(wù)端對(duì)已上傳的內(nèi)容進(jìn)行識(shí)別,必須要生成文件和切片的 hash 作為校驗(yàn). 這里使用Web worker 為JavaScript 創(chuàng)造多線程環(huán)境,調(diào)用Worker()構(gòu)造函數(shù),新建一個(gè)名為hash 的worker 線程. 在主線程調(diào)用worker 線程,通過postMessage()函數(shù)傳入文件內(nèi)容切片后得到的數(shù)組fileChunkList,worker 線程利用 FileReader 讀取每個(gè)切片的 ArrayBuffer 并不斷傳入Spark-md5 中,每計(jì)算完一個(gè)切片通過 postMessage 向主線程發(fā)送一個(gè)進(jìn)度事件. 主線程通過onMessage函數(shù)監(jiān)聽子線程消息,待全部文件讀取完成后,子線程將最終的 hash 發(fā)送給主線程. 整個(gè)流程如圖2 所示.
圖2 Web worker 示意圖
3.2.2 自適應(yīng)分片
在實(shí)際的應(yīng)用場(chǎng)景中,所需要上傳的文件大小往往是不固定的,而分塊大小對(duì)文件傳輸有較大影響[16].因此,目前常用的設(shè)置固定大小的分片方法不具有靈活性. 自適應(yīng)分片算法的核心在于,根據(jù)上傳文件時(shí)的網(wǎng)絡(luò)狀況,實(shí)現(xiàn)切片大小的動(dòng)態(tài)調(diào)整. 在當(dāng)前切片文件上傳完成時(shí),通過獲取當(dāng)前切片文件所用上傳時(shí)間來調(diào)整下一個(gè)切片文件的大小,目的是為了每次上傳時(shí)切片大小與當(dāng)前網(wǎng)速相匹配,具有更好的傳輸效率[17].參考TCP 協(xié)議的慢啟動(dòng)策略思想,從分片的小容量文件傳輸開始試探網(wǎng)絡(luò)狀況,根據(jù)實(shí)際測(cè)得結(jié)果動(dòng)態(tài)調(diào)整下一次分片的大小[18]. 比如,如果理想的狀態(tài)下每20 s 上傳一個(gè)文件塊,其初始文件大小為1 MB,實(shí)際計(jì)算的上傳時(shí)間僅為10 s,那么可以動(dòng)態(tài)的調(diào)整下一個(gè)分片的大小為2 MB. 另一種可能是實(shí)際上傳所用時(shí)間為40 s,那么說明當(dāng)前網(wǎng)絡(luò)狀況不足以傳輸1 MB 文件,下一個(gè)文件的分片大小可以改為初始值的一半. 因而,在自適應(yīng)分片算法的計(jì)算方法中,設(shè)置一個(gè)初始切片文件大小為fileChunk,設(shè)置理想的上傳單個(gè)分片所需時(shí)間為ts,實(shí)際上傳過程中每個(gè)切片所用時(shí)間為t,那么當(dāng)前切片的上傳速率rate可以表示為t/ts. 此時(shí)下一切片的文件大小newFileChunk的計(jì)算方式為:
本文參照文獻(xiàn)[4] 的參數(shù),設(shè)置初始文件大小設(shè)為1 MB,理想的參照上傳時(shí)間ts為2 s,實(shí)際上傳中所用時(shí)間t通過new Date().getTime()獲取上傳請(qǐng)求前后的時(shí)間戳,得到當(dāng)前切片上傳時(shí)間. 利用式(1)不斷計(jì)算得到新的下一切片大小,達(dá)到切片大小動(dòng)態(tài)調(diào)整的效果.
切片調(diào)整部分關(guān)鍵代碼摘錄如下:
3.2.3 多并發(fā)上傳
為充分利用網(wǎng)絡(luò)帶寬,采用多并發(fā)的方式進(jìn)行文件上傳. 并發(fā)上傳的并發(fā)數(shù)受瀏覽器支持的最大并發(fā)數(shù)限制,超過這個(gè)值,執(zhí)行過程中的并發(fā)請(qǐng)求需要等待.文獻(xiàn)[7]中采用固定分片大小結(jié)合多并發(fā)上傳,研究得到在雙核處理器條件下,并發(fā)數(shù)為3 時(shí)上傳文件的耗時(shí)出現(xiàn)拐點(diǎn),也即上傳時(shí)間出現(xiàn)明顯的減少. 本文設(shè)置max為最大并發(fā)數(shù),通過while 循環(huán)執(zhí)行并發(fā)請(qǐng)求,設(shè)置counter計(jì)數(shù),當(dāng)max>0 并且當(dāng)前計(jì)數(shù)值小于請(qǐng)求長(zhǎng)度時(shí)進(jìn)入循環(huán)體. 進(jìn)入執(zhí)行循環(huán)max值減少1,每次傳輸完成,釋放并發(fā)通道,以此保證并發(fā)數(shù)在設(shè)定值. 通過對(duì)max取值3 到6 進(jìn)行分別測(cè)試,得到上傳耗時(shí)在max取值為5 時(shí)出現(xiàn)明顯減少. 以此為基礎(chǔ)結(jié)合自適應(yīng)分片,在代碼實(shí)現(xiàn)中設(shè)置并發(fā)數(shù)為5,使得文件的分片大小每5 片為一組進(jìn)行自適應(yīng)大小的變化,實(shí)際耗時(shí)t通過5 個(gè)切片文件的上傳總耗時(shí)求平均得到. 通過這樣的改進(jìn)方法,得到更短的上傳耗時(shí).
多并發(fā)上傳結(jié)合自適應(yīng)分片算法的流程示意圖如圖3 所示.
圖3 流程示意圖
3.3.1 接收切片文件
對(duì)前端傳遞的FormData,服務(wù)端使用multiparty包進(jìn)行處理,創(chuàng)建target 文件夾作為文件上傳的存儲(chǔ)目錄. 前端在發(fā)送每個(gè)切片時(shí)都攜帶了唯一標(biāo)識(shí)hash,服務(wù)端將處理后的分片對(duì)象從臨時(shí)路徑移動(dòng)到切片文件夾中.
3.3.2 合并切片
服務(wù)端接收到來自前端的合并請(qǐng)求后,對(duì)切片所在文件夾下的所有切片進(jìn)行合并. 首先采用sort()方法根據(jù)切片的下標(biāo)進(jìn)行排序,避免從目錄讀取的文件順序發(fā)生錯(cuò)亂[19]. 使用 fs.createWriteStream 生成可寫流,通過fs.createReadStream 生成可讀流,將切片文件夾內(nèi)的切片傳輸?shù)侥繕?biāo)文件夾中并合并. createWriteStream方法的兩個(gè)參數(shù)控制可讀流傳輸?shù)娇蓪懥髦付ǖ奈恢?這樣做能保證在并發(fā)合并多個(gè)可讀流時(shí),不必按照流的順序一個(gè)接一個(gè)傳輸也能使切片傳輸?shù)秸_的位置[20]. 與確定上一個(gè)寫入完成再讀取下一個(gè)流的方式相比,多并發(fā)上傳大大提高了傳輸效率.
3.3.3 文件秒傳
文件hash 值與文件后綴作為目錄,使用fse.exists-Sync 檢測(cè)文件目錄是否存在,如果存在,則將標(biāo)志位置為false,不需要再次上傳. 如果不存在,則將標(biāo)志位置為true. 在此基礎(chǔ)上,文件秒傳的實(shí)現(xiàn)只需要在用戶選擇上傳已存在的相同資源時(shí),直接提示上傳成功. 在前文服務(wù)端驗(yàn)證hash 的基礎(chǔ)上,如果發(fā)現(xiàn)hash 相同的文件,說明該文件資源已經(jīng)上傳,可以直接返回上傳成功.
本文使用文獻(xiàn)[7]中的設(shè)計(jì)方法作為對(duì)照,將固定分片上傳與自適應(yīng)分片上傳的方法進(jìn)行對(duì)比. 選取了3 個(gè)100 MB 以上不同大小的文件進(jìn)行測(cè)試,文獻(xiàn)[7]所用方法測(cè)得的時(shí)間記做原始方法用時(shí),本文提出的方法記做改進(jìn)方法. 原始方法采用固定分片大小2 MB,同時(shí)選擇并發(fā)數(shù)為5 進(jìn)行多并發(fā)上傳; 改進(jìn)方法選擇相同的并發(fā)數(shù),采用改進(jìn)的自適應(yīng)分片算法,以2 MB大小為起始分片大小進(jìn)行上傳. 瀏覽器選擇Chrome,通過控制臺(tái)的網(wǎng)絡(luò)network 面板查看分片的請(qǐng)求狀態(tài)以及實(shí)驗(yàn)結(jié)果. 在同樣的網(wǎng)絡(luò)環(huán)境下,每個(gè)文件采用兩種上傳方式分別進(jìn)行3 次測(cè)試,統(tǒng)計(jì)其平均值作為對(duì)照,測(cè)試結(jié)果如表1 所示.
在文件上傳的測(cè)試過程中,針對(duì)同一文件,再次上傳時(shí),經(jīng)過對(duì)MD5 碼的校驗(yàn),可以直接實(shí)現(xiàn)秒傳,系統(tǒng)彈窗提示上傳成功. 表1 中傳輸時(shí)間的計(jì)算是從開始上傳到服務(wù)端接口合并完成文件的整個(gè)過程. 由表1可以看出,本系統(tǒng)可以支持500 MB 以上大文件的上傳,不同大小的文件上傳所用時(shí)間改進(jìn)方法均少于原始方法耗時(shí),并且隨著源文件大小的增大更大時(shí)比固定分片上傳具有更明顯的上傳時(shí)間優(yōu)勢(shì). 通過后端流合并的方式對(duì)分片文件進(jìn)行合并,得到的上傳文件與源文件一致,MD5 值的唯一標(biāo)識(shí)也保證了文件秒傳的實(shí)現(xiàn),避免對(duì)同一文件的重復(fù)上傳,節(jié)約了時(shí)間成本.
表1 實(shí)驗(yàn)結(jié)果(s)
本文研究并介紹了常用的大文件上傳方法以及存在的問題,對(duì)本系統(tǒng)所用到的關(guān)鍵技術(shù)Node.js 及File API 進(jìn)行了闡述,通過對(duì)前后端上傳過程的具體研究,實(shí)現(xiàn)了基于Node.js 的大文件上傳系統(tǒng),通過對(duì)多并發(fā)上傳與自適應(yīng)切片相結(jié)合的算法,實(shí)現(xiàn)了更具有靈活性和更高傳輸效率的大文件上傳. 同時(shí)針對(duì)大文件的MD5 標(biāo)識(shí)計(jì)算,利用Web worker 多線程計(jì)算的方式有效地避免主線程的卡頓. 該系統(tǒng)靈活度高,適用性強(qiáng),能夠在文件上傳過程中提高上傳效率,提升用戶體驗(yàn).