天天看點

使用原生Java Web來實作大檔案的上傳BigFileUpload

BigFileUpload

使用原生Java Web來實作大檔案的上傳BigFileUpload

目錄

  • 背景介紹
  • 項目介紹
  • 使用說明
    • 擷取代碼
    • 需要知識點
    • 啟動項目
    • 項目示範
  • 核心講解
  • 功能分析
    • 分塊上傳
    • 秒傳功能
    • 斷點續傳
  • 總結

背景介紹

這個項目是在朋友的一次面試中,面試人提出了一個問題.

我有一個100M的檔案,然後我的寬帶隻有10M,我應該如何處理使用者上傳的檔案?

根據這個問題,我小試牛刀,寫了這個項目.

期間查閱了資料,借鑒了Fourwen的項目的前端架構和md寫法.

再次感謝.

項目介紹

項目采用如下:

  • 上層: Java, JDK8, Tomcat8,
  • 服務端: Jsp, 原生
  • 前端: webuploader, bootstrap, jquery

來進行開發,

針對檔案的上傳,一般可以考慮的功能點有

斷點續傳 在斷網或者在暫停的情況下,能夠在上傳斷點中繼續上傳。

分塊上傳 也是斷點續傳的基礎之一,把大檔案通過前端分塊,然後背景在組在一起。

檔案秒傳 服務中已經有人上傳過檔案,其他人再上傳這個檔案直接記錄并放回成功。

其他功能 下面這些功能歸類到其他,是因為它們基本都是通過WebUploader來實作的,很簡單。

- 多線程上傳 多個線程上傳不同的塊檔案。 
- 檔案進度顯示 顯示檔案的上傳完成情況。 
           

使用說明

擷取代碼

  • GitHub:https://github.com/ck-wizard/BigFileUpload

不會經常更新,下一步會做一個集合公司内部網址的項目.

需要知識點

  • 項目使用nio來進行檔案的讀取和建立
  • 使用原生web來開發,不使用任何架構
  • 使用Apache提供的fileupload來實作上傳資料的擷取
  • 使用Apache提供的codec來實作md5加密
  • 并發的了解

啟動項目

項目示範

功能分析

分塊上傳可以說是我們整個項目的基礎,像斷點續傳、暫停這些都是需要用到分塊。

分塊這塊相對來說比較簡單。前端是采用了webuploader,分塊等基礎功能已經封裝起來,使用友善。

借助webUpload提供給我們的檔案API,前端就顯得異常簡單。

var uploader = WebUploader.create({

        // swf檔案路徑
        swf: '${ctx}/webuploader-0.1.5/Uploader.swf',

        // 檔案接收服務端。
        server: '${ctx}/upload.do',
        //檔案上傳請求的參數表,每次發送都會發送此對象中的參數
        formData: {
            md5: ''
        },

        // 選擇檔案的按鈕。可選。
        // 内部根據目前運作是建立,可能是input元素,也可能是flash.
        pick: '#picker',

        // 不壓縮image, 預設如果是jpeg,檔案上傳前會壓縮一把再上傳!
        resize: false,

        chunked: true, // 分塊
        chunkSize:  *  * , // 位元組 1M分塊
        threads: , //開啟線程
        auto: false,

        // 禁掉全局的拖拽功能。這樣不會出現圖檔拖進頁面的時候,把圖檔打開。
        disableGlobalDnd: true,
        fileNumLimit: ,
        fileSizeLimit:  *  * ,    // 200 M
        fileSingleSizeLimit:  *  *     // 100 M
    });
           

上傳的檔案會被發送到upload.do這個Controller,在裡面的邏輯如下:

  1. 判斷是檔案上傳請求,如果是繼續,否則退出
  2. 使用fileupload jar包解析request請求上傳的基礎資訊
  3. 使用FileUploadBean包裝上傳的基礎資訊.
  4. 拼裝父目錄,校驗是否存在
4.1 不存在就建立
    4.2 存在就進入檢驗
    4.2.1 檢查md5值是否比對, 應該建立資料庫,存儲檔案資訊才是更快 更好的解決辦法.
    4.2.2 若比對直接傳回成功.
    4.2.3 若不成功,删除源檔案再次讀取
           
  1. 寫入該分片資料到指定目錄
寫入規則如下:
    // 0.讀取上傳檔案到數組
    // 1.寫到本地
    // 1.記錄分片數,檢查分片數
    // 2.當對應的md5讀取數量達到對應的檔案後,合并檔案
    // 3.删除臨時檔案
           
  1. 完成

功能分析

分塊上傳

分塊上傳可以說是我們整個項目的基礎,像斷點續傳、暫停這些都是需要用到分塊。

分塊這塊相對來說比較簡單。前端是采用了webuploader,分塊等基礎功能已經封裝起來,使用友善。

借助webUpload提供給我們的檔案API,前端就顯得異常簡單。

