使用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=nulllog.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.使用现成APINioUtils.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);}@Overridepublic 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;@Overridepublic void start() {log.info("start progress {}", fileName);}@Overridepublic void progress(long total, long progressSize) {log.debug("progress {}, total={}, progressSize={}", fileName, total, progressSize);}@Overridepublic 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函数来实现前端页面的开发。

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV中的像素重映射原理及实战分析

引言 映射是个数学术语&#xff0c;指两个元素的集之间元素相互“对应”的关系&#xff0c;为名词。映射&#xff0c;或者射影&#xff0c;在数学及相关的领域经常等同于函数。 基于此&#xff0c;部分映射就相当于部分函数&#xff0c;而完全映射相当于完全函数。 说的简单点…

2.FastRunner定时任务Celery+RabbitMQ

注意&#xff1a;celery版本和Python冲突问题 不能用高版本Python 用3.5以下&#xff0c;因为项目的celery用的django-celery 3.2.2 python3.7 async关键字 冲突版本 celery3.x方案一&#xff1a; celery3.xpython3.6方案二 &#xff1a; celery4.xpython3.7 解决celery执…

海康Visionmaster-环境配置:VB.Net 二次开发环境配 置方法

Visual Basic 进行 VM 二次开发的环境配置分为三步。 第一步&#xff0c;使用 VS 新建一个框架为.NET Framework 4.6.1&#xff0c;平台去勾选首选 32 为的工程&#xff0c;重新生成解决方案&#xff0c;保证工程 Debug 下存在 exe 文件&#xff0c;最后关闭新建工程&#xff1…

2024有哪些免费的mac苹果电脑内存清理工具?

在我们日常使用苹果电脑的过程中&#xff0c;随着时间的推移&#xff0c;可能会发现设备的速度变慢了&#xff0c;甚至出现卡顿的现象。其中一个常见的原因就是程序占用内存过多&#xff0c;导致系统无法高效地运行。那么&#xff0c;苹果电脑内存怎么清理呢&#xff1f;本文将…

Linux动静态库

文章目录 1. 静态库2. 动态库3. 动态库的加载 本章代码gitee仓库&#xff1a;动静态库 1. 静态库 Linux开发工具gcc/g篇&#xff0c;此篇文章讲过动静态库的基本概念&#xff0c;不了解的可以先看一下这篇文章。 现在我们先来制作一个简单的静态库 mymath.h #pragma once#i…

Jmeter- Beanshell语法和常用内置对象(网络整理)

在利用jmeter进行接口测试或者性能测试的时候&#xff0c;我们需要处理一些复杂的请求&#xff0c;此时就需要利用beanshell脚本了&#xff0c;BeanShell是一种完全符合Java语法规范的脚本语言,并且又拥有自己的一些语法和方法&#xff0c;所以它和java是可以无缝衔接的。beans…

<MySQL> 什么是数据库索引?数据库索引的底层结构是什么?

目录 一、什么是数据库索引? 1.1 索引的概念 1.2 索引的特点 1.3 索引的适用场景 1.4 索引的使用 1.4.1 创建索引 1.4.2 查看索引 1.4.3 删除索引 二、数据库索引的底层结构是什么&#xff1f; 2.1 数据库中的 B树 长啥样&#xff1f; 2.2 B树为什么适合做数据库索…

B树与B+树

B树 B树&#xff0c;又称多路平衡查找树&#xff0c;B树中所有结点的孩子个数的最大值称为B树的阶&#xff0c;通常用m表示。一颗m阶B树或为空树&#xff0c;或为满足如下特征的m叉树。 树中每个结点至多有m棵子树&#xff0c;即至多含有m-1个关键字若根结点不是终端结点&…

从0到0.01入门 Webpack| 002.精选 Webpack面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

力扣刷题篇之数与位3

系列文章目录 目录 系列文章目录 前言 数学问题 总结 前言 本系列是个人力扣刷题汇总&#xff0c;本文是数与位。刷题顺序按照[力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 - 力扣&#xff08;LeetCode&#xff09; 数学问题 204. 计数质数 - 力扣&#xff08;Le…

