使用Spring Boot实现大文件断点续传及文件校验

一、简介

随着互联网的快速发展,大文件的传输成为了互联网应用的重要组成部分。然而,由于网络不稳定等因素的影响,大文件的传输经常会出现中断的情况,这时需要重新传输,导致传输效率低下。

为了解决这个问题,可以实现大文件的断点续传功能。断点续传功能可以在传输中断后继续传输,而不需要从头开始传输。这样可以大大提高传输的效率。

Spring Boot是一个快速开发的Java Web开发框架,可以帮助我们快速搭建一个Web应用程序。在Spring Boot中,我们可以很容易地实现大文件的断点续传功能。

本文将介绍如何使用Spring Boot实现大文件的断点续传功能。

二、Spring Boot实现大文件断点续传的原理

实现大文件的断点续传功能,需要在客户端和服务端都进行相应的实现。

实现示例1

服务端如何将一个大视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。

Spring Boot实现HTTP分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。

文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-bom</artifactId>
        <version>5.8.18</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-core</artifactId>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>

代码实现

ResourceController

package com.example.insurance.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import com.example.insurance.common.ContentRange;
import com.example.insurance.common.MediaContentUtil;
import com.example.insurance.common.NioUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 内容资源控制器
 */
@SuppressWarnings("unused")
@Slf4j
@RestController("resourceController")
@RequestMapping(path = "/resource")
public class ResourceController {

    /**
     * 获取文件内容
     *
     * @param fileName 内容文件名称
     * @param response 响应对象
     */
    @GetMapping("/media/{fileName}")
    public void getMedia(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
                         @RequestHeader HttpHeaders headers) {
//        printRequestInfo(fileName, request, headers);

        String filePath = MediaContentUtil.filePath();
        try {
            this.download(fileName, filePath, request, response, headers);
        } catch (Exception e) {
            log.error("getMedia error, fileName={}", fileName, e);
        }
    }

    /**
     * 获取封面内容
     *
     * @param fileName 内容封面名称
     * @param response 响应对象
     */
    @GetMapping("/cover/{fileName}")
    public void getCover(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
                         @RequestHeader HttpHeaders headers) {
//        printRequestInfo(fileName, request, headers);

        String filePath = MediaContentUtil.filePath();
        try {
            this.download(fileName, filePath, request, response, headers);
        } catch (Exception e) {
            log.error("getCover error, fileName={}", fileName, e);
        }
    }


    // ======= internal =======

    private static void printRequestInfo(String fileName, HttpServletRequest request, HttpHeaders headers) {
        String requestUri = request.getRequestURI();
        String queryString = request.getQueryString();
        log.debug("file={}, url={}?{}", fileName, requestUri, queryString);
        log.info("headers={}", headers);
    }

    /**
     * 设置请求响应状态、头信息、内容类型与长度 等。
     * <pre>
     * <a href="https://www.rfc-editor.org/rfc/rfc7233">
     *     HTTP/1.1 Range Requests</a>
     * 2. Range Units
     * 4. Responses to a Range Request
     *
     * <a href="https://www.rfc-editor.org/rfc/rfc2616.html">
     *     HTTP/1.1</a>
     * 10.2.7 206 Partial Content
     * 14.5 Accept-Ranges
     * 14.13 Content-Length
     * 14.16 Content-Range
     * 14.17 Content-Type
     * 19.5.1 Content-Disposition
     * 15.5 Content-Disposition Issues
     *
     * <a href="https://www.rfc-editor.org/rfc/rfc2183">
     *     Content-Disposition</a>
     * 2. The Content-Disposition Header Field
     * 2.1 The Inline Disposition Type
     * 2.3 The Filename Parameter
     * </pre>
     *
     * @param response     请求响应对象
     * @param fileName     请求的文件名称
     * @param contentType  内容类型
     * @param contentRange 内容范围对象
     */
    private static void setResponse(
            HttpServletResponse response, String fileName, String contentType,
            ContentRange contentRange) {
        // http状态码要为206:表示获取部分内容
        response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
        // 支持断点续传,获取部分字节内容
        // Accept-Ranges:bytes,表示支持Range请求
        response.setHeader(HttpHeaders.ACCEPT_RANGES, ContentRange.BYTES_STRING);
        // inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                "inline;filename=" + MediaContentUtil.encode(fileName));
        // Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
        // Content-Range: bytes 0-10/3103,格式为bytes 开始-结束/全部
        response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange.toContentRange());

        response.setContentType(contentType);
        // Content-Length: 11,本次内容的大小
        response.setContentLengthLong(contentRange.applyAsContentLength());
    }

    /**
     * <a href="https://www.jianshu.com/p/08db5ba3bc95">
     *     Spring Boot 处理 HTTP Headers</a>
     */
    private void download(
            String fileName, String path, HttpServletRequest request, HttpServletResponse response,
            HttpHeaders headers)
            throws IOException {
        Path filePath = Paths.get(path + fileName);
        if (!Files.exists(filePath)) {
            log.warn("file not exist, filePath={}", filePath);
            return;
        }
        long fileLength = Files.size(filePath);
//        long fileLength2 = filePath.toFile().length() - 1;
//        // fileLength=1184856, fileLength2=1184855
//        log.info("fileLength={}, fileLength2={}", fileLength, fileLength2);

        // 内容范围
        ContentRange contentRange = applyAsContentRange(headers, fileLength, request);

        // 要下载的长度
        long contentLength = contentRange.applyAsContentLength();
        log.debug("contentRange={}, contentLength={}", contentRange, contentLength);

        // 文件类型
        String contentType = request.getServletContext().getMimeType(fileName);
        // mimeType=video/mp4, CONTENT_TYPE=null
        log.debug("mimeType={}, CONTENT_TYPE={}", contentType, request.getContentType());

        setResponse(response, fileName, contentType, contentRange);

        // 耗时指标统计
        StopWatch stopWatch = new StopWatch("downloadFile");
        stopWatch.start(fileName);
        try {
            // case-1.参考网上他人的实现
//            if (fileLength >= Integer.MAX_VALUE) {
//                NioUtils.copy(filePath, response, contentRange);
//            } else {
//                NioUtils.copyByChannelAndBuffer(filePath, response, contentRange);
//            }

            // case-2.使用现成API
            NioUtils.copyByBio(filePath, response, contentRange);
//            NioUtils.copyByNio(filePath, response, contentRange);

            // case-3.视频分段渐进式播放
//            if (contentType.startsWith("video")) {
//                NioUtils.copyForBufferSize(filePath, response, contentRange);
//            } else {
//                // 图片、PDF等文件
//                NioUtils.copyByBio(filePath, response, contentRange);
//            }
        } finally {
            stopWatch.stop();
            log.info("download file, fileName={}, time={} ms", fileName, stopWatch.getTotalTimeMillis());
        }
    }

    private static ContentRange applyAsContentRange(
            HttpHeaders headers, long fileLength, HttpServletRequest request) {
        /*
         * 3.1. Range - HTTP/1.1 Range Requests
         * https://www.rfc-editor.org/rfc/rfc7233#section-3.1
         * Range: "bytes" "=" first-byte-pos "-" [ last-byte-pos ]
         *
         * For example:
         * bytes=0-
         * bytes=0-499
         */
        // Range:告知服务端,客户端下载该文件想要从指定的位置开始下载
        List<HttpRange> httpRanges = headers.getRange();

        String range = request.getHeader(HttpHeaders.RANGE);
        // httpRanges=[], range=null
        // httpRanges=[448135688-], range=bytes=448135688-
        log.debug("httpRanges={}, range={}", httpRanges, range);

        // 开始下载位置
        long firstBytePos;
        // 结束下载位置
        long lastBytePos;
        if (CollectionUtils.isEmpty(httpRanges)) {
            firstBytePos = 0;
            lastBytePos = fileLength - 1;
        } else {
            HttpRange httpRange = httpRanges.get(0);
            firstBytePos = httpRange.getRangeStart(fileLength);
            lastBytePos = httpRange.getRangeEnd(fileLength);
        }
        return new ContentRange(firstBytePos, lastBytePos, fileLength);
    }
}

NioUtils

package com.example.insurance.common;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.NioUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.io.unit.DataSize;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;

/**
 * NIO相关工具封装,主要针对Channel读写、拷贝等封装
 */
@Slf4j
public final class NioUtils {

    /**
     * 缓冲区大小 16KB
     *
     * @see NioUtil#DEFAULT_BUFFER_SIZE
     * @see NioUtil#DEFAULT_LARGE_BUFFER_SIZE
     */
//    private static final int BUFFER_SIZE = NioUtil.DEFAULT_MIDDLE_BUFFER_SIZE;
    private static final int BUFFER_SIZE = (int) DataSize.ofKilobytes(16L).toBytes();

    /**
     * <pre>
     * <a href="https://blog.csdn.net/qq_32099833/article/details/109703883">
     *     Java后端实现视频分段渐进式播放</a>
     * 服务端如何将一个大的视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。
     * 文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。
     *
     * <a href="https://blog.csdn.net/qq_32099833/article/details/109630499">
     *     大文件分片上传前后端实现</a>
     * </pre>
     */
    public static void copyForBufferSize(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();

        RandomAccessFile randomAccessFile = null;
        OutputStream outputStream = null;
        try {
            // 随机读文件
            randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            // 移动访问指针到指定位置
            randomAccessFile.seek(contentRange.getStart());

            // 注意:缓冲区大小 2MB,视频加载正常;1MB时有部分视频加载失败
            int bufferSize = BUFFER_SIZE;

            //获取响应的输出流
            outputStream = new BufferedOutputStream(response.getOutputStream(), bufferSize);

            // 每次请求只返回1MB的视频流
            byte[] buffer = new byte[bufferSize];
            int len = randomAccessFile.read(buffer);
            //设置此次相应返回的数据长度
            response.setContentLength(len);
            // 将这1MB的视频流响应给客户端
            outputStream.write(buffer, 0, len);

            log.info("file download complete, fileName={}, contentRange={}",
                    fileName, contentRange.toContentRange());
        } catch (ClientAbortException | IORuntimeException e) {
            // 捕获此异常表示用户停止下载
            log.warn("client stop file download, fileName={}", fileName);
        } catch (Exception e) {
            log.error("file download error, fileName={}", fileName, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(randomAccessFile);
        }
    }

    /**
     * 拷贝流,拷贝后关闭流。
     *
     * @param filePath     源文件路径
     * @param response     请求响应
     * @param contentRange 内容范围
     */
    public static void copyByBio(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();

        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            randomAccessFile.seek(contentRange.getStart());

            inputStream = Channels.newInputStream(randomAccessFile.getChannel());
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);

            StreamProgress streamProgress = new StreamProgressImpl(fileName);

            long transmitted = IoUtil.copy(inputStream, outputStream, BUFFER_SIZE, streamProgress);
            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException | IORuntimeException e) {
            // 捕获此异常表示用户停止下载
            log.warn("client stop file download, fileName={}", fileName);
        } catch (Exception e) {
            log.error("file download error, fileName={}", fileName, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(inputStream);
        }
    }

    /**
     * 拷贝流,拷贝后关闭流。
     * <pre>
     * <a href="https://www.cnblogs.com/czwbig/p/10035631.html">
     *     Java NIO 学习笔记(一)----概述,Channel/Buffer</a>
     * </pre>
     *
     * @param filePath     源文件路径
     * @param response     请求响应
     * @param contentRange 内容范围
     */
    public static void copyByNio(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();

        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            randomAccessFile.seek(contentRange.getStart());

            inputStream = Channels.newInputStream(randomAccessFile.getChannel());
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);

            StreamProgress streamProgress = new StreamProgressImpl(fileName);

