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提出&…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...
[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...
【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅
目录 前言 操作系统与驱动程序 是什么,为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中,我们在使用电子设备时,我们所输入执行的每一条指令最终大多都会作用到硬件上,比如下载一款软件最终会下载到硬盘上&am…...
如何在Windows本机安装Python并确保与Python.NET兼容
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
Copilot for Xcode (iOS的 AI辅助编程)
Copilot for Xcode 简介Copilot下载与安装 体验环境要求下载最新的安装包安装登录系统权限设置 AI辅助编程生成注释代码补全简单需求代码生成辅助编程行间代码生成注释联想 代码生成 总结 简介 尝试使用了Copilot,它能根据上下文补全代码,快速生成常用…...
