淺談文件斷點續傳和WebUploader的基本結合



0、寫在前面的話

上篇博客已經是在8月了,期間到底發生了什么,只有我自己知道,反正就是心情特別糟糕,生活狀態工作狀態學習狀態都十分不好,還有心思進取嗎,No!現在狀態好起來了,生活又充滿了希望 :D 

前兩周在寫視頻管理相關的功能,說是要在原來的項目上進行拓展。結果今天領導給我說客戶那邊還沒定,只做技術上研究就行了,不用寫具體功能代碼(我都寫了好嗎?)於是突然時間有騰出來,今天整理一下把內容寫一些。

要努力努力,為了更好的人為了更好的生活。

1、斷點續傳的兩種方式

1.1 RandomAccessFile

客戶端給一個已經上傳的位置標記,然后服務器端就可以在指定的位置進行處理。這個斷點位置的讀取,就要用到RandomAccessFile類,該類不同於InputStream和OutputStream,它既可以對文件進行讀也可以進行寫,兩個重要方法:
  • long getFilePoint():返回文件記錄指針的當前位置,不指定指針的位置默認是0
  • void seek(long pos):設置文件指針偏移,即將文件記錄指針定位到pos位置

至於position位置如何去處理,就看各自的想法了。1)你可以將位置存在瀏覽器(比如localStorage),下次傳輸的時候前端從殘缺位置切割文件 blob.slice() 只傳輸剩余的部分,后端直接接收接着寫入服務器即可;2)也可以前端把文件完整傳輸,同時帶上position參數,由后端通過 RandomAccessFile 在指定位置開始讀取內容即可。

至於客戶端和服務端之間文件的一致性,多使用md5進行校驗。

1.2 分片處理

H5中新增了File API,可以通過使用 slice() 方法生成只有某段文件內容。這個方法就為斷點續傳提供了新的方式,就是分片處理,假設一個文件是100M大小,那么每次傳輸我只需要傳送10M,按序發送10次請求即可。某個分片傳送失敗,那么從這個分片再繼續發送即可,后端則對分片文件進行合並成完整文件。

其實方式和1.1提到的是類似的,不過每次傳輸的數據單位量更大一些,完整文件交給后端進行合並。

2、WebUploader的分片斷點續傳

WebUploader的選項中支持直接開啟分片上傳:
var uploader = WebUploader.Uploader({
    swf: 'path_of_swf/Uploader.swf',

    // 開起分片上傳
    chunked: true,
    // 分片大小,默認5M
    chunkSize: 5242880,
    // 分片出錯后(如網絡原因等造成出錯)的重傳次數
    chunkRetry: 2,
    // 上傳並發數
    threads: 1
});

開啟分片上傳后,插件會自動分片上傳文件,接下來只需要在配置文件跳過和后端處理即可。官方回應在分片發送前會有監聽的事件 uploadBeforeSend,在這個方法的callback里面如果返回的是一個promise,且此promise被reject了,那么此分片就跳過了。( 實際上該方式在自測和咨詢網友時發現,並沒有什么用,即便按照官方說明,分片也沒有跳過,仍然往后端進行了請求發送,同時也附帶有文件
webUploader.on('uploadBeforeSend', function(block, data){
    data.fileMd5 = block.file.wholeMd5;
    var deferred = WebUploader.Deferred();
    var chunk = data.chunk;
    var existChunks = block.file.existChunks;
    //后端返回了已存在分片的數組,這里判斷要發送的分片是否已存在
    if(existChunks && existChunks.indexOf(chunk) != -1) {
        //console.log("分片存在,已跳過:" + chunk);
        deferred.reject();
    } else {
        deferred.resolve();
    }
    return deferred.promise();
});

分片是否存在的判斷,也有不同的方式,一種你可以每次計算分片的md5值發送給后端,如果服務器已存在則跳過,否則就發送;另一種就是只向服務器查詢一次獲取已經存在的分片,然后在瀏覽器端進行比對,但如此需要考慮分片是否並發傳輸,進行相應處理。

我采用的方式是:先對文件進行md5計算,在服務器端創建和md5值同名的文件夾,每次上傳的分片存放在對應文件夾,文件名即分片的序號,比如某文件夾中可能存在文件 0, 1, 2, 3... 前端發送分片前請求后端數據,后端將已經存在的分片名數組返回前端,前端進行跳過處理,同時后端在接收分片也要做是否存在的判斷,已存在的話就不再進行讀寫操作,直到最后分片到達,則進行分片的按序合並即可。

public boolean uploadChunk() throws ChunkUploadException {
    HttpServletRequest request = ServletActionContext.getRequest();
    //封裝源文件信息
    FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
    //獲取同時上傳的文件其他屬性
    Map<String, String> params = getVideoParams(request);

    if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
        throw new ChunkUploadException("文件md5值未傳遞");
    }
    //存放
    File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
    if (!temp.exists()) {
        try {
            VideoUtil.copy(srcFileInfo.getFile(), temp);
        } catch (IOException e) {
            throw new ChunkUploadException("分片上傳失敗: chunkNum" + params.get("chunk"));
        }
    }
    //如果是最后分片
    return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
}

public String upload() {
    boolean isLastChunk = false;
    try {
        isLastChunk = uploadChunk();
    } catch (ChunkUploadException e) {
        e.printStackTrace();
        AjaxSupport.sendFailText(null, e.getMessage());
        return AJAX_RESULT;
    }

    //不是最后的分片,直接返回成功響應
    if (!isLastChunk) {
        AjaxSupport.sendSuccessText("chunk uploaded", "success");
        return AJAX_RESULT;
    } 
    //最后切片
    else {
        HttpServletRequest request = ServletActionContext.getRequest();
        //封裝源文件信息
        FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
        //獲取同時上傳的文件其他屬性
        Map<String, String> params = getVideoParams(request);
        //獲取合並文件的文件名
        String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType();

        //合並文件
        File tempDir = new File(getTempPath(params.get("fileMd5")));
        File[] tempfileArr = tempDir.listFiles();
        File storeFile = new File(getStorePath() + "/" + filename);
        try {
            VideoUtil.merge(tempfileArr, storeFile);
        }
    ...

最后,實際上這種方式斷點續傳仍然存在很多細節沒有考慮,比如多線程,同個瀏覽器兩個tab發送同一文件時如何處理?

3、參考鏈接



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM