1. 程式人生 > 其它 >b/s 大檔案分片上傳處理

b/s 大檔案分片上傳處理

對於大檔案的處理,無論是使用者端還是服務端,如果一次性進行讀取傳送、接收都是不可取,很容易導致記憶體問題。所以對於大檔案上傳,採用切塊分段上傳,從上傳的效率來看,利用多執行緒併發上傳能夠達到最大效率。

 本文是基於 springboot + vue 實現的檔案上傳,本文主要介紹服務端實現檔案上傳的步驟及程式碼實現,vue的實現步驟及實現請移步本人的另一篇文章

詳細思路及原始碼

上傳分步:

本人分析上傳總共分為:

  • 檢查檔案是否已上傳,如已上傳可實現秒傳

  • 建立臨時檔案(._tmp)和上傳的配置檔案(.conf)

  • 使用RandomAccessFile獲取臨時檔案

  • 呼叫RandomAccessFile的getChannel()方法,開啟檔案通道 FileChannel

  • 獲取當前是第幾個分塊,計算檔案的最後偏移量

  • 獲取當前檔案分塊的位元組陣列,用於獲取檔案位元組長度

  • 使用檔案通道FileChannel類的 map()方法建立直接位元組緩衝器 MappedByteBuffer

  • 將分塊的位元組陣列放入到當前位置的緩衝區內 mappedByteBuffer.put(byte[] b)

  • 釋放緩衝區

  • 檢查檔案是否全部完成上傳,如上傳完成將臨時檔名為正式檔名

直接上程式碼

public class FlieChunkUtils {
 
    /**
     * 分塊上傳
     * 第一步:獲取RandomAccessFile,隨機訪問檔案類的物件
     * 第二步:呼叫RandomAccessFile的getChannel()方法,開啟檔案通道 FileChannel
     * 第三步:獲取當前是第幾個分塊,計算檔案的最後偏移量
     * 第四步:獲取當前檔案分塊的位元組陣列,用於獲取檔案位元組長度
     * 第五步:使用檔案通道FileChannel類的 map()方法建立直接位元組緩衝器  MappedByteBuffer
     * 第六步:將分塊的位元組陣列放入到當前位置的緩衝區內  mappedByteBuffer.put(byte[] b);
     * 第七步:釋放緩衝區
     * 第八步:檢查檔案是否全部完成上傳
     *
     * @param param
     * @return
     * @throws Exception
     */
    public static ApiResult uploadByMappedByteBuffer(MultipartFileParam param) throws Exception {
        if (param.getIdentifier() == null || "".equals(param.getIdentifier())) {
            param.setIdentifier(UUID.randomUUID().toString());
        }
        // 判斷是否上傳
        if (ObjectUtil.isEmpty(param.getFile())) {
            return checkUploadStatus(param);
        }
        // 檔名稱
        String fileName = getFileName(param);
        // 臨時檔名稱
        String tempFileName = param.getIdentifier() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
        // 獲取檔案路徑
        String filePath = getUploadPath(param);
        // 建立資料夾
        FileUploadUtils.getAbsoluteFile(filePath, fileName);
        // 建立臨時檔案
        File tempFile = new File(filePath, tempFileName);
        //第一步 獲取RandomAccessFile,隨機訪問檔案類的物件
        RandomAccessFile raf = RandomAccessFileUitls.getModelRW(tempFile);
        //第二步 呼叫RandomAccessFile的getChannel()方法,開啟檔案通道 FileChannel
        FileChannel fileChannel = raf.getChannel();
        //第三步 獲取當前是第幾個分塊,計算檔案的最後偏移量
        long offset = (param.getChunkNumber() - 1) * param.getChunkSize();
        //第四步 獲取當前檔案分塊的位元組陣列,用於獲取檔案位元組長度
        byte[] fileData = param.getFile().getBytes();
        //第五步 使用檔案通道FileChannel類的 map()方法建立直接位元組緩衝器  MappedByteBuffer
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        //第六步 將分塊的位元組陣列放入到當前位置的緩衝區內  mappedByteBuffer.put(byte[] b)
        mappedByteBuffer.put(fileData);
        //第七步 釋放緩衝區
        freeMappedByteBuffer(mappedByteBuffer);
        fileChannel.close();
        raf.close();
        //第八步 檢查檔案是否全部完成上傳
        ApiResult result = ApiResult.success();
        boolean isComplete = checkUploadStatus(param, fileName, filePath);
        if (isComplete) {
            // 完成後,臨時檔名為正式檔名
            renameFile(tempFile, fileName);
            result.put("endUpload", true);
        }
 
        result.put("filePath", FileUploadUtils.getPathFileName(filePath, fileName));
        result.put("fileName", param.getFile().getOriginalFilename());
        return result;
    }
 
