使用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.mzph.cn/news/147174.shtml

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

相关文章

十三、Docker的安装

0.安装Docker Docker 分为 CE 和 EE 两大版本。CE 即社区版&#xff08;免费&#xff0c;支持周期 7 个月&#xff09;&#xff0c;EE 即企业版&#xff0c;强调安全&#xff0c;付费使用&#xff0c;支持周期 24 个月。 Docker CE 分为 stable test 和 nightly 三个更新频道…

联想笔记本电脑触摸板失灵了怎么办

这里写自定义目录标题 thinkbook笔记本电脑触摸板失灵 thinkbook笔记本电脑触摸板失灵 由于重装系统&#xff0c;导致笔记本的触控板失灵&#xff0c; 网上说的办法有 1、按键盘上的ctrlf6键&#xff0c;打开触控板功能&#xff1a;无效 2、设置——>设备——>触控板&am…

【powershell】入门和示例

▒ 目录 ▒ &#x1f6eb; 导读开发环境 1️⃣ 简介用途IDE解决此系统上禁止运行脚本 2️⃣ 语法3️⃣ 实战数据库备份执行循环拷贝文件夹 &#x1f6ec; 文章小结&#x1f4d6; 参考资料 &#x1f6eb; 导读 开发环境 版本号描述文章日期2023-11-17操作系统Win10 - 22H21904…

23111709[含文档+PPT+源码等]计算机毕业设计基于Spring Boot智能无人仓库管理-进销存储

文章目录 **软件开发环境及开发工具&#xff1a;****功能介绍&#xff1a;****论文截图&#xff1a;****数据库&#xff1a;****实现&#xff1a;****代码片段&#xff1a;** 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 软件开发环境及…

SQL server从安装到入门(一)

文章目录 彻底安装怎么安装&#xff1f;Polybase要求安装orcale jre 7更新 51或更高版本&#xff1f;安装完怎么配置&#xff1f;没有SSMS&#xff1f; 熟悉一下SMSS&#xff01; 根据本人实际安装和初步使用SQL server的过程中&#xff0c;经历的一些关键性的步骤和精品文章。…

问卷工具价格一览:合理定价,满足您的预算需求

在市场调研、市场营销和客户反馈收集等方面&#xff0c;问卷调查是一项重要而有效的工具。而在众多的问卷工具中&#xff0c;Zoho Survey以其丰富的功能和灵活的定价模式而备受关注。Zoho Survey的定价如何&#xff1f;今天我们来聊一聊。 Zoho Survey提供了多种定价方案&…

ROS 学习应用篇(九)ROS中launch文件的实现

launch文件就好比一个封装好的命令库&#xff0c;我们按照在终端中输入的代码指令&#xff0c;全部按照launch语言格式封装在一个launch文件中&#xff0c;这样以后执行的时候&#xff0c;就可以不用开很多终端&#xff0c;一条一条输入代码指令。 lauch文件的语言风格很想我之…

用Postman发送xml数据

启动Postman&#xff1a; 点击左上角的“New”&#xff0c;在弹出窗中选择HTTP&#xff1a; 选择POST方法&#xff1a; 点击Body&#xff1a; 选择raw&#xff1a; 在右侧的下拉列表中选择XML&#xff1a; 在下面的输入框中输入或者从其它地方拷贝XML文本&#xff1a;…

websocket学习

写在前面 新公司用到了websocket技术&#xff0c;所以这里学习下。 1&#xff1a;Java原生 1.1&#xff1a;maven <dependency><groupId>org.java-websocket</groupId><artifactId>Java-WebSocket</artifactId><version>1.5.3</ver…

【高并发内存池】第一篇 项目简介及定长内存池

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的…

Android 实现三维空间坐标系(WebView与JS交互,支持多条曲线,可设置坐标轴翻转等)

全部代码已经上传&#xff0c;点击上方进行下载 支持多条曲线的绘制&#xff0c;可旋转拖动放大缩小 1.布局文件&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/…

plantuml最原始的主题如何设置

在startuml下一行添加 skin rose startuml skin rose:Hello world; :This is defined on several **lines**;enduml 效果如下&#xff1a; plantuml官网地址如下&#xff1a; ​​​​​​使用简单的文字描述画UML图的开源工具。轻松从简单的文字说明创建UML图。也有许多种可…

一键云端,AList 整合多网盘,轻松管理文件多元共享!

hello&#xff0c;我是小索奇&#xff0c;本篇教大家如何使用AList实现网盘挂载 可能还是有小伙伴不懂&#xff0c;所以简单介绍一下哈 AList 是一款强大的文件管理工具&#xff0c;为用户提供了将多种云存储服务和文件共享协议集成在一个平台上的便利性。它的独特之处在于&am…

Unity中Shader矩阵的转置矩阵

文章目录 前言一、转置的表示二、转置矩阵三、转置矩阵的总结1、(A^T^)^T^ A2、(A B)^T^ A^T^ B^T^3、(kA)^T^ kA^T^ (k为实数)4、(AB)^T^ B^T^A^T^5、如果 A A^T^ 则称A为对称矩阵6、如果 AA^T^ I(单位矩阵)&#xff0c;则称 A 为正交矩阵&#xff0c;同时 A^T^ A^-1…

复杂数据统计与R语言程序设计实验二

1、创建一个对象&#xff0c;并进行数据类型的转换、判别等操作&#xff0c;步骤如下。 ①使用命令清空工作空间&#xff0c;创建一个对象x&#xff0c;内含元素为序列&#xff1a;1&#xff0c;3&#xff0c;5&#xff0c;6&#xff0c;8。 ②判断对象x是否为数值型数据。 ③…

OpenGL 的学习之路-4(变换)

三大变换&#xff1a;平移、缩放、旋转&#xff08;通过这三种变换&#xff0c;可以将图像移动到任意位置&#xff09; 其实&#xff0c;这背后对应的数学在 闫令琪 图形学课程 中有过一些了解&#xff0c;所以&#xff0c;理解起来也不觉得很困难。看程序吧。 1.画三角形&am…

OpenCV入门2——图像视频的加载与展示一些API

文章目录 题目OpenCV创建显示窗口OpenCV加载显示图片题目 OpenCV保存文件利用OpenCV从摄像头采集视频从多媒体文件中读取视频帧将视频数据录制成多媒体文件OpenCV控制鼠标关于[np.uint8](https://stackoverflow.com/questions/68387192/what-is-np-uint8) OpenCV中的TrackBar控…

初试 jmeter做压力测试

一.前言 压力测试是每一个Web应用程序上线之前都需要做的一个测试&#xff0c;他可以帮助我们发现系统中的瓶颈问题&#xff0c;减少发布到生产环境后出问题的几率&#xff1b;预估系统的承载能力&#xff0c;使我们能根据其做出一些应对措施。所以压力测试是一个非常重要的步…

mac无法向移动硬盘拷贝文件怎么解决?不能读取移动硬盘文件怎么解决

有时候我们在使用mac的时候&#xff0c;会遇到一些问题&#xff0c;比如无法向移动硬盘拷贝文件或者不能读取移动硬盘文件。这些问题会给我们的工作和生活带来不便&#xff0c;所以我们需要找到原因和解决办法。本文将为你介绍mac无法向移动硬盘拷贝文件怎么回事&#xff0c;以…

CTFSHOW 文件上传

web151 JS前端绕过 直接上传 png的图片马 然后抓包修改为php asystem("ls /var/www/html"); asystem("cat /var/www/html/flag.php"); web152 和151一样的方法也可以实现上传 asystem("ls /var/www/html"); asystem("cat /var/www/html…