Spring Boot 实现文件分片上传和下载
文章目录
- 一、原理分析
- 1.1 文件分片
- 1.2 断点续传和断点下载
- 1.2 文件分片下载的 HTTP 参数
- 二、文件上传功能实现
- 2.1 客户端(前端)
- 2.2 服务端
- 三、文件下载功能实现
- 3.1 客户端(前端)
- 3.2 服务端
- 四、功能测试
- 4.1 文件上传功能测试
- 4.2 文件下载功能实现
- 参考资料
完整案例代码:java-demos/spring-boot-demos/spring-boot-file at main · idealzouhu/java-demos (github.com)
一、原理分析
断点上传和下载通常需要支持文件分片。
- 断点上传:上传大文件时,支持从上一次中断的位置继续上传。
- 断点下载:下载大文件时,支持从上一次中断的位置继续下载。
1.1 文件分片
文件分片的核心思想是将服务器上的大文件拆分成若干个小文件,等这些小份文件都下载好了之后,最后将小文件合并成一个完整的大文件。
以文件分片上传为例,客户端责任为:
-
分片: 将文件切割成小片。
-
记录上传进度: 记录哪些分片已上传、哪些还未上传。
-
上传分片: 通过 HTTP 请求将每个分片上传到服务端。
服务端责任为:
- 接收分片: 将每个分片临时存储。
- 记录已接收分片: 记录分片的索引、大小等信息,以防止重复上传。
- 文件合并: 在所有分片上传完成后,将它们合并成完整的文件。
1.2 断点续传和断点下载
断点续传依赖于客户端和服务端的进度记录。
- 客户端:在上传分片时记录当前上传到第几块,下一次可以从该分片继续上传。
- 服务端:通过每个分片的编号记录已接收的分片,检查是否还需要接收未完成的分片。
1.2 文件分片下载的 HTTP 参数
文件分片下载主要依赖于 HTTP 请求头的 Range 参数来实现。 Range 参数用于HTTP请求中,允许客户端请求特定字节范围的内容,而不是整个资源。这通常用于下载大文件时,使得客户端可以实现分块下载、断点续传等功能。
例如,客户端只希望获取从200字节到400字节的内容。
GET /path/to/file.txt HTTP/1.1
Host: example.com
Range: bytes=200-400
服务器处理请求并返回一个206 Partial Content响应。
HTTP/1.1 206 Partial Content
Content-Range: bytes 200-400/1000
Content-Length: 201
Content-Type: text/plain...(这里是文件的第200到400字节的内容)...
二、文件上传功能实现
在实现文件上传功能中, 客户端负责分片上传并记录上传进度,服务端负责接收和合并分片。同时,本文使用分片编号来实现的,并没有使用 range 参数。
2.1 客户端(前端)
创建 fragmentUpload.html 文件,具体代码为:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>File Chunk Upload</title>
</head>
<body>
<h2>File Chunk Upload with Resume</h2>
<input type="file" id="fileInput">
<button onclick="uploadFile()">Upload</button>
<div id="progress"></div><script>const CHUNK_SIZE = 5 * 1024 * 1024; // 1MBlet uploadedChunks = 0;async function uploadFile() {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert('Please select a file!');return;}let totalChunks = Math.ceil(file.size / CHUNK_SIZE);while (uploadedChunks < totalChunks) {let start = uploadedChunks * CHUNK_SIZE;let end = Math.min(start + CHUNK_SIZE, file.size);let chunk = file.slice(start, end);let formData = new FormData();formData.append('chunk', chunk);formData.append('fileName', file.name);formData.append('chunkNumber', uploadedChunks + 1);formData.append('totalChunks', totalChunks);try {await fetch('/upload-chunk', {method: 'POST',body: formData});uploadedChunks++;document.getElementById('progress').innerText = `Uploaded chunk ${uploadedChunks} of ${totalChunks}`;} catch (error) {alert('Upload failed. Retrying...');break;}}}
</script>
</body>
</html>
前端通过 File.slice() 方法实现文件分片,并逐个上传到服务器。如果上传过程中中断,记录上传进度,并在恢复时从中断处继续上传。
2.2 服务端
服务端接收分片,保存到指定目录,并在所有分片上传完成后合并它们。其中,每个上传的分片通过 chunkNumber 参数被保存到指定的临时目录中。
@RequiredArgsConstructor
@RestController
public class FileChunkUploadController {// 文件临时目录,用于保存上传的分片文件private static final String TEMP_DIR = "D:\\Learning\\temp\\";// 文件上传目录,用于保存合并后的文件private static final String UPLOAD_DIR = "D:\\Learning\\upload\\";/*** 处理单个分片上传请求* <p>* 当文件较大或网络条件不稳定时,客户端可以将文件分割成多个分片分别上传* 这个方法负责接收单个分片,并将其保存到临时目录当所有分片上传完成后,将它们合并成一个完整的文件* </p>** @param chunk 分片文件,包含文件的一部分* @param fileName 原始文件名,用于合并分片时命名* @param chunkNumber 当前分片的编号,从1开始* @param totalChunks 总分片数,用于判断是否所有分片都已上传* @return 分片上传的状态信息* @throws IOException 如果文件操作失败*/@PostMapping("/upload-chunk")public ResponseEntity<String> uploadChunk(@RequestParam("chunk") MultipartFile chunk,@RequestParam("fileName") String fileName,@RequestParam("chunkNumber") int chunkNumber,@RequestParam("totalChunks") int totalChunks) throws IOException {// 保存分片到临时目录File tempFile = new File(TEMP_DIR + fileName + "_" + chunkNumber);chunk.transferTo(tempFile);// 检查是否所有分片都已上传if (isAllChunksUploaded(fileName, totalChunks)) {mergeChunks(fileName, totalChunks);}return ResponseEntity.ok("Chunk " + chunkNumber + " uploaded");}// 判断是否所有分片都上传完毕private boolean isAllChunksUploaded(String fileName, int totalChunks) {for (int i = 1; i <= totalChunks; i++) {File file = new File(TEMP_DIR + fileName + "_" + i);if (!file.exists()) {return false;}}return true;}// 合并所有分片private void mergeChunks(String fileName, int totalChunks) throws IOException {File mergedFile = new File(UPLOAD_DIR + fileName);try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {for (int i = 1; i <= totalChunks; i++) {File chunkFile = new File(TEMP_DIR + fileName + "_" + i);try (FileInputStream fis = new FileInputStream(chunkFile)) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = fis.read(buffer)) != -1) {fos.write(buffer, 0, bytesRead);}}chunkFile.delete(); // 删除分片}}}
}
三、文件下载功能实现
在实现文件下载功能中,
- 客户端负责接收分片,记录上传进度,以及最后的合并分片。
- 服务端负责下载分片。
3.1 客户端(前端)
客户端实现逻辑为:
-
存储所有分片:使用
blobParts数组来存储每个下载的分片的Blob对象。 -
整合分片:在所有分片下载完成后,使用
new Blob(blobParts)创建一个完整的Blob,然后生成一个 URL 并触发下载。 -
触发下载:在所有分片下载并整合完后,创建一个下载链接并点击它以触发下载。
创建 fragmentDownload.html 文件,具体代码为:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>File Download with Chunking</title><style>body {font-family: Arial, sans-serif;}#downloadForm {margin: 20px;}#chunkSize {width: 100px;}#progress {margin-top: 20px;}</style>
</head>
<body>
<h1>File Download with Chunking</h1>
<div id="downloadForm"><label for="fileName">File Name:</label><input type="text" id="fileName" placeholder="example.txt" required><br><br><label for="chunkSize">Chunk Size (bytes):</label><input type="number" id="chunkSize" value="1048576" required> <!-- 1 MB --><br><br><button id="downloadButton">Download File</button>
</div>
<div id="progress"><p>Download Progress: <span id="progressText">0</span>%</p>
</div><script>async function getFileLength(fileName) {const response = await fetch(`download-chunk?fileName=${fileName}`, { method: 'HEAD' });const contentLength = response.headers.get('content-length');return parseInt(contentLength, 10);}async function downloadFile(fileName, start, end) {const range = `bytes=${start}-${end}`;const response = await fetch(`download-chunk?fileName=${fileName}`, {method: 'GET',headers: {'Range': range,}});if (response.status === 206) {return await response.blob(); // 返回 Blob 数据} else {throw new Error(`Error: ${response.status}`);}}async function downloadFileInChunks(fileName, chunkSize) {const fileLength = await getFileLength(fileName);let start = 0;let end = Math.min(chunkSize - 1, fileLength - 1);const totalChunks = Math.ceil(fileLength / chunkSize);let downloadedChunks = 0;const blobParts = []; // 存储所有分片的 Blobwhile (start < fileLength) {try {const blob = await downloadFile(fileName, start, end);blobParts.push(blob); // 将分片加入数组downloadedChunks++;const progressPercentage = Math.round((downloadedChunks / totalChunks) * 100);document.getElementById('progressText').innerText = progressPercentage;start += chunkSize;end = Math.min(start + chunkSize - 1, fileLength - 1);} catch (error) {console.error(`Failed to download chunk: ${error}`);break;}}// 所有分片下载完成后,整合成一个 Blobconst finalBlob = new Blob(blobParts);const url = URL.createObjectURL(finalBlob);const a = document.createElement('a');a.href = url;a.download = fileName; // 设置下载文件名document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url); // 释放内存if (start >= fileLength) {alert('Download completed!');}}document.getElementById("downloadButton").onclick = function() {const fileName = document.getElementById("fileName").value;const chunkSize = parseInt(document.getElementById("chunkSize").value);downloadFileInChunks(fileName, chunkSize).catch(console.error);};
</script>
</body>
</html>
在上述代码中,
downloadFileInChunks函数:控制文件下载的分片逻辑,循环调用downloadFile函数进行分片下载。downloadFile函数:执行实际的文件下载请求。其中,HTTP 请求设置为xhr.open("GET", download-chunk?fileName=${fileName}, true);
3.2 服务端
服务端主要实现根据 range 参数返回对应的文件分片即可。
@RestController
public class FileChunkDownloadController {private static final String FILE_DIRECTORY = "D:\\Program Files\\";// 处理文件下载请求的方法@GetMapping("/download-chunk")public ResponseEntity<StreamingResponseBody> downloadFile(@RequestParam String fileName,@RequestHeader(value = HttpHeaders.RANGE, required = false) String range) throws IOException {// 根据文件名构建文件对象File file = new File(FILE_DIRECTORY, fileName);if (!file.exists()) {return ResponseEntity.status(HttpStatus.NOT_FOUND).build();}// 获取文件长度, 初始化下载的起始和结束位置long fileLength = file.length();long start = 0;long end = fileLength - 1;// 处理 Range 请求if (range != null) {// 解析 Range 请求中的起始和结束位置String[] ranges = range.replace("bytes=", "").split("-");start = Long.parseLong(ranges[0]);if (ranges.length > 1 && !ranges[1].isEmpty()) {end = Long.parseLong(ranges[1]);}}// 确保请求的范围合法if (start > end || start >= fileLength) {// 如果请求范围不合法,返回 416 REQUESTED RANGE NOT SATISFIABLEreturn ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).header(HttpHeaders.CONTENT_RANGE, "bytes */" + fileLength).build();}// 如果结束位置超出文件长度,调整结束位置if (end >= fileLength) {end = fileLength - 1;}// 设置内容长度long contentLength = end - start + 1;// 使用 final 关键字定义的变量final long finalStart = start;final long finalEnd = end;final long finalContentLength = contentLength;// 创建 StreamingResponseBody 对象StreamingResponseBody responseBody = outputStream -> {try (InputStream inputStream = new FileInputStream(file)) {inputStream.skip(finalStart); // 跳过起始位置byte[] buffer = new byte[1024];int bytesRead;long bytesToRead = finalContentLength;while (bytesToRead > 0 && (bytesRead = inputStream.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead))) != -1) {outputStream.write(buffer, 0, bytesRead);bytesToRead -= bytesRead;}} catch (IOException e) {// 打印异常信息e.printStackTrace();}};return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE).header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").header(HttpHeaders.CONTENT_RANGE, "bytes " + finalStart + "-" + finalEnd + "/" + fileLength).header(HttpHeaders.ACCEPT_RANGES, "bytes").header(HttpHeaders.CONTENT_LENGTH, String.valueOf(finalContentLength)).body(responseBody);}
}
四、功能测试
4.1 文件上传功能测试
打开客户端 http://localhost:8080/fragmentUpload.html, 上传文件。

在测试断点续传的过程中,重启服务端,然后再次点击客户端前端界面的 Upload 按钮。
4.2 文件下载功能实现
打开客户端 http://localhost:8080/fragmentDownload.html, 上传文件 。
不能直接使用 http://localhost:8080/download-chunk?fileName=demo.txt 来访问,会出现问题。

参考资料
实现大文件的断点下载、分片下载 (qq.com)
相关文章:
Spring Boot 实现文件分片上传和下载
文章目录 一、原理分析1.1 文件分片1.2 断点续传和断点下载1.2 文件分片下载的 HTTP 参数 二、文件上传功能实现2.1 客户端(前端)2.2 服务端 三、文件下载功能实现3.1 客户端(前端)3.2 服务端 四、功能测试4.1 文件上传功能测试4.2 文件下载功能实现 参考资料 完整案例代码&…...
夹逼准则求数列极限(复习总结)
记住这两个准则,然后我们就开始看题目 因为是证明题,所以要放缩到什么值已经是确定的了。也就是放缩到0,然后很明显地可以看出前面已经有一个可以使得极限是0了,并且后面的值明显小于1,就是逐渐缩小的趋势,…...
【python】OpenCV—WaterShed Algorithm(1)
文章目录 1、功能描述2、代码实现3、完整代码4、效果展示5、涉及到的库函数5.1、cv2.pyrMeanShiftFiltering5.2、cv2.morphologyEx5.3、cv2.distanceTransform5.4、cv2.normalize5.5、cv2.watershed 6、参考 1、功能描述 基于分水岭算法对图片进行分割 分水岭分割算法&#x…...
查找与排序-插入排序
思考:在把待排序的元素插入已经有序的子序列中时,是不是一定要逐一比较?有没有改进方法? 在查找插入位置的时候可以采用折半(二分)搜索的办法。 一、折半插入排序 1.折半插入排序算法的基本思想 假设待…...
JAVA基础:多线程 (学习笔记)
多线程 一,什么是线程? 程序:为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码进程:程序的一次执行过程。 正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的…...
盲盒小程序/APP系统,市场发展下的新机遇
当下,年轻人热衷于各种潮玩商品,尤其是一盲盒为主的潮流玩具风靡市场,吸引了众多入局者。随着互联网信息技术的快速发展,各类线上盲盒小程序又进一步推动了盲盒市场的发展,成为年轻人拆盲盒的主要阵地。在盲盒经济中&a…...
Unity3D LayoutGroup组件详解
Unity3D中的LayoutGroup组件是一种强大的工具,用于动态调整UI元素的布局。它主要包括三种类型:Horizontal Layout Group(水平布局组)、Vertical Layout Group(垂直布局组)和Grid Layout Group(网…...
[NeetCode 150] Foreign Dictionary
Foreign Dictionary There is a foreign language which uses the latin alphabet, but the order among letters is not “a”, “b”, “c” … “z” as in English. You receive a list of non-empty strings words from the dictionary, where the words are sorted lex…...
小新学习K8s第一天之K8s基础概念
目录 一、Kubernetes(K8s)概述 1.1、什么是K8s 1.2、K8s的作用 1.3、K8s的功能 二、K8s的特性 2.1、弹性伸缩 2.2、自我修复 2.3、服务发现和负载均衡 2.4、自动发布(默认滚动发布模式)和回滚 2.5、集中化配置管理和密钥…...
如何用终端批量修改一个文件夹里面所有图片的后缀名?
步骤: winr ,然后输入cmd,打开终端 使用cd命令导航到要修改图片后缀名的文件夹。eg.我的该文件夹(C:\dog)下,保存的图片。(cd和文件目录之间要有空格)批量改变后缀名,假如让后缀名全部要从 ".webp&q…...
关于AI网络架构的文章
思科OCP anounce了800G 51.2T G200-based minipack3 switch。对比之前Tesla anounce的TTPoE。真的很好奇,谁是AI-networking的未来,以及思科是否走在正确的路上,以及S1背后的技术。 大致浏览了相关的文章,先mark住,回…...
【ChatGPT】在多轮对话中引导 ChatGPT 保持一致性
在多轮对话中引导 ChatGPT 保持一致性 多轮对话是与 ChatGPT 等对话模型互动时的一大特点,特别是在复杂任务和长时间对话中,保持对话的一致性显得尤为重要。用户往往希望 ChatGPT 能够在上下文中理解先前的对话内容,避免反复重申问题或者给出…...
【Chapter 7】因果推断中的机器学习:从T-学习器到双重稳健估计
随着机器学习技术的发展,数据科学家们开始探索如何将这些先进的方法应用于因果推断问题,尤其是处理异质性效应(Effect Heterogeneity)时。本章将介绍几种基于机器学习的因果推断方法,包括T-学习器、X-学习器和双重稳健…...
vim的使用方法
常见的命令可参考: Linux vi/vim | 菜鸟教程www.runoob.com/linux/linux-vim.html编辑https://link.zhihu.com/?targethttps%3A//www.runoob.com/linux/linux-vim.html 1. vim的工作模式 vi/vim 共分为三种模式,命令模式、编辑输入模式和末行&am…...
OPPO携手比亚迪共同探索手机与汽车互融新时代
10月23日,OPPO与比亚迪宣布签订战略合作协议,双方将共同推进手机与汽车的互融合作,这一合作也标志着两大行业巨头在技术创新和产业融合上迈出了重要一步,为手机与汽车的深度融合探索新的可能。 OPPO创始人兼首席执行官陈明永、OP…...
Apache Linkis:重新定义计算中间件
在大数据技术蓬勃发展的今天,我们见证了从单一计算引擎到多元化计算范式的演进。然而,随着企业数据应用场景的日益丰富,一个严峻的挑战逐渐显现:如何有效管理和协调各类计算引擎,使其能够高效协同工作?Apac…...
go gorm简单使用方法
GORM 是 Go 语言中一个非常流行的 ORM(对象关系映射)库,它允许开发者通过结构体来定义数据库表结构,并提供了丰富的 API 来操作数据库。 安装 go get -u gorm.io/gorm go get -u gorm.io/driver/sqlite表结构 在 gorm 中定义表结…...
【c++高级篇】--多任务编程/多线程(Thread)
目录 1.进程和线程的概念: 1.1 进程(Process): 1.2线程(Thread): 1.3 对比总结: 2.多线程编程: 2.1 基于线程的多任务处理(Thread)…...
【力扣专题栏】两数相加,如何实现存储在链表中的整数相加?
题解目录 1、题目描述解释2、算法原理解析3、代码编写(原始版本)4、代码编写(优化版本) 1、题目描述解释 2、算法原理解析 3、代码编写(原始版本) /*** Definition for singly-linked list.* struct ListN…...
SOLID - 接口隔离原则(Interface Segregation Principle)
SOLID - 接口隔离原则(Interface Segregation Principle) 定义 接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计中的五个基本原则之一,通常缩写为SOLID中的I。这一原则由Robert C. Martin提出&…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器
一、原理介绍 传统滑模观测器采用如下结构: 传统SMO中LPF会带来相位延迟和幅值衰减,并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF),可以去除高次谐波,并且不用相位补偿就可以获得一个误差较小的转子位…...
nnUNet V2修改网络——暴力替换网络为UNet++
更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...
论文阅读:LLM4Drive: A Survey of Large Language Models for Autonomous Driving
地址:LLM4Drive: A Survey of Large Language Models for Autonomous Driving 摘要翻译 自动驾驶技术作为推动交通和城市出行变革的催化剂,正从基于规则的系统向数据驱动策略转变。传统的模块化系统受限于级联模块间的累积误差和缺乏灵活性的预设规则。…...
