[천 단어 길이의 텍스트] Vue+SpringBoot는 대용량 파일 2차 전송, 중단점 재개 전송 및 조각 업로드 완료 튜토리얼을 실현합니다. (Gitee 소스 코드 제공)

서문: 최근 실제 프로젝트에서 요구사항이 발생했는데, 고객이 상대적으로 큰 파일을 업로드하는 경우가 있는데, 기존의 파일 업로드 솔루션을 사용할 경우 높은 서버 압력, 리소스 낭비, 메모리 오버플로 등의 보안 위험이 있을 수 있습니다. 일련의 문제를 해결하기 위해서는 대용량 파일 업로드를 실현하기 위한 새로운 기술 솔루션이 필요하다는 것인데, 시간이 나면 인터넷에서 관련 튜토리얼을 참조하고 마지막으로 직접 요약합니다. 이 블로그에서, 대용량 파일을 구현하는 방법을 단계별로 설명하겠습니다. 두 번째 전송, 중단점 재개 전송 및 조각화의 세 가지 기능에 대해 각 코드를 설명합니다. 블로그 마지막 부분에서 Gitee 소스 코드를 모두에게 제공하겠습니다. 다운로드.

목차

1. 이 기술 솔루션을 사용하는 이유 

2. 즉시전송이란?

3. 중단점에서 재개란 무엇입니까? 

4. 멀티파트 업로드란 무엇입니까?

5. 업로드 과정

6. SpringBoot 프로젝트 빌드

6.1 준비사항 

6.1.1 pom 종속성 가져오기

6.1.2, yml 구성 파일

6.1.3, mybatis-config.xml 구성 파일

6.1.4, SQL 파일

6.2, 상수 클래스 

6.2.1, HttpStatus 공통 반환 상태 코드 상수

6.3, 엔터티 클래스

6.3.1, AjaxResult 통합 결과 캡슐화

6.3.2, 파일 슬라이스 클래스

6.3.3, 파일 정보 클래스

6.4, GlobalCorsConfig 글로벌 CORS 교차 도메인 구성

6.5, 지속성 레이어

6.5.1, FileChunkMapper.xml 파일

6.5.2, FileChunkMapper 파일

6.5.3, FileInfoMapper.xml 파일

6.5.4, FileInfoMapper 파일

6.6 파일 업로드 핵심 서비스 클래스(중요)

6.6.1, 파일 업로드 확인 로직

6.6.2 조각 업로드의 핵심 논리 실현

6.6.3 파일 조각화 처리

6.6.4, 전체 코드

6.7, FileController 요청 계층

7. Vue 프로젝트 빌드

7.1 준비사항

7.2, 홈뷰 페이지

7.2.1 업로더 구성요소 사용

7.2.2 데이터 데이터 초기화

7.2.3, checkChunkUploadedByResponse 메소드(중요)

7.2.4,parseTimeRemaining 메소드

7.2.5 기타 매개변수

7.2.6, onFileAdded 메소드(중요)

7.2.7, getFileMD5 메소드(중요)

7.2.8, 전체 코드

여덟째, 프로젝트를 실행하라

8.1 분할 업로드

8.2 중단점 재개 

8.3 두 번째 패스 

9. Gitee 소스코드 주소

10. 요약


1. 이 기술 솔루션을 사용하는 이유 

프런트 엔드가 매우 큰 파일(예: 1G)을 한 번에 업로드하고 조각화/중단점 재개 전송과 같은 기술 솔루션을 채택하지 않으면 주로 다음과 같은 숨겨진 위험이나 문제에 직면하게 됩니다.

1. 네트워크 전송 속도가 느리다

업로드 시간이 길고 파일의 최종 완전 업로드는 지속적이고 안정적인 업스트림 대역폭을 차지해야 하며, 네트워크 상태가 좋지 않으면 업로드 속도가 매우 느려져 사용자 경험이 손상됩니다.

2. 중간에 실패할 경우 재업로드가 필요합니다.

네트워크 등의 이유로 업로드 프로세스가 중단되면 전체 전송이 실패합니다. 이를 위해서는 사용자가 전체 파일을 다시 업로드하고 작업을 반복하면 됩니다.

3. 서버의 압박이 심합니다.

서버는 대용량 파일을 지속적으로 처리하기 위해 많은 리소스를 점유해야 하므로 서버 성능에 많은 부담을 주고 다른 서비스에 영향을 미칠 수 있습니다.

4. 교통자원 낭비

대용량 파일을 한 번에 완전히 업로드하는 경우, 동일한 파일이 이미 존재하는 경우 반복적으로 많은 양의 네트워크 트래픽을 소비하게 되어 데이터 낭비가 됩니다.

5. 업로드 진행 프롬프트를 구현하기가 어렵습니다.

사용자는 업로드 진행 상황을 인지할 수 없으며, 업로드가 실패할 경우 얼마나 많은 데이터가 업로드되었는지 알 수 없습니다.

따라서 이러한 문제를 해결하기 위해서는 단편화, 전송 재개 등의 기술을 활용하는 것이 매우 중요하다. 데이터 블록을 일괄적으로 업로드할 수 있어 한꺼번에 업로드하는 단점을 피할 수 있습니다. 동시에 업로드된 조각의 확인 및 기록과 같은 방법과 결합하면 전체 업로드 프로세스를 제어 및 복구할 수 있고 트래픽을 절약할 수 있으며 전송 효율성을 크게 향상시킬 수 있습니다.

2. 즉시전송이란?

나는 이 프로젝트를 사용하여 Miaochuan의 구현 논리를 이해하기 쉽게 설명할 것입니다.

1. 클라이언트 Vue가 파일을 업로드할 때 먼저 파일의 MD5 값을 계산한 다음 MD5 값을 SpringBoot 서버 로 보냅니다.

2. SpringBoot 서버는 MD5 값을 수신한 후 MyBatis를 사용하여 MySQL 데이터베이스 에 쿼리하여 동일한 MD5 값을 가진 파일이 이미 존재하는지 확인합니다.

3. 존재한다면 파일이 업로드 되었다는 의미이며, 서버는 해당 파일에 어떤 조각이 존재하는지 데이터베이스로부터 직접 조회하여 클라이언트에게 반환한다.

4. 클라이언트는 파일 조각화 정보를 얻은 후 실제 파일 콘텐츠를 업로드하지 않고 전체 파일을 직접 조립합니다.

5. MD5 값이 데이터베이스에 없으면 파일이 업로드되지 않았음을 의미하며 서버는 클라이언트가 전체 파일을 업로드해야 한다고 반환합니다.

6. 클라이언트가 파일을 업로드한 후 서버는 MySQL 데이터베이스에 파일과 MD 5 간의 해당 관계를 추가 하고 파일 조각화 정보를 저장합니다.

7. 다음에 동일한 파일을 업로드할 때 MD5 값을 사용하여 몇 초 안에 업로드할 수 있습니다.

따라서 핵심은 MySQL 데이터베이스를 사용하여 각 파일의 MD5 및 조각화 정보를 기록하고 업로드 시 MD5 를 통해 쿼리하는 것이며 MySQL은 동일한 파일의 반복 업로드를 피하기 위해 즉시 전송을 허용할지 여부를 판단할 수 있습니다.

3. 중단점에서 재개란 무엇입니까? 