            long transmitted = NioUtil.copyByNIO(inputStream, outputStream,
                    BUFFER_SIZE, streamProgress);
            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException | IORuntimeException e) {
            // 捕获此异常表示用户停止下载
            log.warn("client stop file download, fileName={}", fileName);
        } catch (Exception e) {
            log.error("file download error, fileName={}", fileName, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(inputStream);
        }
    }

    /**
     * <pre>
     * <a href="https://blog.csdn.net/lovequanquqn/article/details/104562945">
     *     SpringBoot Java实现Http方式分片下载断点续传+实现H5大视频渐进式播放</a>
     * SpringBoot 实现Http分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。
     * 二、Http分片下载断点续传实现
     * 四、缓存文件定时删除任务
     * </pre>
     */
    public static void copy(Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();
        // 要下载的长度
        long contentLength = contentRange.applyAsContentLength();

        BufferedOutputStream outputStream = null;
        RandomAccessFile randomAccessFile = null;
        // 已传送数据大小
        long transmitted = 0;
        try {
            randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            randomAccessFile.seek(contentRange.getStart());
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
            // 把数据读取到缓冲区中
            byte[] buffer = new byte[BUFFER_SIZE];

            int len = BUFFER_SIZE;
            //warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
            //不然会会先读取randomAccessFile,造成后面读取位置出错;
            while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }
            //处理不足buffer.length部分
            if (transmitted < contentLength) {
                len = randomAccessFile.read(buffer, 0, (int) (contentLength - transmitted));
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }

            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException e) {
            // 捕获此异常表示用户停止下载
            log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
        } catch (Exception e) {
            log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(randomAccessFile);
        }
    }

    /**
     * 通过数据传输通道和缓冲区读取文件数据。
     * <pre>
     * 当文件长度超过{@link Integer#MAX_VALUE}时,
     * 使用{@link FileChannel#map(FileChannel.MapMode, long, long)}报如下异常。
     * java.lang.IllegalArgumentException: Size exceeds Integer.MAX_VALUE
     *   at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:863)
     *   at com.example.insurance.controller.ResourceController.download(ResourceController.java:200)
     * </pre>
     *
     * @param filePath     源文件路径
     * @param response     请求响应
     * @param contentRange 内容范围
     */
    public static void copyByChannelAndBuffer(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();
        // 要下载的长度
        long contentLength = contentRange.applyAsContentLength();

        BufferedOutputStream outputStream = null;
        FileChannel inChannel = null;
        // 已传送数据大小
        long transmitted = 0;
        long firstBytePos = contentRange.getStart();
        long fileLength = contentRange.getLength();
        try {
            inChannel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.WRITE);
            // 建立直接缓冲区
            MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, firstBytePos, fileLength);
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
            // 把数据读取到缓冲区中
            byte[] buffer = new byte[BUFFER_SIZE];

            int len = BUFFER_SIZE;
            // warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
            // 不然会会先读取file,造成后面读取位置出错
            while ((transmitted + len) <= contentLength) {
                inMap.get(buffer);
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }
            // 处理不足buffer.length部分
            if (transmitted < contentLength) {
                len = (int) (contentLength - transmitted);
                buffer = new byte[len];
                inMap.get(buffer);
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }

            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException e) {
            // 捕获此异常表示用户停止下载
            log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
        } catch (Exception e) {
            log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(inChannel);
        }
    }

}

ContentRange

package com.example.insurance.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 内容范围对象
 * <pre>
 * <a href="https://www.rfc-editor.org/rfc/rfc7233#section-4.2">
 *     4.2. Content-Range - HTTP/1.1 Range Requests</a>
 * Content-Range: "bytes" first-byte-pos "-" last-byte-pos  "/" complete-length
 *
 * For example:
 * Content-Range: bytes 0-499/1234
 * </pre>
 *
 * @see org.apache.catalina.servlets.DefaultServlet.Range
 */
@Getter
@AllArgsConstructor
public class ContentRange {

    /**
     * 第一个字节的位置
     */
    private final long start;
    /**
     * 最后一个字节的位置
     */
    private long end;
    /**
     * 内容完整的长度/总长度
     */
    private final long length;

    public static final String BYTES_STRING = "bytes";

