当前位置:   article > 正文

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

springboot整合文件断点续传

一、简介

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

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

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

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

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

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

实现示例1

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

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

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

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>cn.hutool</groupId>
  5. <artifactId>hutool-bom</artifactId>
  6. <version>5.8.18</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>
  12. <dependencies>
  13. <dependency>
  14. <groupId>cn.hutool</groupId>
  15. <artifactId>hutool-core</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.projectlombok</groupId>
  19. <artifactId>lombok</artifactId>
  20. <optional>true</optional>
  21. </dependency>
  22. </dependencies>

代码实现

ResourceController

  1. package com.example.insurance.controller;
  2. import javax.servlet.http.HttpServletRequest;
  3. import javax.servlet.http.HttpServletResponse;
  4. import java.io.IOException;
  5. import java.nio.file.Files;
  6. import java.nio.file.Path;
  7. import java.nio.file.Paths;
  8. import java.util.List;
  9. import com.example.insurance.common.ContentRange;
  10. import com.example.insurance.common.MediaContentUtil;
  11. import com.example.insurance.common.NioUtils;
  12. import lombok.extern.slf4j.Slf4j;
  13. import org.springframework.http.HttpHeaders;
  14. import org.springframework.http.HttpRange;
  15. import org.springframework.http.HttpStatus;
  16. import org.springframework.util.CollectionUtils;
  17. import org.springframework.util.StopWatch;
  18. import org.springframework.web.bind.annotation.GetMapping;
  19. import org.springframework.web.bind.annotation.PathVariable;
  20. import org.springframework.web.bind.annotation.RequestHeader;
  21. import org.springframework.web.bind.annotation.RequestMapping;
  22. import org.springframework.web.bind.annotation.RestController;
  23. /**
  24. * 内容资源控制器
  25. */
  26. @SuppressWarnings("unused")
  27. @Slf4j
  28. @RestController("resourceController")
  29. @RequestMapping(path = "/resource")
  30. public class ResourceController {
  31. /**
  32. * 获取文件内容
  33. *
  34. * @param fileName 内容文件名称
  35. * @param response 响应对象
  36. */
  37. @GetMapping("/media/{fileName}")
  38. public void getMedia(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
  39. @RequestHeader HttpHeaders headers) {
  40. // printRequestInfo(fileName, request, headers);
  41. String filePath = MediaContentUtil.filePath();
  42. try {
  43. this.download(fileName, filePath, request, response, headers);
  44. } catch (Exception e) {
  45. log.error("getMedia error, fileName={}", fileName, e);
  46. }
  47. }
  48. /**
  49. * 获取封面内容
  50. *
  51. * @param fileName 内容封面名称
  52. * @param response 响应对象
  53. */
  54. @GetMapping("/cover/{fileName}")
  55. public void getCover(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
  56. @RequestHeader HttpHeaders headers) {
  57. // printRequestInfo(fileName, request, headers);
  58. String filePath = MediaContentUtil.filePath();
  59. try {
  60. this.download(fileName, filePath, request, response, headers);
  61. } catch (Exception e) {
  62. log.error("getCover error, fileName={}", fileName, e);
  63. }
  64. }
  65. // ======= internal =======
  66. private static void printRequestInfo(String fileName, HttpServletRequest request, HttpHeaders headers) {
  67. String requestUri = request.getRequestURI();
  68. String queryString = request.getQueryString();
  69. log.debug("file={}, url={}?{}", fileName, requestUri, queryString);
  70. log.info("headers={}", headers);
  71. }
  72. /**
  73. * 设置请求响应状态、头信息、内容类型与长度 等。
  74. * <pre>
  75. * <a href="https://www.rfc-editor.org/rfc/rfc7233">
  76. * HTTP/1.1 Range Requests</a>
  77. * 2. Range Units
  78. * 4. Responses to a Range Request
  79. *
  80. * <a href="https://www.rfc-editor.org/rfc/rfc2616.html">
  81. * HTTP/1.1</a>
  82. * 10.2.7 206 Partial Content
  83. * 14.5 Accept-Ranges
  84. * 14.13 Content-Length
  85. * 14.16 Content-Range
  86. * 14.17 Content-Type
  87. * 19.5.1 Content-Disposition
  88. * 15.5 Content-Disposition Issues
  89. *
  90. * <a href="https://www.rfc-editor.org/rfc/rfc2183">
  91. * Content-Disposition</a>
  92. * 2. The Content-Disposition Header Field
  93. * 2.1 The Inline Disposition Type
  94. * 2.3 The Filename Parameter
  95. * </pre>
  96. *
  97. * @param response 请求响应对象
  98. * @param fileName 请求的文件名称
  99. * @param contentType 内容类型
  100. * @param contentRange 内容范围对象
  101. */
  102. private static void setResponse(
  103. HttpServletResponse response, String fileName, String contentType,
  104. ContentRange contentRange) {
  105. // http状态码要为206:表示获取部分内容
  106. response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
  107. // 支持断点续传,获取部分字节内容
  108. // Accept-Ranges:bytes,表示支持Range请求
  109. response.setHeader(HttpHeaders.ACCEPT_RANGES, ContentRange.BYTES_STRING);
  110. // inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
  111. response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
  112. "inline;filename=" + MediaContentUtil.encode(fileName));
  113. // Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
  114. // Content-Range: bytes 0-10/3103,格式为bytes 开始-结束/全部
  115. response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange.toContentRange());
  116. response.setContentType(contentType);
  117. // Content-Length: 11,本次内容的大小
  118. response.setContentLengthLong(contentRange.applyAsContentLength());
  119. }
  120. /**
  121. * <a href="https://www.jianshu.com/p/08db5ba3bc95">
  122. * Spring Boot 处理 HTTP Headers</a>
  123. */
  124. private void download(
  125. String fileName, String path, HttpServletRequest request, HttpServletResponse response,
  126. HttpHeaders headers)
  127. throws IOException {
  128. Path filePath = Paths.get(path + fileName);
  129. if (!Files.exists(filePath)) {
  130. log.warn("file not exist, filePath={}", filePath);
  131. return;
  132. }
  133. long fileLength = Files.size(filePath);
  134. // long fileLength2 = filePath.toFile().length() - 1;
  135. // // fileLength=1184856, fileLength2=1184855
  136. // log.info("fileLength={}, fileLength2={}", fileLength, fileLength2);
  137. // 内容范围
  138. ContentRange contentRange = applyAsContentRange(headers, fileLength, request);
  139. // 要下载的长度
  140. long contentLength = contentRange.applyAsContentLength();
  141. log.debug("contentRange={}, contentLength={}", contentRange, contentLength);
  142. // 文件类型
  143. String contentType = request.getServletContext().getMimeType(fileName);
  144. // mimeType=video/mp4, CONTENT_TYPE=null
  145. log.debug("mimeType={}, CONTENT_TYPE={}", contentType, request.getContentType());
  146. setResponse(response, fileName, contentType, contentRange);
  147. // 耗时指标统计
  148. StopWatch stopWatch = new StopWatch("downloadFile");
  149. stopWatch.start(fileName);
  150. try {
  151. // case-1.参考网上他人的实现
  152. // if (fileLength >= Integer.MAX_VALUE) {
  153. // NioUtils.copy(filePath, response, contentRange);
  154. // } else {
  155. // NioUtils.copyByChannelAndBuffer(filePath, response, contentRange);
  156. // }
  157. // case-2.使用现成API
  158. NioUtils.copyByBio(filePath, response, contentRange);
  159. // NioUtils.copyByNio(filePath, response, contentRange);
  160. // case-3.视频分段渐进式播放
  161. // if (contentType.startsWith("video")) {
  162. // NioUtils.copyForBufferSize(filePath, response, contentRange);
  163. // } else {
  164. // // 图片、PDF等文件
  165. // NioUtils.copyByBio(filePath, response, contentRange);
  166. // }
  167. } finally {
  168. stopWatch.stop();
  169. log.info("download file, fileName={}, time={} ms", fileName, stopWatch.getTotalTimeMillis());
  170. }
  171. }
  172. private static ContentRange applyAsContentRange(
  173. HttpHeaders headers, long fileLength, HttpServletRequest request) {
  174. /*
  175. * 3.1. Range - HTTP/1.1 Range Requests
  176. * https://www.rfc-editor.org/rfc/rfc7233#section-3.1
  177. * Range: "bytes" "=" first-byte-pos "-" [ last-byte-pos ]
  178. *
  179. * For example:
  180. * bytes=0-
  181. * bytes=0-499
  182. */
  183. // Range:告知服务端,客户端下载该文件想要从指定的位置开始下载
  184. List<HttpRange> httpRanges = headers.getRange();
  185. String range = request.getHeader(HttpHeaders.RANGE);
  186. // httpRanges=[], range=null
  187. // httpRanges=[448135688-], range=bytes=448135688-
  188. log.debug("httpRanges={}, range={}", httpRanges, range);
  189. // 开始下载位置
  190. long firstBytePos;
  191. // 结束下载位置
  192. long lastBytePos;
  193. if (CollectionUtils.isEmpty(httpRanges)) {
  194. firstBytePos = 0;
  195. lastBytePos = fileLength - 1;
  196. } else {
  197. HttpRange httpRange = httpRanges.get(0);
  198. firstBytePos = httpRange.getRangeStart(fileLength);
  199. lastBytePos = httpRange.getRangeEnd(fileLength);
  200. }
  201. return new ContentRange(firstBytePos, lastBytePos, fileLength);
  202. }
  203. }