그럼 이번 프로젝트로 송전을 재개한다는 개념을 알기 쉽게 설명하겠습니다.

1. 프런트엔드 Vue는 파일을 업로드할 때 파일을 여러 개의 작은 조각으로 자르고 매번 하나의 작은 조각을 업로드합니다.

2. 작은 블록이 업로드될 때마다 백엔드 SpringBoot는 작은 블록의 일련 번호, 파일 MD5, 콘텐츠 해시 등과 같은 이 작은 블록의 정보를 기록합니다. MySQL 데이터베이스에 저장할 수 있습니다.

3. 업로드가 중단되면 Vue 측은 SpringBoot에게 어떤 작은 조각이 업로드되었는지 물어볼 수 있습니다.

4. SpringBoot는 데이터베이스에서 쿼리하고 업로드된 작은 정보를 Vue에 반환합니다.

5. Vue는 중간에 중단되는 작은 부분만 계속해서 업로드할 수 있습니다.

6. SpringBoot는 작은 블록의 일련 번호와 파일의 MD5에 따라 이러한 작은 블록을 완전한 파일로 재조립합니다.

7. 추후에 해당 파일이 다시 업로드되면 MD5 값을 이용하여 해당 파일이 이미 존재함을 알 수 있으며, 실제 콘텐츠를 업로드하지 않고 바로 업로드 성공이 반환됩니다. 이런 방식으로 슬라이스를 업로드하고 업로드된 슬라이스의 정보를 지속적으로 기록함으로써 중단점에서 업로드를 재개할 수 있습니다.

중요한 것은 SpringBoot가 업로드된 슬라이스 정보를 기록하고 얻기 위한 인터페이스를 제공해야 한다는 것입니다. Vue 측에서는 순서대로 슬라이스하고 업로드해야 합니다. 마지막으로 SpringBoot는 파일을 하나로 합칩니다. 간단히 말해서 네트워크 및 기타 이유로 업로드가 중단됩니다. 전송된 데이터의 양을 기록하여, 중단 후에도 남은 데이터를 계속해서 업로드할 수 있는 기술 솔루션입니다.

4. 멀티파트 업로드란 무엇입니까?

마지막으로 이번 프로젝트를 바탕으로 멀티파트 업로드의 개념을 이해하기 쉽게 설명하겠습니다.

1. 분할 업로드의 목적은 대용량 파일을 여러 개의 작은 조각으로 나누어 동시에 업로드하여 전송 속도를 향상시키는 것입니다.

2. 대용량 파일은 구성된 조각 크기(예: 50M 조각)에 따라 분할될 수 있습니다.

3. Vue 프로젝트는 각 분할을 SpringBoot 서버에 순서대로 업로드한 다음 각 조각을 순서대로 업로드합니다.

4. SpringBoot 서버는 조각을 수신한 후 임시로 로컬에 저장할 수 있고 조각 일련 번호, 파일 MD5 등과 같은 조각의 특성 정보를 기록하고 데이터베이스에 쓸 수 있습니다.

5. 모든 조각을 업로드한 후 SpringBoot는 이를 순서대로 원본 전체 파일로 재조립합니다.

5. 업로드 과정

다음은 제가 그린 프런트 엔드 슬라이스 처리 로직입니다.

이것은 백엔드에서 슬라이스를 처리하기 위해 제가 그린 논리입니다.

이것은 전체 프로젝트 실행 프로세스를 논리적으로 정리한 것입니다. 

1. 생성되면 업로더 구성 요소를 초기화하고 조각 크기, 업로드 방법 및 기타 구성을 지정합니다.

2. onFileAdded 메서드에서 선택한 파일에 대한 MD5를 계산한 후 file.resume()을 호출하여 업로드를 시작합니다.

3. file.resume()은 먼저 내부적으로 GET 요청을 보내 서버에 업로드된 파일 조각을 요청합니다.

4. 서버는 업로드된 조각 목록이 포함된 JSON을 반환합니다.

5. 업로더 구성 요소는 checkChunkUploadedByResponse를 호출하여 현재 청크가 업로드된 목록에 있는지 확인합니다.

6. 업로드되지 않은 세그먼트의 경우 file.resume()은 세그먼트를 업로드하기 위해 POST 요청을 계속 트리거합니다.

7. POST 요청에는 조각의 데이터 및 오프셋과 같은 정보가 포함됩니다.

8. 서버는 조각난 데이터를 수신하고 이를 파일의 지정된 위치에 쓴 후 성공적인 응답을 반환합니다.

9. 업로더 구성요소는 세그먼트가 업로드되었음을 기록합니다.

10. 모든 조각을 순서대로 업로드한 후 서버 측은 모든 조각을 완전한 파일로 병합합니다.

11. 업로드가 성공했음을 알리기 위해 onFileSuccess가 호출됩니다.

12. 이런 식으로 업로드된 조각에 대해 조회하는 GET 요청 + 완료되지 않은 조각 업로드 POST + 확인을 통해 중단점 재개/조각 업로드가 실현됩니다.

제가 그린 스윔레인 그래프는 다음과 같습니다.

6. SpringBoot 프로젝트 빌드

다음으로 이 프로젝트의 백엔드를 구축하는 방법을 단계별로 설명하겠습니다. 전체 프로젝트의 스크린샷은 다음과 같습니다.

6.1 준비사항 

6.1.1 pom 종속성 가져오기

백엔드의 전체 종속성 정보입니다.

전체 코드:

<dependencies>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 常用工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- MySQL依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <!-- Mybatis依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <!-- Lombok依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
    </dependencies>

6.1.2, yml 구성 파일

기본 구성 백엔드 포트는 9090이고 단일 파일 제한은 최대 100MB이며 MySQL의 구성 정보와 MyBatis의 구성 정보입니다.

전체 코드:

server:
  port: 9090

spring:
  servlet:
    multipart:
      max-request-size: 100MB
      max-file-size: 100MB
  datasource:
    username: 用户名
    password: 密码
    url: jdbc:mysql://localhost:3306/数据库名?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis配置
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.example.**.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath:mapping/*.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml

6.1.3, mybatis-config.xml 구성 파일

mybatis의 구성 파일을 저장하기 위해 리소스 폴더 아래에 새 mybatis 폴더를 만듭니다.

전체 코드:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存 -->
        <setting name="cacheEnabled"             value="true"   />
        <!-- 允许JDBC 支持自动生成主键 -->
        <setting name="useGeneratedKeys"         value="true"   />
        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
        <setting name="defaultExecutorType"      value="SIMPLE" />
        <!-- 指定 MyBatis 所用日志的具体实现 -->
        <setting name="logImpl"                  value="SLF4J"  />
        <!-- 使用驼峰命名法转换字段 -->
         <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

</configuration>

6.1.4, SQL 파일

파일 조각화 테이블 저장:

CREATE TABLE `file_chunk`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `identifier` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件md5',
  `chunk_number` int NULL DEFAULT NULL COMMENT '当前分块序号',
  `chunk_size` bigint NULL DEFAULT NULL COMMENT '分块大小',
  `current_chunk_size` bigint NULL DEFAULT NULL COMMENT '当前分块大小',
  `total_size` bigint NULL DEFAULT NULL COMMENT '文件总大小',
  `total_chunks` int NULL DEFAULT NULL COMMENT '分块总数',
  `filename` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '文件分片表' ROW_FORMAT = DYNAMIC;

파일 정보 테이블 저장:


CREATE TABLE `file_info`  (
  `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键',
  `origin_file_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文件源名',
  `file_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '新存放文件名',
  `file_path` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件存放路径',
  `file_size` bigint NULL DEFAULT NULL COMMENT '文件总大小',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '文件信息表' ROW_FORMAT = Dynamic;

6.2, 상수 클래스 

6.2.1, HttpStatus 공통 반환 상태 코드 상수

전체 코드:

package com.example.bigupload.constant;

/**
 * 返回状态码
 * @author HTT
 */