【开源】基于JAVA的大学兼职教师管理系统

项目编号&#xff1a; S 004 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S004&#xff0c;文末获取源码。} 项目编号&#xff1a;S004&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容三、界面展示3.1 登录注册3.2 学生教师管…

APIcloud 【现已更名 用友开发中心】 iOS发版 应用程序请求用户同意访问相机和照片,但没有在目的字符串中充分说明相机和照片的使用。

iOS 审核时 提示 首次安装软件 获取相机 相册 提示信息 怎么修改 我们注意到你的应用程序请求用户同意访问相机和照片&#xff0c;但没有在目的字符串中充分说明相机和照片的使用。 为了解决这个问题&#xff0c;修改应用信息中的目的字符串是合适的。相机和照片的Plist文件&a…

Skywalking流程分析_9(JDK类库中增强流程)

前言 之前的文章详细介绍了关于非JDK类库的静态方法、构造方法、实例方法的增强拦截流程&#xff0c;本文会详细分析JDK类库中的类是如何被增强拦截的 回到最开始的SkyWalkingAgent#premain try {/** 里面有个重点逻辑 把一些类注入到Boostrap类加载器中 为了解决Bootstrap类…

python科研绘图:P-P图与Q-Q图

目录 什么是P-P图与Q-Q图 分位数 百分位数 Q-Q图步骤与原理 Shapiro-Wilk检验 绘制Q-Q图 绘制P-P图 什么是P-P图与Q-Q图 P-P图和Q-Q图都是用于检验样本的概率分布是否服从某种理论分布。 P-P图的原理是检验实际累积概率分布与理论累积概率分布是否吻合。若吻合&#xf…

SpringBoot整合Quartz示例

数据表 加不加无所谓,如果需要重启服务器后重新执行所有JOB就把sql加上 如果不加表 将application.properties中的quartz数据库配置去掉 自己执行自己的逻辑来就好,大不了每次启动之后重新加载自己的逻辑 链接&#xff1a;https://pan.baidu.com/s/1KqOPYMfI4eHcEMxt5Bmt…

2023年05月 Python(六级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 明明每天坚持背英语单词,他建立了英语单词错题本文件“mistakes.txt”,将每天记错的单词增加到该文件中,下列打开文件的语句最合适的是?( ) A: f = open(“mistakes.txt”) B: …

yolov5模型代码怎么修改

yaml配置文件 深度乘积因子 宽度乘积因子 所有版本只有这两个参数的不同&#xff0c;s m l x逐渐加宽加深 各种类型层参数对照 backbone里的各层&#xff0c;在这里解析&#xff0c;只需要改.yaml里的各层参数就能控制网络结构 修改网络结构 第一步&#xff1a;把新加的模块…

家庭网络中的组网方式

家庭网络中&#xff0c;目前也衍生出了比较多的组网方式&#xff0c;也不是只有Easymesh&#xff0c;我们还是要辩证的去看&#xff0c;没有绝对的好和坏&#xff0c;需求不同&#xff0c;取舍不同。 这里博主简单的介绍几种组网方式&#xff0c;上图也比较直观 1.wds wds是…

Windows系统中搭建docker (ubuntu,Docker-desktop)

一、docker安装前的准备工作 1. 开启CPU虚拟化&#xff0c;新电脑该默认是开启的&#xff0c;如果没开启可以根据自己电脑型号品牌搜索如克开启CPU虚拟化。当开启成功后可在设备管理器中看到。 2.开通Hyper-V 通过 Windows 控制面板 --> 程序和功能 -->启用或关闭…

Windows 11 设置 wsl-ubuntu 使用桥接网络

Windows 11 设置 wsl-ubuntu 使用桥接网络 0. 背景1. Windows 11 下启用 Hyper-V2. 使用 Hyper-V 虚拟交换机管理器创建虚拟网络3. 创建 .wslconfig 文件4. 配置 wsl.conf 文件5. 配置 wsl-network.conf 文件6. 创建 00-wsl2.yaml7. 安装 net-tools 和 openssh-server 0. 背景 …