NioUtils

  1. package com.example.insurance.common;
  2. import javax.servlet.http.HttpServletResponse;
  3. import java.io.BufferedOutputStream;
  4. import java.io.InputStream;
  5. import java.io.OutputStream;
  6. import java.io.RandomAccessFile;
  7. import java.nio.MappedByteBuffer;
  8. import java.nio.channels.Channels;
  9. import java.nio.channels.FileChannel;
  10. import java.nio.file.Path;
  11. import java.nio.file.StandardOpenOption;
  12. import cn.hutool.core.io.IORuntimeException;
  13. import cn.hutool.core.io.IoUtil;
  14. import cn.hutool.core.io.NioUtil;
  15. import cn.hutool.core.io.StreamProgress;
  16. import cn.hutool.core.io.unit.DataSize;
  17. import lombok.extern.slf4j.Slf4j;
  18. import org.apache.catalina.connector.ClientAbortException;
  19. /**
  20. * NIO相关工具封装,主要针对Channel读写、拷贝等封装
  21. */
  22. @Slf4j
  23. public final class NioUtils {
  24. /**
  25. * 缓冲区大小 16KB
  26. *
  27. * @see NioUtil#DEFAULT_BUFFER_SIZE
  28. * @see NioUtil#DEFAULT_LARGE_BUFFER_SIZE
  29. */
  30. // private static final int BUFFER_SIZE = NioUtil.DEFAULT_MIDDLE_BUFFER_SIZE;
  31. private static final int BUFFER_SIZE = (int) DataSize.ofKilobytes(16L).toBytes();
  32. /**
  33. * <pre>
  34. * <a href="https://blog.csdn.net/qq_32099833/article/details/109703883">
  35. * Java后端实现视频分段渐进式播放</a>
  36. * 服务端如何将一个大的视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。
  37. * 文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。
  38. *
  39. * <a href="https://blog.csdn.net/qq_32099833/article/details/109630499">
  40. * 大文件分片上传前后端实现</a>
  41. * </pre>
  42. */
  43. public static void copyForBufferSize(
  44. Path filePath, HttpServletResponse response, ContentRange contentRange) {
  45. String fileName = filePath.getFileName().toString();
  46. RandomAccessFile randomAccessFile = null;
  47. OutputStream outputStream = null;
  48. try {
  49. // 随机读文件
  50. randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  51. // 移动访问指针到指定位置
  52. randomAccessFile.seek(contentRange.getStart());
  53. // 注意:缓冲区大小 2MB,视频加载正常;1MB时有部分视频加载失败
  54. int bufferSize = BUFFER_SIZE;
  55. //获取响应的输出流
  56. outputStream = new BufferedOutputStream(response.getOutputStream(), bufferSize);
  57. // 每次请求只返回1MB的视频流
  58. byte[] buffer = new byte[bufferSize];
  59. int len = randomAccessFile.read(buffer);
  60. //设置此次相应返回的数据长度
  61. response.setContentLength(len);
  62. // 将这1MB的视频流响应给客户端
  63. outputStream.write(buffer, 0, len);
  64. log.info("file download complete, fileName={}, contentRange={}",
  65. fileName, contentRange.toContentRange());
  66. } catch (ClientAbortException | IORuntimeException e) {
  67. // 捕获此异常表示用户停止下载
  68. log.warn("client stop file download, fileName={}", fileName);
  69. } catch (Exception e) {
  70. log.error("file download error, fileName={}", fileName, e);
  71. } finally {
  72. IoUtil.close(outputStream);
  73. IoUtil.close(randomAccessFile);
  74. }
  75. }
  76. /**
  77. * 拷贝流,拷贝后关闭流。
  78. *
  79. * @param filePath 源文件路径
  80. * @param response 请求响应
  81. * @param contentRange 内容范围
  82. */
  83. public static void copyByBio(
  84. Path filePath, HttpServletResponse response, ContentRange contentRange) {
  85. String fileName = filePath.getFileName().toString();
  86. InputStream inputStream = null;
  87. OutputStream outputStream = null;
  88. try {
  89. RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  90. randomAccessFile.seek(contentRange.getStart());
  91. inputStream = Channels.newInputStream(randomAccessFile.getChannel());
  92. outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  93. StreamProgress streamProgress = new StreamProgressImpl(fileName);
  94. long transmitted = IoUtil.copy(inputStream, outputStream, BUFFER_SIZE, streamProgress);
  95. log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  96. } catch (ClientAbortException | IORuntimeException e) {
  97. // 捕获此异常表示用户停止下载
  98. log.warn("client stop file download, fileName={}", fileName);
  99. } catch (Exception e) {
  100. log.error("file download error, fileName={}", fileName, e);
  101. } finally {
  102. IoUtil.close(outputStream);
  103. IoUtil.close(inputStream);
  104. }
  105. }
  106. /**
  107. * 拷贝流,拷贝后关闭流。
  108. * <pre>
  109. * <a href="https://www.cnblogs.com/czwbig/p/10035631.html">
  110. * Java NIO 学习笔记(一)----概述,Channel/Buffer</a>
  111. * </pre>
  112. *
  113. * @param filePath 源文件路径
  114. * @param response 请求响应
  115. * @param contentRange 内容范围
  116. */
  117. public static void copyByNio(
  118. Path filePath, HttpServletResponse response, ContentRange contentRange) {
  119. String fileName = filePath.getFileName().toString();
  120. InputStream inputStream = null;
  121. OutputStream outputStream = null;
  122. try {
  123. RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  124. randomAccessFile.seek(contentRange.getStart());
  125. inputStream = Channels.newInputStream(randomAccessFile.getChannel());
  126. outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  127. StreamProgress streamProgress = new StreamProgressImpl(fileName);
  128. long transmitted = NioUtil.copyByNIO(inputStream, outputStream,
  129. BUFFER_SIZE, streamProgress);
  130. log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  131. } catch (ClientAbortException | IORuntimeException e) {
  132. // 捕获此异常表示用户停止下载
  133. log.warn("client stop file download, fileName={}", fileName);
  134. } catch (Exception e) {
  135. log.error("file download error, fileName={}", fileName, e);
  136. } finally {
  137. IoUtil.close(outputStream);
  138. IoUtil.close(inputStream);
  139. }
  140. }
  141. /**
  142. * <pre>
  143. * <a href="https://blog.csdn.net/lovequanquqn/article/details/104562945">
  144. * SpringBoot Java实现Http方式分片下载断点续传+实现H5大视频渐进式播放</a>
  145. * SpringBoot 实现Http分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。
  146. * 二、Http分片下载断点续传实现
  147. * 四、缓存文件定时删除任务
  148. * </pre>
  149. */
  150. public static void copy(Path filePath, HttpServletResponse response, ContentRange contentRange) {
  151. String fileName = filePath.getFileName().toString();
  152. // 要下载的长度
  153. long contentLength = contentRange.applyAsContentLength();
  154. BufferedOutputStream outputStream = null;
  155. RandomAccessFile randomAccessFile = null;
  156. // 已传送数据大小
  157. long transmitted = 0;
  158. try {
  159. randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  160. randomAccessFile.seek(contentRange.getStart());
  161. outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  162. // 把数据读取到缓冲区中
  163. byte[] buffer = new byte[BUFFER_SIZE];
  164. int len = BUFFER_SIZE;
  165. //warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
  166. //不然会会先读取randomAccessFile,造成后面读取位置出错;
  167. while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buffer)) != -1) {
  168. outputStream.write(buffer, 0, len);
  169. transmitted += len;
  170. log.info("fileName={}, transmitted={}", fileName, transmitted);
  171. }
  172. //处理不足buffer.length部分
  173. if (transmitted < contentLength) {
  174. len = randomAccessFile.read(buffer, 0, (int) (contentLength - transmitted));
  175. outputStream.write(buffer, 0, len);
  176. transmitted += len;
  177. log.info("fileName={}, transmitted={}", fileName, transmitted);
  178. }
  179. log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  180. } catch (ClientAbortException e) {
  181. // 捕获此异常表示用户停止下载
  182. log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
  183. } catch (Exception e) {
  184. log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
  185. } finally {
  186. IoUtil.close(outputStream);
  187. IoUtil.close(randomAccessFile);
  188. }
  189. }
  190. /**
  191. * 通过数据传输通道和缓冲区读取文件数据。
  192. * <pre>
  193. * 当文件长度超过{@link Integer#MAX_VALUE}时,
  194. * 使用{@link FileChannel#map(FileChannel.MapMode, long, long)}报如下异常。
  195. * java.lang.IllegalArgumentException: Size exceeds Integer.MAX_VALUE
  196. * at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:863)
  197. * at com.example.insurance.controller.ResourceController.download(ResourceController.java:200)
  198. * </pre>
  199. *
  200. * @param filePath 源文件路径
  201. * @param response 请求响应
  202. * @param contentRange 内容范围
  203. */
  204. public static void copyByChannelAndBuffer(
  205. Path filePath, HttpServletResponse response, ContentRange contentRange) {
  206. String fileName = filePath.getFileName().toString();
  207. // 要下载的长度
  208. long contentLength = contentRange.applyAsContentLength();
  209. BufferedOutputStream outputStream = null;
  210. FileChannel inChannel = null;
  211. // 已传送数据大小
  212. long transmitted = 0;
  213. long firstBytePos = contentRange.getStart();
  214. long fileLength = contentRange.getLength();
  215. try {
  216. inChannel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.WRITE);
  217. // 建立直接缓冲区
  218. MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, firstBytePos, fileLength);
  219. outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  220. // 把数据读取到缓冲区中
  221. byte[] buffer = new byte[BUFFER_SIZE];
  222. int len = BUFFER_SIZE;
  223. // warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
  224. // 不然会会先读取file,造成后面读取位置出错
  225. while ((transmitted + len) <= contentLength) {
  226. inMap.get(buffer);
  227. outputStream.write(buffer, 0, len);
  228. transmitted += len;
  229. log.info("fileName={}, transmitted={}", fileName, transmitted);
  230. }
  231. // 处理不足buffer.length部分
  232. if (transmitted < contentLength) {
  233. len = (int) (contentLength - transmitted);
  234. buffer = new byte[len];
  235. inMap.get(buffer);
  236. outputStream.write(buffer, 0, len);
  237. transmitted += len;
  238. log.info("fileName={}, transmitted={}", fileName, transmitted);
  239. }
  240. log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  241. } catch (ClientAbortException e) {
  242. // 捕获此异常表示用户停止下载
  243. log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
  244. } catch (Exception e) {
  245. log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
  246. } finally {
  247. IoUtil.close(outputStream);
  248. IoUtil.close(inChannel);
  249. }
  250. }
  251. }