public class HttpStatus
{
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;
}

6.3, 엔터티 클래스

6.3.1, AjaxResult 통합 결과 캡슐화

전체 코드:

package com.example.bigupload.domain;

import com.example.bigupload.constant.HttpStatus;
import org.apache.commons.lang3.ObjectUtils;

import java.util.HashMap;

/**
 * 操作消息提醒
 *
 * @author HTT
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     */
    public AjaxResult(int code, String msg)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(int code, String msg, Object data)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (ObjectUtils.isNotEmpty(data))
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(int code, String msg)
    {
        return new AjaxResult(code, msg, null);
    }

    /**
     * 方便链式调用
     *
     * @param key 键
     * @param value 值
     * @return 数据对象
     */
    @Override
    public AjaxResult put(String key, Object value)
    {
        super.put(key, value);
        return this;
    }
}

6.3.2, 파일 슬라이스 클래스

전체 코드:

package com.example.bigupload.domain;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import java.io.Serializable;
import java.util.Date;

@Data
public class FileChunk implements Serializable {

    /**
     * 主键
     */
    private Long id;
    /**
     * 文件 md5
     */
    private String identifier;
    /**
     * 当前分块序号
     */
    private Integer chunkNumber;
    /**
     * 分块大小
     */
    private Long chunkSize;
    /**
     * 当前分块大小
     */
    private Long currentChunkSize;
    /**
     * 文件总大小
     */
    private Long totalSize;
    /**
     * 分块总数
     */
    private Integer totalChunks;
    /**
     * 文件名
     */
    private String filename;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 文件分片数据
     */
    private MultipartFile file;

}

6.3.3, 파일 정보 클래스

전체 코드:

package com.example.bigupload.domain;

import lombok.Data;

import java.util.Date;

@Data
public class FileInfo {

    /**
     * 主键
     */
    private String id;

    /**
     * 文件源名
     */
    private String originFileName;

    /**
     * 新存放文件名
     */
    private String fileName;

    /**
     * 文件存放路径
     */
    private String filePath;

    /**
     * 文件总大小
     */
    private Long fileSize;

    /**
     * 创建时间
     */
    private Date createTime;

}

6.4, GlobalCorsConfig 글로벌 CORS 교차 도메인 구성

1. @Configuration 주석은 이것이 구성 클래스임을 나타냅니다.

2. WebMvcConfigurer 인터페이스는 SpringMVC의 구성을 사용자 정의하는 데 사용됩니다.

3. addCorsMappings 메소드는 교차 도메인 액세스 정책을 정의하는 데 사용됩니다.

4. addMapping("/**")은 모든 요청 경로를 가로채는 것을 의미합니다.

5. allowedOriginPatterns("*")는 모든 도메인 이름 요청이 허용됨을 나타냅니다.

6.allowCredentials(true)는 쿠키가 허용됨을 의미합니다.

7. allowedHeaders는 허용된 요청 헤더를 나타냅니다.

8. allowedMethods는 허용된 요청 방법을 나타냅니다.

9. maxAge(3600)은 실행 전 요청의 유효 기간이 3600초임을 나타냅니다.

이러한 방식으로 글로벌 CORS 크로스 도메인 구성이 구현되어 모든 도메인 이름의 요청이 이 서비스에 액세스할 수 있으며 요청 헤더 및 방법을 자유롭게 설정할 수 있으며 최대 유효 기간은 1시간입니다.

전체 코드:

package com.example.bigupload.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedHeaders("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Authorization", "Cache-Control", "Content-Type")
                .maxAge(3600);
    }
}

6.5, 지속성 레이어

6.5.1, FileChunkMapper.xml 파일

주로 두 개의 SQL을 작성합니다. 하나는 md5 암호화된 정보에 따라 데이터베이스의 모든 조각화 정보를 쿼리하는 것이고, 다른 하나는 성공적인 각 조각 업로드의 파일 정보를 기록하는 것입니다.

전체 코드:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.bigupload.mapper.FileChunkMapper">
    <select id="findFileChunkParamByMd5" resultType="com.example.bigupload.domain.FileChunk">
        SELECT * FROM file_chunk where identifier =#{identifier}
    </select>

    <select id="findCountByMd5" resultType="Integer">
        SELECT COUNT(*) FROM file_chunk where identifier =#{identifier}
    </select>

    <insert id="insertFileChunk" parameterType="com.example.bigupload.domain.FileChunk">
        INSERT INTO file_chunk
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="identifier != null">identifier,</if>
            <if test="chunkNumber != null">chunk_number,</if>
            <if test="chunkSize != null">chunk_size,</if>
            <if test="currentChunkSize != null">current_chunk_size,</if>
            <if test="totalSize != null">total_size,</if>
            <if test="totalChunks != null">total_chunks,</if>
            <if test="filename != null">filename,</if>
            <if test="createTime != null">create_time,</if>
        </trim>
        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
            <if test="identifier != null">#{identifier},</if>
            <if test="chunkNumber != null">#{chunkNumber},</if>
            <if test="chunkSize != null">#{chunkSize},</if>
            <if test="currentChunkSize != null">#{currentChunkSize},</if>
            <if test="totalSize != null">#{totalSize},</if>
            <if test="totalChunks != null">#{totalChunks},</if>
            <if test="filename != null">#{filename},</if>
            <if test="createTime != null">#{createTime},</if>
        </trim>
    </insert>
</mapper>

6.5.2, FileChunkMapper 파일

전체 코드:

package com.example.bigupload.mapper;

import com.example.bigupload.domain.FileChunk;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * @author HTT
 */
@Mapper
public interface FileChunkMapper {

    public List<FileChunk> findFileChunkParamByMd5(String identifier);

    public Integer findCountByMd5(String identifier);

    public int insertFileChunk(FileChunk fileChunk);
}

6.5.3, FileInfoMapper.xml 파일