    /**
     * 檢查檔案是否上傳
     *
     * @param param
     * @return
     * @throws Exception
     */
    public static ApiResult checkUploadStatus(MultipartFileParam param) throws Exception {
        String fileName = getFileName(param);
        // 校驗conf檔案
        File confFile = checkConfFile(fileName, getUploadPath(param));
        // 獲取完成列表
        byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
        Listuploadeds = new ArrayList<>();
        for (int i = 0; i < completeStatusList.length; i++) {
            if (completeStatusList[i] == Byte.MAX_VALUE) {
                uploadeds.add(i + 1 + "");
            }
        }
        ApiResultsuccess = ApiResult.success();
        success.put("uploaded", uploadeds);
        success.put("skipUpload", completeStatusList.length > 0 && completeStatusList.length == uploadeds.size());
        // 新檔案
        if (ObjectUtil.isEmpty(completeStatusList)) {
            success.put("chunk", false);
            return success;
        }
        if (completeStatusList.length < param.getChunkNumber()) {
            success.put("chunk", false);
            return success;
        }
        byte b = completeStatusList[param.getChunkNumber() - 1];
        if (b != Byte.MAX_VALUE) {
            success.put("chunk", false);
            return success;
        }
        success.put("filePath", FileUploadUtils.getPathFileName(getUploadPath(param), fileName));
        success.put("chunk", true);
        return success;
    }
 
    /**
     * 檔案下載
     *
     * @param filePath 檔案地址
     * @param request
     * @param response
     * @throws IOException
     */
    public static void download(String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 初始化 response
        response.reset();
        // 獲取檔案
        File file = new File(getDownloadPath(filePath));
        long fileLength = file.length();
        //獲取從那個位元組開始讀取檔案
        String rangeString = request.getHeader("Range");
        long range = 0;
        if (StrUtil.isNotBlank(rangeString)) {
            range = Long.valueOf(rangeString.substring(rangeString.indexOf("=") + 1, rangeString.indexOf("-")));
        }
        if (range >= fileLength) {
            throw new CustomException("檔案讀取長度過長");
        }
        long byteLength = 1024 * 1024;
        if (range + byteLength > fileLength) {
            byteLength = fileLength;
        }
        // 隨機讀檔案RandomAccessFile
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        try {
            // 移動訪問指標到指定位置
            randomAccessFile.seek(range);
            // 每次請求只返回1MB的視訊流
            byte[] bytes = new byte[(int) byteLength];
            int len = randomAccessFile.read(bytes);
            //獲取響應的輸出流
            OutputStream outputStream = response.getOutputStream();
            //返回碼需要為206,代表只處理了部分請求,響應了部分資料
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            //設定此次相應返回的資料長度
            response.setContentLength(len);
            //設定此次相應返回的資料範圍
            response.setHeader("Content-Range", "bytes " + range + "-" + len + "/" + fileLength);
            // 將這1MB的視訊流響應給客戶端
            outputStream.write(bytes, 0, len);
            outputStream.close();
            //randomAccessFile.close();
            System.out.println("返回資料區間:【" + range + "-" + (range + len) + "】");
        } finally {
            randomAccessFile.close();
        }
    }
 
    /**
     * 檔案重新命名
     *
     * @param toBeRenamed   將要修改名字的檔案
     * @param toFileNewName 新的名字
     * @return
     */
    private static boolean renameFile(File toBeRenamed, String toFileNewName) {
        //檢查要重新命名的檔案是否存在,是否是檔案
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            return false;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改檔名
        return toBeRenamed.renameTo(newFile);
    }
 