ContentRange

  1. package com.example.insurance.common;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. /**
  5. * 内容范围对象
  6. * <pre>
  7. * <a href="https://www.rfc-editor.org/rfc/rfc7233#section-4.2">
  8. * 4.2. Content-Range - HTTP/1.1 Range Requests</a>
  9. * Content-Range: "bytes" first-byte-pos "-" last-byte-pos "/" complete-length
  10. *
  11. * For example:
  12. * Content-Range: bytes 0-499/1234
  13. * </pre>
  14. *
  15. * @see org.apache.catalina.servlets.DefaultServlet.Range
  16. */
  17. @Getter
  18. @AllArgsConstructor
  19. public class ContentRange {
  20. /**
  21. * 第一个字节的位置
  22. */
  23. private final long start;
  24. /**
  25. * 最后一个字节的位置
  26. */
  27. private long end;
  28. /**
  29. * 内容完整的长度/总长度
  30. */
  31. private final long length;
  32. public static final String BYTES_STRING = "bytes";
  33. /**
  34. * 组装内容范围的响应头。
  35. * <pre>
  36. * <a href="https://www.rfc-editor.org/rfc/rfc7233#section-4.2">
  37. * 4.2. Content-Range - HTTP/1.1 Range Requests</a>
  38. * Content-Range: "bytes" first-byte-pos "-" last-byte-pos "/" complete-length
  39. *
  40. * For example:
  41. * Content-Range: bytes 0-499/1234
  42. * </pre>
  43. *
  44. * @return 内容范围的响应头
  45. */
  46. public String toContentRange() {
  47. return BYTES_STRING + ' ' + start + '-' + end + '/' + length;
  48. // return "bytes " + start + "-" + end + "/" + length;
  49. }
  50. /**
  51. * 计算内容完整的长度/总长度。
  52. *
  53. * @return 内容完整的长度/总长度
  54. */
  55. public long applyAsContentLength() {
  56. return end - start + 1;
  57. }
  58. /**
  59. * Validate range.
  60. *
  61. * @return true if the range is valid, otherwise false
  62. */
  63. public boolean validate() {
  64. if (end >= length) {
  65. end = length - 1;
  66. }
  67. return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
  68. }
  69. @Override
  70. public String toString() {
  71. return "firstBytePos=" + start +
  72. ", lastBytePos=" + end +
  73. ", fileLength=" + length;
  74. }
  75. }