var uploader = WebUploader.create({

            // swf檔案路徑
            swf: '${ctx}/webuploader-0.1.5/Uploader.swf',

            // 檔案接收服務端。
            server: '${ctx}/upload.do',
            //檔案上傳請求的參數表,每次發送都會發送此對象中的參數
            formData: {
                md5: ''
            },

            // 選擇檔案的按鈕。可選。
            // 内部根據目前運作是建立,可能是input元素,也可能是flash.
            pick: '#picker',

            // 不壓縮image, 預設如果是jpeg,檔案上傳前會壓縮一把再上傳!
            resize: false,

            chunked: true, // 分塊
            chunkSize:  *  * , // 位元組 1M分塊
            threads: , //開啟線程
            auto: false,

            // 禁掉全局的拖拽功能。這樣不會出現圖檔拖進頁面的時候,把圖檔打開。
            disableGlobalDnd: true,
            fileNumLimit: ,
            fileSizeLimit:  *  * ,    // 200 M
            fileSingleSizeLimit:  *  *     // 100 M
        });
           

伺服器先建立一個md5檔案夾,然後按照上傳的檔案名進行一套規範命名,寫入到一個臨時檔案中.

然後記錄這個臨時檔案.

// 規範命名
    String fileName = param.getName();
    String uploadDirPath = finalDirPath + param.getMd5();
    String tempFileName = fileName + "_" + param.getChunk() + "_tmp";
    Path tmpDir = Paths.get(uploadDirPath);
    // 寫入臨時檔案
    Path path = Paths.get(uploadDirPath, tempFileName);
    byte[] fileData = FileUtils.read(param.getFile(), );
    Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
    FileUtils.authorizationAll(path);
    // 記錄
    FileBean fileBean;
    if(fileMap.containsKey(param.getMd5())) {
        fileBean = fileMap.get(param.getMd5());
    } else {
        fileBean = new FileBean(param.getName(), param.getChunks(), param.getMd5());
        fileMap.put(param.getMd5(), fileBean);
    }
    fileBean.setChunk(param.getChunk());
           

然後當檔案分片都上傳完成後,在把分片合并為一個檔案,并且删除所有臨時檔案.

if(fileBean.isLoadComplate()) {
    // 合并檔案..
    Path realFile = Paths.get(uploadDirPath, fileBean.getName());
    realFile = Files.createFile(realFile);
    // 設定權限
    FileUtils.authorizationAll(realFile);
    for(int i =  ; i < fileBean.getChunks(); i++) {
        // 擷取每個分片
        tempFileName = fileName + "_" + i + "_tmp";
        Path itemPath = Paths.get(uploadDirPath, tempFileName);
        byte[] bytes = Files.readAllBytes(itemPath);
        Files.write(realFile, bytes, StandardOpenOption.APPEND);
        //寫完後删除掉臨時檔案.
        Files.delete(itemPath);
    }
    logger.info("合并檔案{}成功", fileName);
    }
           

秒傳功能

上傳檔案是若發現父目錄已經建立,并且目錄下有上傳的檔案名,那麼進行md5比較,若相同,直接傳回,若不相同,删除目錄檔案,重新上傳.

if (!Files.exists(tmpDir)) {
        Files.createDirectory(tmpDir);
    } else {
        // 檔案夾已存在
        // 1.檢查是否有檔案,有進入2, 沒有進3
        Path localPath = Paths.get(uploadDirPath, fileName);

        // 2.檢查md5值是否比對, 應該建立資料庫,存儲檔案資訊才是更快 更好的解決辦法.
        // 2.1.若比對直接傳回成功.
        // 2.2 若不成功,删除源檔案再次讀取
        if(Files.exists(localPath)) {
            String nowMd5 = DigestUtils.md5Hex(Files.newInputStream(localPath, StandardOpenOption.READ));
            if(StringUtils.equals(param.getMd5(), nowMd5)) {
                // 比較相等,那麼直接傳回成功.
                logger.info("已檢測到重複檔案{},并且比較md5相等,已直接傳回", fileName);
                return;
            } else {
                // 删除
                logger.info("已經存在的檔案的md5不比對上傳上來的檔案的md5,删除後重新下載下傳");
                Files.delete(localPath);
            }
        }
        // 3. 直接寫入到具體目錄下.
    }
           

斷點續傳

斷點續傳,就是在檔案上傳的過程中發生了中斷,人為因素(暫停)或者不可抗力(斷網或者網絡差)導緻了檔案上傳到一半失敗了。然後在環境恢複的時候,重新上傳該檔案,而不至于是從新開始上傳的。

檔案上傳時,擷取分片大小,同伺服器目錄存儲的分片大小進行比較,若一直,直接傳回成功.

//寫入該分片資料
    Path path = Paths.get(uploadDirPath, tempFileName);
    //檔案上傳時,擷取是否有分片,如果有直接傳回.
    if(!Files.exists(path)) {
        // 不存在
        byte[] fileData = FileUtils.read(param.getFile(), );
        try {
            Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
        } catch (IOException e) {
            // 删除上傳的檔案
            Files.delete(path);
            throw e;
        }
        FileUtils.authorizationAll(path);
    } else {
        return;
    }
           

總結

選擇使用原生是為了鍛煉自己不要忘記基礎,前前後後寫了3天,複習了不少檔案相關的操作,并且對lambda表達式和流

有了進一步了解,還是很滿足的.

在并發的情況下進行檔案上傳,在使用一個執行個體的成員變量進行存儲的時候,在方法上面使用synchronized或代碼段加synchronized

或Lock或使用AtomInteger去進行并發操作,都沒能達到正确統計的目的.最後使用ConcurrentHashMap才完成了正确的計數.

由此看來,多線程環境下,我還是個小菜鳥啊. 努力加油了.