    /**
     * 组装内容范围的响应头。
     * <pre>
     * <a href="https://www.rfc-editor.org/rfc/rfc7233#section-4.2">
     *     4.2. Content-Range - HTTP/1.1 Range Requests</a>
     * Content-Range: "bytes" first-byte-pos "-" last-byte-pos  "/" complete-length
     *
     * For example:
     * Content-Range: bytes 0-499/1234
     * </pre>
     *
     * @return 内容范围的响应头
     */
    public String toContentRange() {
        return BYTES_STRING + ' ' + start + '-' + end + '/' + length;
//        return "bytes " + start + "-" + end + "/" + length;
    }

    /**
     * 计算内容完整的长度/总长度。
     *
     * @return 内容完整的长度/总长度
     */
    public long applyAsContentLength() {
        return end - start + 1;
    }

    /**
     * Validate range.
     *
     * @return true if the range is valid, otherwise false
     */
    public boolean validate() {
        if (end >= length) {
            end = length - 1;
        }
        return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
    }

    @Override
    public String toString() {
        return "firstBytePos=" + start +
                ", lastBytePos=" + end +
                ", fileLength=" + length;
    }
}

StreamProgressImpl

package com.example.insurance.common;

import cn.hutool.core.io.StreamProgress;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 数据流进度条
 */
@Slf4j
@AllArgsConstructor
public class StreamProgressImpl implements StreamProgress {

    private final String fileName;

    @Override
    public void start() {
        log.info("start progress {}", fileName);
    }

    @Override
    public void progress(long total, long progressSize) {
        log.debug("progress {}, total={}, progressSize={}", fileName, total, progressSize);
    }

    @Override
    public void finish() {
        log.info("finish progress {}", fileName);
    }
}

MediaContentUtil

package com.example.insurance.common;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * 文件内容辅助方法集
 */
public final class MediaContentUtil {

    public static String filePath() {
        String osName = System.getProperty("os.name");
        String filePath = "/data/files/";
        if (osName.startsWith("Windows")) {
            filePath = "D:\" + filePath;
        }
//        else if (osName.startsWith("Linux")) {
//            filePath = MediaContentConstant.FILE_PATH;
//        }
        else if (osName.startsWith("Mac") || osName.startsWith("Linux")) {
            filePath = "/home/admin" + filePath;
        }
        return filePath;
    }

    public static String encode(String fileName) {
        return URLEncoder.encode(fileName, StandardCharsets.UTF_8);
    }

    public static String decode(String fileName) {
        return URLDecoder.decode(fileName, StandardCharsets.UTF_8);
    }
}

实现示例2

代码实现

(1)客户端需要实现以下功能
  • 建立连接:客户端需要连接服务端,并建立连接。
  • 分块传输文件:客户端需要将文件分成若干块,并逐块传输。在传输中,每个块传输完成后,需要将已传输的位置发送给服务端,以便服务端记录传输位置。
  • 计算MD5值:在传输完成后,客户端需要计算文件的MD5值,以确保传输的完整性。
  • 与服务端比较MD5值:在计算出MD5值后,客户端需要将MD5值发送给服务端,并与服务端返回的MD5值比较,以确保传输的完整性。
(2)服务端需要实现以下功能
  • 建立连接:服务端需要等待客户端连接,并建立连接。
  • 接收文件:服务端需要接收客户端传输的文件。在接收文件时,需要记录传输的位置,并在传输中断后继续接收文件。
  • 计算MD5值:在接收完成后,服务端需要计算文件的MD5值,以确保传输的完整性。
  • 返回MD5值:在计算出MD5值后,服务端需要将MD5值返回给客户端。
1.编写客户端代码

在客户端中,我们需要实现以下功能:

  • 建立连接:使用Java的Socket类建立与服务端的连接。
  • 分块传输文件:将文件分成若干块,并逐块传输。在传输中,每个块传输完成后,需要将已传输的位置发送给服务端,以便服务端记录传输位置。
  • 计算MD5值:在传输完成后,计算文件的MD5值,以确保传输的完整性。
  • 与服务端比较MD5值:将MD5值发送给服务端,并与服务端返回的MD5值比较,以确保传输的完整性。

以下是客户端代码的实现:

@RestController
@RequestMapping("/file")
public class FileController {
    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("fileName") String fileName,
            @RequestParam("startPosition") long startPosition) {
        try {            // 建立连接
            Socket socket = new Socket("localhost", 8080);
            OutputStream outputStream = socket.getOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            // 分块传输文件
            FileInputStream fileInputStream = (FileInputStream) file.getInputStream();
            fileInputStream.skip(startPosition);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fileInputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            // 计算MD5值
            fileInputStream.getChannel().position(0);
            String md5 = DigestUtils.md5Hex(fileInputStream);
            // 与服务端比较MD5值
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            String serverMd5 = (String) objectInputStream.readObject();
            if (!md5.equals(serverMd5)) {
                throw new RuntimeException("MD5值不匹配");
            }
            // 关闭连接
            objectOutputStream.close();
            outputStream.close();
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
        return ResponseEntity.ok().build();
    }
}
2.编写服务端代码

在服务端中,我们需要实现以下功能:

  • 建立连接:使用Java的ServerSocket类等待客户端连接,并建立连接。
  • 接收文件:接收客户端传输的文件。在接收文件时,需要记录传输的位置,并在传输中断后继续接收文件。
  • 计算MD5值:在接收完成后,计算文件的MD5值,以确保传输的完整性。
  • 返回MD5值:将MD5值返回给客户端。

以下是服务端代码的实现:

@RestController
@RequestMapping("/file")
public class FileController {
    private final String FILE_PATH = "/tmp/upload/";

    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(HttpServletRequest request, @RequestParam("fileName") String fileName) {
        try {
            // 建立连接            
            ServerSocket serverSocket = new ServerSocket(8080);
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            // 接收文件            
            String filePath = FILE_PATH + fileName;
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
            long startPosition = randomAccessFile.length();
            randomAccessFile.seek(startPosition);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                randomAccessFile.write(buffer, 0, len);
            }   // 计算MD5值        
            FileInputStream fileInputStream = new FileInputStream(filePath);
            String md5 = DigestUtils.md5Hex(fileInputStream);
            // 返回MD5值        
            OutputStream outputStream = socket.getOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(md5);        // 关闭连
            objectInputStream.close();
            inputStream.close();
            randomAccessFile.close();
            socket.close();
            serverSocket.close();
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
        return ResponseEntity.ok().build();
    }
}
3. 编写前端代码

在前端中,我们需要实现以下功能:

  • 选择文件:提供一个文件选择框,让用户选择要上传的文件。
  • 分块上传:将文件分块上传到服务器。在上传过程中,需要记录上传的位置,并在上传中断后继续上传。

以下是前端代码的实现:

<html>
<head>
    <meta charset="UTF-8">
    <title>Spring Boot File Upload</title>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>
<body><input type="file" id="file">
<button onclick="upload()">Upload</button>
<script>    var file;
var startPosition = 0;
$('#file').on('change', function () {
    file = this.files[0];
});

function upload() {
    if (!file) {
        alert('Please select a file!');
        return;
    }
    var formData = new FormData();
    formData.append('file', file);
    formData.append('fileName', file.name);
    formData.append('startPosition', startPosition);
    $.ajax({
        url: '/file/upload',
        type: 'post',
        data: formData,
        cache: false,
        processData: false,
        contentType: false,
        success: function () {
            alert('Upload completed!');
        },
        error: function (xhr) {
            alert(xhr.responseText);
        },
        xhr: function () {
            var xhr = $.ajaxSettings.xhr();
            xhr.upload.onprogress = function (e) {
                if (e.lengthComputable) {
                    var percent = e.loaded / e.total * 100;
                    console.log('Upload percent: ' + percent.toFixed(2) + '%');
                }
            };
            return xhr;
        }
    });
}</script>
</body>
</html>

总结

本文介绍了如何使用Spring Boot实现大文件断点续传。在实现中,我们使用了Java的RandomAccessFile类来实现文件的分块上传和断点续传,使用了Spring Boot的RestController注解来实现Web服务的开发,使用了jQuery的Ajax函数来实现前端页面的开发。

在实际开发中,需要注意以下几点

  • 上传文件的大小和分块的大小需要根据实际情况进行设置,以确保上传速度和服务器的稳定性。
  • 在上传过程中,需要对异常情况进行处理,以确保程序的健壮性。
  • 在上传完成后,需要对上传的文件进行校验,以确保传输的完整性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/154918.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第四代智能井盖传感器:万宾科技助力城市安全