StreamProgressImpl

  1. package com.example.insurance.common;
  2. import cn.hutool.core.io.StreamProgress;
  3. import lombok.AllArgsConstructor;
  4. import lombok.extern.slf4j.Slf4j;
  5. /**
  6. * 数据流进度条
  7. */
  8. @Slf4j
  9. @AllArgsConstructor
  10. public class StreamProgressImpl implements StreamProgress {
  11. private final String fileName;
  12. @Override
  13. public void start() {
  14. log.info("start progress {}", fileName);
  15. }
  16. @Override
  17. public void progress(long total, long progressSize) {
  18. log.debug("progress {}, total={}, progressSize={}", fileName, total, progressSize);
  19. }
  20. @Override
  21. public void finish() {
  22. log.info("finish progress {}", fileName);
  23. }
  24. }

MediaContentUtil

  1. package com.example.insurance.common;
  2. import java.net.URLDecoder;
  3. import java.net.URLEncoder;
  4. import java.nio.charset.StandardCharsets;
  5. /**
  6. * 文件内容辅助方法集
  7. */
  8. public final class MediaContentUtil {
  9. public static String filePath() {
  10. String osName = System.getProperty("os.name");
  11. String filePath = "/data/files/";
  12. if (osName.startsWith("Windows")) {
  13. filePath = "D:\" + filePath;
  14. }
  15. // else if (osName.startsWith("Linux")) {
  16. // filePath = MediaContentConstant.FILE_PATH;
  17. // }
  18. else if (osName.startsWith("Mac") || osName.startsWith("Linux")) {
  19. filePath = "/home/admin" + filePath;
  20. }
  21. return filePath;
  22. }
  23. public static String encode(String fileName) {
  24. return URLEncoder.encode(fileName, StandardCharsets.UTF_8);
  25. }
  26. public static String decode(String fileName) {
  27. return URLDecoder.decode(fileName, StandardCharsets.UTF_8);
  28. }
  29. }