    /**
     * 檢查檔案上傳進度
     *
     * @return
     */
    private static boolean checkUploadStatus(MultipartFileParam param, String fileName, String filePath) throws Exception {
        // 校驗conf檔案
        File confFile = checkConfFile(fileName, filePath);
        // 讀取conf
        RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw");
        //設定檔案長度
        if (confAccessFile.length() != param.getTotalChunks()) {
            confAccessFile.setLength(param.getTotalChunks());
        }
        //設定起始偏移量
        confAccessFile.seek(param.getChunkNumber() - 1);
        //將指定的一個位元組寫入檔案中 127,
        confAccessFile.write(Byte.MAX_VALUE);
        byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
        byte isComplete = Byte.MAX_VALUE;
        //這一段邏輯有點複雜,看的時候思考了好久,建立conf檔案檔案長度為總分片數,每上傳一個分塊即向conf檔案中寫入一個127,那麼沒上傳的位置就是預設的0,已上傳的就是Byte.MAX_VALUE 127
        for (int i = 0; i < completeStatusList.length && isComplete == Byte.MAX_VALUE; i++) {
            // 按位與運算,將&兩邊的數轉為二進位制進行比較,有一個為0結果為0,全為1結果為1  eg.3&5  即 0000 0011 & 0000 0101 = 0000 0001   因此,3&5的值得1。
            isComplete = (byte) (isComplete & completeStatusList[i]);
        }
        if (isComplete == Byte.MAX_VALUE) {
            //如果全部檔案上傳完成,刪除conf檔案
            // FileUtils.deleteFile(confFile.getPath());
            return true;
        }
        return false;
    }
 
 
    /**
     * 在MappedByteBuffer釋放後再對它進行讀操作的話就會引發jvm crash,在併發情況下很容易發生
     * 正在釋放時另一個執行緒正開始讀取,於是crash就發生了。所以為了系統穩定性釋放前一般需要檢 查是否還有執行緒在讀或寫
     *
     * @param mappedByteBuffer
     */
    private static void freeMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Object run() {
                    try {
                        Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]);
                        //可以訪問private的許可權
                        getCleanerMethod.setAccessible(true);
                        //在具有指定引數的 方法物件上呼叫此 方法物件表示的底層方法
                        sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer,
                                new Object[0]);
                        cleaner.clean();
                    } catch (Exception e) {
                        log.error("clean MappedByteBuffer error!!!", e);
                    }
                    return null;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    private static String getFileName(MultipartFileParam param) {
        String extension;
        if (ObjectUtil.isNotEmpty(param.getFile())) {
            // return param.getFile().getOriginalFilename();
            String filename = param.getFile().getOriginalFilename();
            extension = filename.substring(filename.lastIndexOf("."));
            //return  FileUploadUtils.extractFilename(param.getFile());
        } else {
            extension = param.getFilename().substring(param.getFilename().lastIndexOf("."));
            //return DateUtils.datePath() + "/" + IdUtil.fastUUID() + extension;
        }
        return param.getIdentifier() + extension;
    }
 
    private static String getUploadPath(MultipartFileParam param) {
        return FileUploadUtils.getDefaultBaseDir() + "/" + param.getObjectType();
    }
 
    private static String getDownloadPath(String filePath) {
        // 本地資源路徑
        String localPath = WhspConfig.getProfile();
        // 資料庫資源地址
        String loadPath = localPath + StrUtil.subAfter(filePath, Constants.RESOURCE_PREFIX, false);
        return loadPath;
    }
 
    private static File checkConfFile(String fileName, String filePath) throws Exception {
        File confFile = FileUploadUtils.getAbsoluteFile(filePath, fileName + ".conf");
        if (!confFile.exists()) {
            confFile.createNewFile();
        }
        return confFile;
    }
}

到此這篇關於springboot大檔案上傳、分片上傳、斷點續傳、秒傳的實現的文章就介紹到這了

白皮書,  功能介紹,  功能對比,

控制元件原始碼下載:

asp.net原始碼下載jsp-springboot原始碼下載jsp-eclipse原始碼下載jsp-myeclipse原始碼下載php原始碼下載csharp-winform原始碼下載vue-cli原始碼下載c++原始碼下載

測試與配置:

asp.net-測試與配置jsp-eclipse-測試與配置jsp-springboot-測試與配置jsp-myeclipse-mysql-測試與配置php-測試與配置C#(WinFrom)測試與配置

C++-WTL測試與配置

效果展示:

編輯