在繁华喧嚣的城市里人来人往&#xff0c;井盖作为基础设施的一个组成部分在路面上分布范围广。然而这些看似普通的井盖却存在着位移、水浸的风险&#xff0c;可能给我们的生活带来诸多不便&#xff0c;更会威胁到我们的人身安全。如何有效监测和管理井盖的状态&#xff0c;成为…

leetcode刷题日记:160. Intersection of Two Linked Lists(相交链表)

给出两个单链表的头结点headA与headB&#xff0c;让我们找出两个链表相接的起始节点&#xff0c;如果两个链表不存在相交结点返回null。 我们就先假设存在这样两个链表&#xff0c;链表1与链表2&#xff0c;假设链表1的长度为 L 1 L_1 L1​和 L 2 L_2 L2​,假设对于两个链表&am…

MatrixOne完成与欧拉、麒麟信安的兼容互认

近日&#xff0c;超融合异构云原生数据库MatrixOne企业版软件V1.0完成了与欧拉开源操作系统&#xff08;openEuler简称“欧拉”&#xff09;、麒麟信安操作系统系列产品和虚拟化平台的相互兼容认证&#xff0c;通过了欧拉兼容性测评&#xff0c;获得了《openEuler技术测评证书》…

JS进阶——作用域、解构、箭头函数

1、作用域 作用域&#xff08;scope&#xff09;规定了变量能够被访问的“范围”&#xff0c;离开了这个“范围”变量便不能被访问。 1.1 局部作用域 局部作用域可分为函数作用域和块作用域。 1.1.1 函数作用域 在函数内部声明的变量只能在函数内部被访问&#xff0c;外部无…

Linux C 线程

线程 概述线程和进程的异同如何选择使用进程还是线程 函数获取进程自身ID  pthread_self创建线程  pthread_create退出线程  pthread_exit线程等待  pthread_join 四种线程模型1 &#xff09;单线程2 &#xff09;单线程3 &#xff09;双线程4 &#xff09;三线程 概述…

【实习】modbus

介绍 详解Modbus通信协议—清晰易懂 Modbus协议是一个master/slave架构的协议。有一个节点是master节点&#xff0c;其他使用Modbus协议参与通信的节点是slave节点。每一个slave设备都有一个唯一的地址。在串行和MB网络中&#xff0c;只有被指定为主节点的节点可以启动一个命令…

探索 AI 算法与链上资产,ForthTech 如何提供稳健交易策略

从传统股票、期货市场发家&#xff0c;ForthTech 如何找到了 AI 赋能下数字资产交易策略与保值增值的技术路径&#xff1f;面对变幻不居的 Web3 行业&#xff0c;如何才能更好地应对市场波动&#xff0c;找到基建设施、资金管理、技术工具的优化方向&#xff0c;给用户更加安全…

QT自定义信号,信号emit,信号参数注册

qt如何自定义信号 使用signals声明返回值是void在需要发送信号的地方使用 emit 信号名字(参数)进行发送 在需要链接的地方使用connect进行链接 ct进行链接

LeetCode - 141. 环形链表 (C语言,快慢指针,配图)

目录 1. 什么是快慢指针 2. 非环形链表 3.代码展示 4.扩展&#xff1a;fast走3步&#xff0c;slow走一步呢&#xff1f; 1. 什么是快慢指针 这里我们我们将介绍环形链表的经典解法——快慢指针&#xff0c;简单理解&#xff0c;指针移动快的叫做快指针fast&#xff0c;移动…

汽车 CAN\CANFD数据记录仪

CAN FD数据记录仪解决汽车电子数据记录与偶发性故障查找问题。 1、脱机离线记录两路CAN/CANFD通道数据 脱机离线记录两路CAN/CANFD通道数据&#xff0c;可记录6个月数据。每个通 道单独设置触发记录模式、触发前预记录报文个数&#xff08;默认1000帧&#xff09;及 过滤规则&a…