实现示例2

代码实现

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

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

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

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

  1. @RestController
  2. @RequestMapping("/file")
  3. public class FileController {
  4. @PostMapping("/upload")
  5. public ResponseEntity<?> uploadFile(
  6. @RequestParam("file") MultipartFile file,
  7. @RequestParam("fileName") String fileName,
  8. @RequestParam("startPosition") long startPosition) {
  9. try { // 建立连接
  10. Socket socket = new Socket("localhost", 8080);
  11. OutputStream outputStream = socket.getOutputStream();
  12. ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
  13. // 分块传输文件
  14. FileInputStream fileInputStream = (FileInputStream) file.getInputStream();
  15. fileInputStream.skip(startPosition);
  16. byte[] buffer = new byte[1024];
  17. int len;
  18. while ((len = fileInputStream.read(buffer)) != -1) {
  19. outputStream.write(buffer, 0, len);
  20. }
  21. // 计算MD5值
  22. fileInputStream.getChannel().position(0);
  23. String md5 = DigestUtils.md5Hex(fileInputStream);
  24. // 与服务端比较MD5值
  25. InputStream inputStream = socket.getInputStream();
  26. ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
  27. String serverMd5 = (String) objectInputStream.readObject();
  28. if (!md5.equals(serverMd5)) {
  29. throw new RuntimeException("MD5值不匹配");
  30. }
  31. // 关闭连接
  32. objectOutputStream.close();
  33. outputStream.close();
  34. socket.close();
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
  38. }
  39. return ResponseEntity.ok().build();
  40. }
  41. }