전체 코드:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.bigupload.mapper.FileInfoMapper">
    <insert id="insert">
        INSERT INTO file_info
            (id, origin_file_name, file_name, file_path, file_size, create_time)
        VALUES
            (#{id}, #{originFileName}, #{fileName}, #{filePath}, #{fileSize}, #{createTime});
    </insert>
</mapper>

6.5.4, FileInfoMapper 파일

package com.example.bigupload.mapper;

import com.example.bigupload.domain.FileInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface FileInfoMapper {

  int insert(FileInfo fileInfo);

}

6.6 파일 업로드 핵심 서비스 클래스(중요)

6.6.1, 파일 업로드 확인 로직

1. 파일 식별자(md5)에 따라 해당 파일이 이미 데이터베이스에 존재하는지 쿼리합니다.

List<FileChunk> list = fileChunkMapper.findFileChunkParamByMd5(fileChunk.getIdentifier());
Map<String, Object> data = new HashMap<>(1);

2. 찾을 수 없으면 파일이 업로드되지 않았다는 뜻이며, Upload=false를 반환합니다.

if (list == null || list.size() == 0) {
    data.put("uploaded", false);
    return AjaxResult.success("文件上传成功",data);
}

3. 쿼리에서 한 부분만 발견되면 전체 파일이 업로드되었음을 의미하며 upload=true가 반환됩니다.

if (list.get(0).getTotalChunks() == 1) {
    data.put("uploaded", true);
    data.put("url", "");
    return AjaxResult.success("文件上传成功",data);
}

4. 여러 개의 데이터를 조회하는 경우 이는 대용량 파일을 분할하여 업로드한 것을 의미합니다.

5. 조각난 데이터를 탐색하여 각 파일 블록의 번호를 획득하고 이를 UploadFiles 배열에 저장합니다.

6. 마지막으로 업로드된 청크를 프런트 엔드로 반환합니다.

// 处理分片
int[] uploadedFiles = new int[list.size()];
int index = 0;
for (FileChunk fileChunkItem : list) {
    uploadedFiles[index] = fileChunkItem.getChunkNumber();
    index++;
}
data.put("uploadedChunks", uploadedFiles);
return AjaxResult.success("文件上传成功",data);

7. 프런트 엔드는 데이터를 얻은 후 어떤 조각이 대용량 파일에 업로드되었는지, 어떤 조각이 업로드되도록 남아 있는지 알고 있습니다.

8. 그런 다음 중단점에서 재개 효과를 얻기 위해 중단점에서 나머지 조각을 계속 업로드합니다.따라서 이 코드는 주로 파일 데이터베이스를 확인하여 파일 전체 또는 일부가 이미 존재하는지 여부를 확인하여 필요한지 여부를 결정합니다. 전체 파일을 업로드하거나 중단점 업로드를 계속합니다.

이를 통해 반복적인 업로드를 방지하고 전송 효율을 높일 수 있으며, 이는 즉시 전송과 재개 가능한 전송을 구현하는 핵심 로직입니다.

키 코드:

    public AjaxResult checkUpload(FileChunk fileChunk) {
        List<FileChunk> list = fileChunkMapper.findFileChunkParamByMd5(fileChunk.getIdentifier());
        Map<String, Object> data = new HashMap<>(1);
        // 判断文件存不存在
        if (list == null || list.size() == 0) {
            data.put("uploaded", false);
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理单文件
        if (list.get(0).getTotalChunks() == 1) {
            data.put("uploaded", true);
            data.put("url", "");
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理分片
        int[] uploadedFiles = new int[list.size()];
        int index = 0;
        for (FileChunk fileChunkItem : list) {
            uploadedFiles[index] = fileChunkItem.getChunkNumber();
            index++;
        }
        data.put("uploadedChunks", uploadedFiles);
        return AjaxResult.success("文件上传成功",data);
    }

6.6.2 조각 업로드의 핵심 논리 실현

이 코드는 RandomAccessFile을 사용하여 조각 업로드 및 쓰기 논리를 구현하는 것입니다.

특정 논리:

1. RandomAccessFile 객체를 생성하고 파일 경로에 따라 저장한 후 읽기 및 쓰기 모드로 파일을 열고 이를 사용하여 파일에 대한 임의 액세스 읽기 및 쓰기를 수행합니다.

RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");

2. 각 프래그먼트의 ChunkSize를 계산하고, 프런트 엔드에서 이를 전달하지 않으면 기본값(50MB, 제가 정의한 상수)을 사용합니다.

public static final long DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024;
long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();

3. (조각 번호 - 1) * 조각 크기로 계산되는 현재 조각의 오프셋을 계산합니다.

long offset = chunkSize * (fileChunk.getChunkNumber() - 1);

4. 검색() 메서드를 사용하여 조각의 오프셋 위치를 찾습니다.

randomAccessFile.seek(offset);

5. write() 메서드를 사용하여 현재 조각의 파일 내용 바이트를 씁니다.

randomAccessFile.write(fileChunk.getFile().getBytes());

6. 모든 샤드가 기록될 때까지 이 과정을 반복합니다.

7. 마지막으로 파일을 닫습니다.

randomAccessFile.close();

RandomAccessFile은 임의의 위치에서 파일을 읽고 쓸 수 있으므로 조각 업로드 효과를 얻기 위해 조각 순서대로 지정된 위치에 쓸 수 있습니다. 이 방법을 사용하면 운영 체제의 파일 캐시를 최대한 활용할 수 있어 더욱 효율적입니다. 각 샤드는 한 번만 작성되며 파일을 읽고 수정할 필요가 없으므로 IO 작업이 절약됩니다. 이는 RandomAccessFile을 통해 조각 업로드를 구현하고 각 조각을 완전한 파일로 연결하는 일반적인 방법입니다.

간단한 실제 사례를 소개하여 이해하기 쉽게 설명하겠습니다.

먼저 오프셋이 무엇인지 소개하겠습니다. 오프셋은 전체 파일에서 각 조각의 위치를 ​​결정하는 데 사용됩니다.

이 키 코드에 따르면:

// 计算每个分片大小 
long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();

// 计算分片的偏移量
long offset = chunkSize * (fileChunk.getChunkNumber() - 1); 

오프셋 = 조각 크기 * (샤드 시퀀스 번호 - 1)

230MB 파일을 예로 들어보겠습니다.

1. 청크 크기는 50MB입니다.

2. 첫 번째 조각 번호 fileChunk, getChunkNumber()는 1입니다.

3. 그런 다음 첫 번째 슬라이스의 오프셋 = 50MB * (1 - 1) = 0입니다.

두 번째 슬라이스의 조각 번호는 2이고, 두 번째 슬라이스의 오프셋은 50MB * (2 - 1) = 50MB입니다. 유사하게 각 슬라이스의 오프셋을 계산할 수 있습니다.

n번째 슬라이스의 오프셋 공식은 다음과 같습니다: 오프셋 = 슬라이스 크기 * (n번째 슬라이스의 세그먼트 번호 - 1).

따라서 전체 실행 프로세스는 다음과 같습니다.

230MB 파일은 5개의 조각으로 나뉩니다.

1. 첫 번째 조각: 50MB, 오프셋은 0입니다.

2. 두 번째 조각: 50MB, 오프셋 50MB.

3. 세 번째 조각: 50MB, 오프셋 100MB.

4. 네 번째 조각: 50MB, 오프셋 150MB.

5. 다섯 번째 조각: 30MB, 오프셋 200MB.

클라이언트는 다음 5개의 조각을 동시에 업로드할 수 있습니다.

1. 첫 번째 조각을 업로드하고 오프셋 0에 씁니다.

2. 두 번째 조각을 업로드하고 오프셋 50MB에 씁니다.

3. 세 번째 조각을 업로드하고 오프셋 100MB에 씁니다.

4. 네 번째 조각을 업로드하고 오프셋 150MB에 씁니다.

5. 다섯 번째 조각을 업로드하고 오프셋 200MB에 씁니다.

서버측에서는 RandomAccessFile을 통해 오프셋에 따라 각 샤드의 내용을 직접 쓸 수 있습니다. 이런 방식으로 50MB 조각 5개를 통해 230MB의 대용량 파일을 빠르게 업로드할 수 있어 조각 업로드 효과를 실현하고 전송 속도를 향상시킬 수 있습니다.

키 코드:

    private void uploadFileByRandomAccessFile(String filePath, FileChunk fileChunk) throws IOException {

        RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
        // 分片大小必须和前端匹配,否则上传会导致文件损坏
        long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();
        // 偏移量
        long offset = chunkSize * (fileChunk.getChunkNumber() - 1);
        // 定位到该分片的偏移量
        randomAccessFile.seek(offset);
        // 写入
        randomAccessFile.write(fileChunk.getFile().getBytes());
        randomAccessFile.close();

    }

6.6.3 파일 조각화 처리

여기서 가장 중요한 것은 파일 조각의 무작위 읽기 및 쓰기를 구현하고 업로드된 파일 조각 데이터를 올바른 오프셋 위치에 쓸 수 있는 uploadFileByRandomAccessFile 메서드입니다. 

파일의 고유 식별자(MD5)에 따라 데이터베이스의 총 조각 수를 쿼리하고, 프런트 엔드 조각의 총 수와 동일하면 대용량 파일이 업로드되었음을 의미하며 전체 파일 정보 이번에 업로드된 내용은 데이터베이스에 저장됩니다.

키 코드:

public AjaxResult uploadChunkFile(FileChunk fileChunk) throws IOException {

        String newFileName = fileChunk.getIdentifier() + fileChunk.getFilename();
        String filePath = "D:\\大文件分片存放\\" + newFileName;
        uploadFileByRandomAccessFile(filePath, fileChunk);
        fileChunk.setCreateTime(new Date());
        fileChunkMapper.insertFileChunk(fileChunk);

        //数据库中已上传的分片总数
        Integer count = fileChunkMapper.findCountByMd5(fileChunk.getIdentifier());
        if(fileChunk.getTotalChunks().equals(count)){
            FileInfo fileInfo = new FileInfo();
            String originalFilename = fileChunk.getFile().getOriginalFilename();
            fileInfo.setId(UUID.randomUUID().toString());
            fileInfo.setOriginFileName(originalFilename);
            fileInfo.setFileName(newFileName);
            fileInfo.setFilePath(filePath);
            fileInfo.setFileSize(fileChunk.getTotalSize());
            fileInfo.setCreateTime(new Date());
            fileInfoMapper.insert(fileInfo);
        }
        return AjaxResult.success("文件上传成功");
    }

6.6.4, 전체 코드

package com.example.bigupload.service;

import com.example.bigupload.domain.AjaxResult;
import com.example.bigupload.domain.FileChunk;
import com.example.bigupload.domain.FileInfo;
import com.example.bigupload.mapper.FileChunkMapper;
import com.example.bigupload.mapper.FileInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;

/**
 * @author HTT
 */
@Slf4j
@Service
public class UploadService {

    /**
     * 默认的分片大小:50MB
     */
    public static final long DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024;

    @Resource
    private FileChunkMapper fileChunkMapper;

    @Resource
    private FileInfoMapper fileInfoMapper;

    public AjaxResult checkUpload(FileChunk fileChunk) {
        List<FileChunk> list = fileChunkMapper.findFileChunkParamByMd5(fileChunk.getIdentifier());
        Map<String, Object> data = new HashMap<>(1);
        // 判断文件存不存在
        if (list == null || list.size() == 0) {
            data.put("uploaded", false);
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理单文件
        if (list.get(0).getTotalChunks() == 1) {
            data.put("uploaded", true);
            data.put("url", "");
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理分片
        int[] uploadedFiles = new int[list.size()];
        int index = 0;
        for (FileChunk fileChunkItem : list) {
            uploadedFiles[index] = fileChunkItem.getChunkNumber();
            index++;
        }
        data.put("uploadedChunks", uploadedFiles);
        return AjaxResult.success("文件上传成功",data);
    }

    /**
     * 上传分片文件
     * @param fileChunk
     * @return
     * @throws Exception
     */
    public AjaxResult uploadChunkFile(FileChunk fileChunk) throws IOException {

        String newFileName = fileChunk.getIdentifier() + fileChunk.getFilename();
        String filePath = "D:\\大文件分片存放\\" + newFileName;
        uploadFileByRandomAccessFile(filePath, fileChunk);
        fileChunk.setCreateTime(new Date());
        fileChunkMapper.insertFileChunk(fileChunk);

        //数据库中已上传的分片总数
        Integer count = fileChunkMapper.findCountByMd5(fileChunk.getIdentifier());
        if(fileChunk.getTotalChunks().equals(count)){
            FileInfo fileInfo = new FileInfo();
            String originalFilename = fileChunk.getFile().getOriginalFilename();
            fileInfo.setId(UUID.randomUUID().toString());
            fileInfo.setOriginFileName(originalFilename);
            fileInfo.setFileName(newFileName);
            fileInfo.setFilePath(filePath);
            fileInfo.setFileSize(fileChunk.getTotalSize());
            fileInfo.setCreateTime(new Date());
            fileInfoMapper.insert(fileInfo);
        }
        return AjaxResult.success("文件上传成功");
    }

    private void uploadFileByRandomAccessFile(String filePath, FileChunk fileChunk) throws IOException {

        RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
        // 分片大小必须和前端匹配,否则上传会导致文件损坏
        long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();
        // 偏移量
        long offset = chunkSize * (fileChunk.getChunkNumber() - 1);
        // 定位到该分片的偏移量
        randomAccessFile.seek(offset);
        // 写入
        randomAccessFile.write(fileChunk.getFile().getBytes());
        randomAccessFile.close();

    }


}

6.7, FileController 요청 계층

글은 매우 간결합니다. 이유는 모두 동일한 /upload 인터페이스이고, 요청 메소드는 각각 get 및 post입니다. 왜냐하면 업로더 구성요소는 확인할 때 먼저 /upload의 get 요청을 호출한 다음 조각을 업로드하고 /upload 요청 게시.

1. 먼저 GET 요청을 사용하여 파일이 존재하는지 확인하고 즉시 전송, 중단점 재개 전송 또는 다중 부분 업로드를 구현할지 결정합니다.

2. 그런 다음 클라이언트는 POST 요청을 사용하여 각 조각을 순서대로 업로드하고 서버는 조각을 받은 후 파일을 작성합니다.

전체 코드:

package com.example.bigupload.controller;

import com.example.bigupload.domain.AjaxResult;
import com.example.bigupload.domain.FileChunk;
import com.example.bigupload.service.UploadService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/file")
public class FileController {

    @Resource
    private UploadService uploadService;

    @GetMapping("/upload")
    public AjaxResult checkUpload(FileChunk fileChunk){
        return uploadService.checkUpload(fileChunk);
    }

    @PostMapping("/upload")
    public AjaxResult uploadChunkFile(FileChunk fileChunk) throws Exception {
        return uploadService.uploadChunkFile(fileChunk);
    }

}

7. Vue 프로젝트 빌드

여기서는 Vue2.0을 사용하고 있습니다. 이것은 프로젝트의 전체 스크린샷입니다.

7.1 준비사항

1. 업로더 설치

npm install --save vue-simple-uploader

2. Spark-md5에 따르면

npm install --save spark-md5

3. main.js에 구성요소 소개

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

7.2, 홈뷰 페이지

실제로 여기서의 주요 로직은 먼저 getFileMD5 메소드를 통해 이 파일의 MD5 검사 코드를 슬라이싱하고 생성한 다음 해당 파일을 사용하는 것입니다.현재 파일의 업로드된 조각 정보를 모두 가져온 다음 checkChunkUploadedByResponse 메소드의 로직을 사용합니다. 어떤 조각을 아직 업로드해야 하는지 판단합니다.

7.2.1 업로더 구성요소 사용

이 구성 요소는 업로드 구성 요소인 타사 라이브러리 vue-uploader를 사용합니다.

매개변수 설명:

1. ref: 구성 요소에 참조 이름을 지정합니다. 여기에는 업로더가 있습니다.

2. 옵션: 업로드 주소, 허용되는 파일 형식 등 업로드 옵션.

3. autoStart: 업로드를 자동으로 시작할지 여부, 기본값은 true이며 여기서는 false로 설정됩니다.

4. fileStatusText: 사용자 정의 업로드 상태 프롬프트 텍스트입니다.

5. @file-add: 파일을 추가할 때의 Hook입니다.

6. @file-success: 파일 업로드 성공 시 Hook을 생성합니다.

7. @file-error: 파일 업로드 실패 시 후크됩니다.

8. @file-progress: 파일 업로드 진행을 위한 후크입니다.

세 가지 하위 구성요소가 포함되어 있습니다.

1. Uploader-unsupport: 브라우저가 지원하지 않는 경우 이 구성요소를 표시합니다.

2. 업로더 드롭: 업로드 영역 구성 요소를 끌어서 놓아 콘텐츠를 사용자 정의합니다.

3. uploader-btn: 업로드 버튼 구성요소입니다.

4. uploader-files: 업로드할 파일을 선택하는 목록 구성 요소입니다.

따라서 이 구성 요소는 파일 선택, 업로드 드래그 앤 드롭, 업로드 진행률 표시, 업로드 결과 호출과 같은 완전한 파일 업로드 기능을 구현합니다. 옵션을 통해 업로드 매개변수를 구성하고, 다양한 후크를 통해 업로드 결과를 처리하고, 업로드 영역의 스타일을 사용자 정의하여 완전한 업로드 구성 요소를 구현할 수 있습니다.

전체 코드:

<template>
  <div class="home">
    <uploader
        ref="uploader"
        :options="options"
        :autoStart="false"
        :file-status-text="fileStatusText"
        @file-added="onFileAdded"
        @file-success="onFileSuccess"
        @file-error="onFileError"
        @file-progress="onFileProgress"
        class="uploader-example"
    >
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
      </uploader-drop>
      <uploader-files> </uploader-files>
    </uploader>
    <br />
  </div>
</template>

7.2.2 데이터 데이터 초기화

먼저 Md5 도구를 도입하고 조각 크기를 미리 초기화하십시오.

import SparkMD5 from "spark-md5";
const CHUNK_SIZE = 50 * 1024 * 1024;

옵션 매개변수:

1. 대상: 업로드된 인터페이스 주소

2. testChunks: 멀티파트 업로드 확인을 활성화할지 여부입니다. 부분 업로드는 파일을 작은 조각으로 나누어 동시에 업로드하는 방식으로, 대용량 파일 업로드의 효율성을 높일 수 있습니다.

3. uploadMethod: 업로드 방법, 기본값은 POST입니다.

4. ChunkSize: 조각 크기, 기본값은 1MB입니다. 이는 실제 상황에 따라 조정할 수 있으며, 조각이 너무 작으면 요청 수가 늘어나고, 조각이 너무 크면 업로드에 실패할 경우 많이 재전송하게 됩니다.

5.simultaneousUploads: 동시 업로드 블록 수, 기본값은 3이며 필요에 따라 조정될 수 있습니다.

7.2.3, checkChunkUploadedByResponse 메소드(중요)

이 메소드의 기능은 현재 업로드된 조각이 성공적으로 업로드되었는지 확인하는 것입니다.

이 메소드는 업로드 인터페이스에서 제공하는 가져오기 요청을 한 번만 호출하여 백엔드에서 이 파일의 모든 조각 정보를 가져옵니다. 여기에는 두 개의 매개변수가 있습니다.

청크: 오프셋과 같은 데이터를 포함한 현재 조각에 대한 정보입니다.

메시지: 업로드 인터페이스에서 제공한 가져오기 요청에 의해 반환된 모든 조각화 정보를 호출합니다.

이 메서드가 true를 반환하면 현재 세그먼트가 성공적으로 업로드되었음을 의미하므로 사후 업로드 요청을 보내기 위해 업로드 메서드를 호출할 필요가 없습니다.

함수 내에서 서버가 반환한 응답 콘텐츠 메시지는 먼저 JSON 개체로 구문 분석됩니다.

let messageObj = JSON.parse(message);

그런 다음 데이터 필드를 데이터 개체로 꺼냅니다.

let dataObj = messageObj.data;

그런 다음 업로드된 필드가 데이터 객체에 포함되어 있는지 판단하고, 직접 반환된 Upload 값이 있는 경우 업로드된 필드는 일반적으로 조각 업로드가 완료되었는지 여부를 나타내는 서버의 신호입니다.

if (dataObj.uploaded !== undefined) {
    return dataObj.uploaded;
}

업로드된 파트 목록이 [1, 2]인 경우, 즉 파트 1과 파트 2는 성공적으로 업로드되었으며 파트 3과 파트 4는 아직 업로드되지 않은 것입니다.

검증할 현재 블록은 블록 2, 오프셋=1입니다.

그런 다음 실행 프로세스는 다음과 같습니다.

1. dataObj.uploadedChunks의 값은 [1, 2]입니다.

2. Chunk.offset의 값은 여전히 ​​1입니다.

3. Chunk.offset+1은 블록 2의 시퀀스 번호가 2라고 계산합니다.

4. indexOf(2)는 배열 [1, 2]에서 값 2를 찾습니다.

5. 반환된 인덱스 값은 1, 즉 찾음입니다.

따라서 파트 2의 경우 true를 반환하여 파트가 성공적으로 업로드되었음을 나타냅니다.

return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;

return의 목적은 서버에서 반환한 업로드된 블록 목록을 보고 현재 블록이 성공적으로 업로드되었는지 확인하는 것입니다. 그렇지 않은 경우 upload에서 제공하는 post 요청을 계속해서 호출하여 업로드합니다.

7.2.4,parseTimeRemaining 메소드

남은 업로드 시간의 형식을 지정하는 방법입니다. 원래 시간은 xx일 xx시간 xx분 xx초 형식으로 표시됩니다.

키 코드:

parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
    return parsedTimeRemaining
     .replace(/\syears?/, "年")
     .replace(/\days?/, "天")
     .replace(/\shours?/, "小时")
     .replace(/\sminutes?/, "分钟")
     .replace(/\sseconds?/, "秒");
},

따라서 이러한 구성은 주로 분할 업로드를 위한 것이며 청크, 동시성, 검증과 같은 메커니즘을 통해 대용량 파일 업로드의 효율성과 경험을 향상시킵니다.

키 코드:

    options: {
        target: 'http://127.0.0.1:9090/file/upload',
        testChunks: true,
        uploadMethod: "post",
        chunkSize: CHUNK_SIZE,
        // 并发上传数,默认为 3
        simultaneousUploads: 3,
        checkChunkUploadedByResponse: (chunk, message) => {
          let messageObj = JSON.parse(message);
          let dataObj = messageObj.data;
          if (dataObj.uploaded !== undefined) {
            return dataObj.uploaded;
          }
          return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
        },
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
          return parsedTimeRemaining
              .replace(/\syears?/, "年")
              .replace(/\days?/, "天")
              .replace(/\shours?/, "小时")
              .replace(/\sminutes?/, "分钟")
              .replace(/\sseconds?/, "秒");
        },
      },

7.2.5 기타 매개변수

매개변수 역할:

1. fileStatus: 업로드 상태를 표시하는 구성 요소에 사용될 업로드 상태에 대한 텍스트 설명을 정의합니다.

2. uploadFileList: 업로드에 성공한 파일 목록으로, 업로드에 성공한 파일의 정보를 저장하는 데 사용할 수 있습니다.

키 코드:

fileStatus: {
  success: "上传成功",
  error: "上传错误",
  uploading: "正在上传",
  paused: "停止上传",
  waiting: "等待中",
},
uploadFileList: [],

7.2.6, onFileAdded 메소드(중요)

특정 논리:

1. 추가된 파일을 uploadFileList에 추가합니다.

2. MD5를 계산한 후 file.resume()을 호출합니다.

3. file.resume()은 파일의 MD5 정보를 포함하여 내부적으로 GET 요청을 구성하고 이를 서버로 보냅니다.

4. 서버는 요청을 받은 후 MD5에 따라 파일이 존재하는지 확인합니다.

5. 존재하는 경우 업로드된 블록 정보를 반환합니다.

6. 프런트 엔드는 응답을 받은 후 checkChunkUploadedByResponse 로직을 호출합니다.

7. checkChunkUploadedByResponse는 응답 데이터에 따라 각 청크의 업로드 상태를 확인하고, 응답에 따라 업로드된 청크와 업로드되지 않은 청크를 판단합니다.

8. 업로드되지 않은 청크 업로드 업로드되지 않은 조각의 경우 file.resume()은 모든 조각이 확인되고 업로드될 때까지 조각을 업로드하라는 요청(POST)을 계속 트리거합니다.

따라서 file.resume() 메소드에는 checkChunkUploadedByResponse의 확인 로직뿐만 아니라 실제 부분 업로드를 트리거하는 로직도 포함되어 있습니다.

키 코드:

    onFileAdded(file) {
      this.uploadFileList.push(file);
      // 2. 计算文件 MD5 并请求后台判断是否已上传,是则取消上传
      this.getFileMD5(file, (md5) => {
        if (md5 != "") {
          // 修改文件唯一标识
          file.uniqueIdentifier = md5;
          // 恢复上传
          file.resume();
        }
      });
    },

7.2.7, getFileMD5 메소드(중요)

업로드하기 전에 파일의 MD5 값을 계산하고, 슬라이싱하는 동안 MD5 누적 계산을 수행합니다. 일반적인 논리는 다음과 같습니다.

먼저 SparkMD5 개체 스파크를 초기화하여 MD5의 누적 값을 저장한 다음 FileReader를 통해 블록 단위로 파일 내용을 읽습니다. 매번 청크 크기의 슬라이스를 읽을 때마다 각 슬라이스를 읽을 때 슬라이스 내용을 스파크 개체에 추가하고 MD5를 수행합니다. 축적한 다음 모든 슬라이스를 읽을 때까지 프로세스를 반복합니다. 마지막으로 Spark.end()를 호출하여 파일의 누적 계산된 전체 MD5 값을 가져옵니다.

특정 논리:

1. MD5 값을 계산하기 위해 SparkMD5 개체 스파크를 생성합니다.

let spark = new SparkMD5.ArrayBuffer();

2. 파일 내용을 읽기 위한 FileReader 객체 fileReader를 생성합니다.

let fileReader = new FileReader();

3. 파일의 슬라이스 메소드 획득을 담당하는 File 객체의 슬라이스 메소드의 호환 가능한 구현을 획득합니다.

let blobSlice =
          File.prototype.slice ||
          File.prototype.mozSlice ||
          File.prototype.webkitSlice;

4. 총 청크 수를 계산하며, 각 청크의 크기는 CHUNK_SIZE입니다.

let currentChunk = 0;
let chunks = Math.ceil(file.size / CHUNK_SIZE);

5. 시작 시간 startTime을 기록합니다.

let startTime = new Date().getTime();

6. file.pause() 파일 업로드를 일시 중지합니다.

file.pause();

7. loadNext 함수는 파일 조각을 로드하는 데 사용됩니다.

특정 논리:

첫 번째 단계는 현재 조각의 시작 위치 start를 계산하는 것입니다. 이는 현재 조각 일련 번호 currentChunk에 조각 크기 CHUNK_SIZE를 곱한 것입니다.

두 번째 단계는 현재 조각의 끝 위치를 계산하는 것입니다. 시작 + 조각 크기가 총 크기 file.size보다 크거나 같으면 end는 총 크기 file.size이고, 그렇지 않으면 end는 시작 + 조각 크기 CHUNK_SIZE입니다. .

세 번째 단계는 파일의 처음부터 끝까지 해당 부분을 가로채는 blobSlice 메소드를 통해 파일의 슬라이스 Blob 객체를 얻는 것입니다. 실제로는 슬라이싱 메서드를 호출하고 파일 객체와 범위를 전달해 실제 파일 슬라이싱 작업을 수행하는 것입니다.

4단계: FileReader의 readAsArrayBuffer 메서드를 호출하여 Blob 조각 개체의 콘텐츠를 읽습니다.

5단계. readAsArrayBuffer는 FileReader의 onload 이벤트를 트리거 하고 나중에 이벤트 콜백에서 조각 콘텐츠를 가져와 처리할 수 있습니다.

따라서 loadNext의 기능은 조각 크기와 일련 번호에 따라 지정된 파일의 조각 Blob을 얻은 다음 FileReader를 사용하여 이 조각의 ArrayBuffer 콘텐츠를 읽는 것입니다.

키 코드:

    function loadNext() {
        const start = currentChunk * CHUNK_SIZE;
        const end =
            start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }

8. 이 코드는 FileReader의 onload 이벤트에 대한 콜백 함수로, 조각 내용을 읽은 후 MD5 누적 계산을 처리하는 데 사용됩니다.

특정 논리:

첫 번째 단계는 읽은 조각난 ArrayBuffer 콘텐츠를 Spark.append(e.target.result)를 통해 스파크에 추가하고 MD5 누적을 수행하는 것입니다.

두 번째 단계는 조각 번호의 currentChunk가 여전히 총 ​​청크 수보다 작다면 아직 읽지 않은 조각이 있음을 의미한다고 판단하는 것입니다. currentChunk는 자동으로 증가되고 다음 조각을 로드하기 위해 loadNext()가 호출됩니다.

그렇지 않으면 세 번째 단계는 모든 조각을 읽었음을 의미합니다. Spark 및 end()를 통해 최종 MD5 값 md5를 계산하고, 시간이 많이 걸리는 인쇄 및 계산을 수행하고, 콜백을 통해 md5 값을 반환합니다.

네 번째 단계인 콜백 함수는 외부에서 md5를 수신하여 파일이 중복되었는지 판단하고 후속 로직을 결정합니다.

따라서 이 코드의 기능은 파일 조각을 반복적으로 읽고, MD5를 누적 및 계산하고, 마지막으로 MD5 결과를 반환하여 전체 파일의 MD5 검증을 완료하는 것입니다. 조각으로 로드함으로써 대용량 파일 전체를 한 번에 로드함으로써 발생하는 성능 문제를 피하고 효율적인 MD5 계산이 실현됩니다.

키 코드:

      fileReader.onload = function (e) {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
        } else {
          let md5 = spark.end();
          console.log(
              `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`
          );
          callback(md5);
        }
      };
      fileReader.onerror = function () {
        this.$message.error("文件读取错误");
        file.cancel();
      };

7.2.8, 전체 코드

<template>
  <div class="home">
    <uploader
        ref="uploader"
        :options="options"
        :autoStart="false"
        :file-status-text="fileStatusText"
        @file-added="onFileAdded"
        @file-success="onFileSuccess"
        @file-error="onFileError"
        @file-progress="onFileProgress"
        class="uploader-example"
    >
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
      </uploader-drop>
      <uploader-files> </uploader-files>
    </uploader>
    <br />
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'
import SparkMD5 from "spark-md5";
const CHUNK_SIZE = 50 * 1024 * 1024;
export default {
  name: 'HomeView',
  components: {
    HelloWorld
  },
  data() {
    return {
      options: {
        target: 'http://127.0.0.1:9090/file/upload',
        testChunks: true,
        uploadMethod: "post",
        chunkSize: CHUNK_SIZE,
        simultaneousUploads: 3,
        checkChunkUploadedByResponse: (chunk, message) => {
          let messageObj = JSON.parse(message);
          let dataObj = messageObj.data;
          if (dataObj.uploaded !== undefined) {
            return dataObj.uploaded;
          }
          return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
        },
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
          return parsedTimeRemaining
              .replace(/\syears?/, "年")
              .replace(/\days?/, "天")
              .replace(/\shours?/, "小时")
              .replace(/\sminutes?/, "分钟")
              .replace(/\sseconds?/, "秒");
        },
      },
      fileStatus: {
        success: "上传成功",
        error: "上传错误",
        uploading: "正在上传",
        paused: "停止上传",
        waiting: "等待中",
      },
      uploadFileList: [],
    };
  },
  methods: {
    onFileAdded(file) {
      this.uploadFileList.push(file);
      // 2. 计算文件 MD5 并请求后台判断是否已上传,是则取消上传
      this.getFileMD5(file, (md5) => {
        if (md5 != "") {
          // 修改文件唯一标识
          file.uniqueIdentifier = md5;
          // 恢复上传
          file.resume();
        }
      });
    },
    onFileSuccess(rootFile, file, response, chunk) {
      console.log("上传成功");
    },
    onFileError(rootFile, file, message, chunk) {
      console.log("上传出错:" + message);
    },
    onFileProgress(rootFile, file, chunk) {
      console.log(`当前进度:${Math.ceil(file._prevProgress * 100)}%`);
    },
    
    getFileMD5(file, callback) {
      let spark = new SparkMD5.ArrayBuffer();
      let fileReader = new FileReader();
      let blobSlice =
          File.prototype.slice ||
          File.prototype.mozSlice ||
          File.prototype.webkitSlice;
      let currentChunk = 0;
      let chunks = Math.ceil(file.size / CHUNK_SIZE);
      let startTime = new Date().getTime();
      file.pause();
      loadNext();
      fileReader.onload = function (e) {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
        } else {
          let md5 = spark.end();
          console.log(
              `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`
          );
          callback(md5);
        }
      };
      fileReader.onerror = function () {
        this.$message.error("文件读取错误");
        file.cancel();
      };

      function loadNext() {
        const start = currentChunk * CHUNK_SIZE;
        const end =
            start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },
    fileStatusText(status) {
      console.log(11111)
      console.log(status)
      if (status === "md5") {
        return "校验MD5";
      }
      return this.fileStatus[status];
    },
  },
}
</script>

여덟째, 프로젝트를 실행하라

다음으로, 실제로 이 프로젝트를 실행하고 그 효과를 보여드리겠습니다.

8.1 분할 업로드

업로드용으로 미리 141.1MB 크기의 파일을 준비했고, 업로드용으로 기본적으로 2개로 잘랐습니다.

데이터베이스에 기록된 정보는 다음과 같습니다.

조각 정보:

파일 정보:

141M 파일이 3개로 잘려지지 않고 2개로만 잘려져 있고, 두 번째 파일의 크기가 91MB인 이유가 궁금하시다면 그 이유를 아래에서 자세히 설명드리겠습니다. 

이론적으로 141M 파일은 50MB, 50MB, 41MB 세 개의 조각화된 파일로 나뉘는데 마지막 41MB는 50MB를 충족하지 못하기 때문입니다. 분할 횟수를 줄이기 위해 3개의 블록을 병합하므로 최종 조각은 50MB입니다. 그리고 91MB.

8.2 중단점 재개 

때로는 네트워크상의 이유가 있거나 고객이 나머지 부분을 저장하고 내일 업로드하기를 원할 수도 있습니다. 

다음번에는 업로드가 성공할 때까지 마지막 업로드 진행상황이 유지됩니다! 

데이터베이스에 기록된 정보는 다음과 같습니다.

조각 정보:

샤드 정보 중 일부만 업로드되기 때문에 데이터베이스에 기록됩니다. 

파일 정보:

파일이 완전히 업로드되지 않았기 때문에 파일 정보 테이블에는 파일 정보가 저장되지 않습니다. 

다음번에는 지난번 진행상황에 따라 계속해서 업로드하도록 하겠습니다.

데이터베이스에 기록된 정보는 다음과 같습니다. 

조각 정보:

파일 정보:

8.3 두 번째 패스 

몇 초 만에 업로드하는 것은 매우 간단합니다. 저는 지금 대용량 파일을 업로드하기로 선택했습니다. 

당연히 슬라이싱은 수행하지 않고 검증 인터페이스만 한 번 호출하고 이 파일의 모든 샤드 정보를 MD5 기반의 데이터베이스에서 쿼리하여 프런트엔드로 반환한 것을 프런트엔드의 샤드 정보와 비교하면, 파일 업로드가 완료된 것을 확인하니 1초 안에 업로드가 완료됩니다! 

9. Gitee 소스코드 주소

여기에서는 프로젝트의 전체 코드를 공개 소스로 제공하므로 스스로 배울 수 있습니다!

프런트 엔드: Vue는 몇 초 만에 대용량 파일 전송 구현 + 중단점 재개 전송 + 조각 업로드

백엔드: SpringBoot는 몇 초 만에 대용량 파일 전송 구현 + 중단점 재개 전송 + 조각 업로드

10. 요약

위 내용은 Vue+SpringBoot의 초 단위 대용량 파일 전송, 중단점 이력서 전송 및 조각 업로드의 전체 프로세스에 대한 개인적인 설명입니다. 거의 모든 위치가 마련되어 있습니다. 질문이 있는 경우 댓글 영역에서 토론을 환영합니다. !

추천

출처blog.csdn.net/HJW_233/article/details/132224072