NetApp E5700 系列混合闪存存储系统,将企业应用程序的性能提升到极致

主要优势 优势1、卓越的性能 • 利用最适合现代企业级应用&#xff08;例如&#xff0c;大数据分析、技术计算、视频监控以及备份和恢复&#xff09;的混合系统提高性能、IOPS 和密度。 优势2、无与伦比的价值 • 利用三个不同的磁盘系统架、多种驱动器类型和一套齐备的 SAN …

KT148A语音芯片使用串口uart本控制的完整说明_包含硬件和指令举例

一、功能简介 KT148A肯定是支持串口的&#xff0c;有客户反馈使用一线还是不方便&#xff0c;比如一些大型的系统不适合有延时的操作&#xff0c;所以更加倾向于使用uart控制&#xff0c;这里我们也给出解决方案 延伸出来另外一个版本&#xff0c;KT158A 注意次版本芯片还是…

教育数字化助力打造个性化语言学习环境

2023年,我国教育数字化呈现高速发展态势,网络教育用户规模、在线教育市场规模、数字内容市场规模再创历史新高,数字校园建设普及率、教师数字技术素养等均高于全球平均水平。 在数字技术支撑下,新的语言学习方式也在逐渐普及。 语言学家克拉申(Stephen Kr-ashen)提出的二语习得…

解决Web端请求响应超时HTTP状态码504和110 timed out错误(Nginx配置调整)

前言 在前端开发中&#xff0c;发送请求时&#xff0c;有时会遇到请求响应超时的问题&#xff08;如 HTTP 状态码504 和 110错误&#xff09;。这种问题可能是由于网络延迟、服务器响应时间过长或请求数据量过大等原因造成的。为了解决这个问题&#xff0c;我们可以通过配置 N…

python科研绘图:带正态分布的直方图

带正态分布的直方图是一种用直方图表示数据分布的图表&#xff0c;其中数据经过了正态分布的拟合。正态分布是一种常见的概率分布&#xff0c;具有平均值和标准差。在带正态分布的直方图中&#xff0c;数据被分成不同的区间&#xff0c;每个区间的频数或频率可以用颜色或标签表…

配电室中如何安装六氟化硫SF6气体泄漏报警装置?

六氟化硫气体泄漏报警装置安装位置产品的设计、检验、制造均遵循GB16808-2008《可燃气体报警控制器》和GB12358-2006《作业场所环境气体检测报警仪通用技术要求》严格设计。是经过高速CPU数据处理&#xff0c;通过LCD显示出探测器的浓度、状态并输出相应的控制信号。报警控制器…

恶意软件之系统病毒

病毒是迄今为止最常见的恶意软件类型之一。它是一种能够感染、破坏计算机设备&#xff0c;并在其运行系统上自我复制的程序。由于病毒是自我复制的&#xff0c;一旦安装并运行&#xff0c;它们就可以在同一网络上自动从一台设备传播到另一台设备&#xff0c;无需人为干预。病毒…

熬夜整理的Figma插件合集分享,快码住!

越来越多的设计师逐渐从用Sketch转变为用Figma做设计。相比起Sketch&#xff0c;Figma的基本功能上确实很厉害&#xff0c;但是他比较缺乏的一个东西就是没有很多丰富实用的插件支持。目前Figma作为一个快速发展的平台&#xff0c;逐渐搭建起了自己的辅助插件系统。如果你已经准…

vue+springboot实现图形验证码Kaptcha

1、前端 form使用了element-ui的组件&#xff0c;主要还是看img标签&#xff0c;src绑定了form.imgCodeUrl数据&#xff0c;点击图片时触发refreshCode更新图片验证码。 <el-form-item prop"verificationCode" label"验证码" style"text-align: l…

【informer】 时间序列的预测学习 2021 AAAI best paper

文章目录 前言1.引入2.数据集3. 训练4其它【待续】 前言 数据集 https://github.com/zhouhaoyi/ETDataset/blob/main/README_CN.md 代码&#xff1a;https://github.com/zhouhaoyi/Informer2020#reproducibility 21年的paper:https://arxiv.org/pdf/2012.07436.pdf 论文在代码…