2.编写服务端代码

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

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

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

  1. @RestController
  2. @RequestMapping("/file")
  3. public class FileController {
  4. private final String FILE_PATH = "/tmp/upload/";
  5. @PostMapping("/upload")
  6. public ResponseEntity<?> uploadFile(HttpServletRequest request, @RequestParam("fileName") String fileName) {
  7. try {
  8. // 建立连接
  9. ServerSocket serverSocket = new ServerSocket(8080);
  10. Socket socket = serverSocket.accept();
  11. InputStream inputStream = socket.getInputStream();
  12. ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
  13. // 接收文件
  14. String filePath = FILE_PATH + fileName;
  15. RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
  16. long startPosition = randomAccessFile.length();
  17. randomAccessFile.seek(startPosition);
  18. byte[] buffer = new byte[1024];
  19. int len;
  20. while ((len = inputStream.read(buffer)) != -1) {
  21. randomAccessFile.write(buffer, 0, len);
  22. } // 计算MD5值
  23. FileInputStream fileInputStream = new FileInputStream(filePath);
  24. String md5 = DigestUtils.md5Hex(fileInputStream);
  25. // 返回MD5值
  26. OutputStream outputStream = socket.getOutputStream();
  27. ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
  28. objectOutputStream.writeObject(md5); // 关闭连
  29. objectInputStream.close();
  30. inputStream.close();
  31. randomAccessFile.close();
  32. socket.close();
  33. serverSocket.close();
  34. } catch (Exception e) {
  35. e.printStackTrace();
  36. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
  37. }
  38. return ResponseEntity.ok().build();
  39. }
  40. }
3. 编写前端代码

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

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

以下是前端代码的实现:

  1. <html>
  2. <head>
  3. <meta charset="UTF-8">
  4. <title>Spring Boot File Upload</title>
  5. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  6. </head>
  7. <body><input type="file" id="file">
  8. <button onclick="upload()">Upload</button>
  9. <script> var file;
  10. var startPosition = 0;
  11. $('#file').on('change', function () {
  12. file = this.files[0];
  13. });
  14. function upload() {
  15. if (!file) {
  16. alert('Please select a file!');
  17. return;
  18. }
  19. var formData = new FormData();
  20. formData.append('file', file);
  21. formData.append('fileName', file.name);
  22. formData.append('startPosition', startPosition);
  23. $.ajax({
  24. url: '/file/upload',
  25. type: 'post',
  26. data: formData,
  27. cache: false,
  28. processData: false,
  29. contentType: false,
  30. success: function () {
  31. alert('Upload completed!');
  32. },
  33. error: function (xhr) {
  34. alert(xhr.responseText);
  35. },
  36. xhr: function () {
  37. var xhr = $.ajaxSettings.xhr();
  38. xhr.upload.onprogress = function (e) {
  39. if (e.lengthComputable) {
  40. var percent = e.loaded / e.total * 100;
  41. console.log('Upload percent: ' + percent.toFixed(2) + '%');
  42. }
  43. };
  44. return xhr;
  45. }
  46. });
  47. }</script>
  48. </body>
  49. </html>

总结

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

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

  • 上传文件的大小和分块的大小需要根据实际情况进行设置,以确保上传速度和服务器的稳定性。
  • 在上传过程中,需要对异常情况进行处理,以确保程序的健壮性。
  • 在上传完成后,需要对上传的文件进行校验,以确保传输的完整性。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/528109
推荐阅读
相关标签
  

闽ICP备14008679号