SpringBoot客户端实现分片、断点上传

news2024/11/19 15:39:20

客户端的分片上传和断点续传是指将一个文件拆分为多个分片,在网络不稳定或上传中断后,可以从中断处继续上传而不需要重新上传整个文件。 当需要上传大型文件时,客户端的分片上传和断点续传是一种常用的技术。它可以提高上传效率并减少网络中断造成的影响。

大文件下载流程

  1. 发起下载请求:客户端向服务器发送下载请求,请求包括要下载的文件名和希望的分片大小。
  2. 获取文件信息:客户端接收到服务器的响应,从响应头中获取文件的总大小(fSize),文件名(fName)以及服务器是否支持分片下载(Accept-Range字段)等信息。
  3. 计算分片数目:根据希望的分片大小和文件的总大小,计算出所需的分片数目。例如,如果希望每个分片大小为1MB,文件总大小为10MB,则需要分为10个分片。
  4. 请求下载分片:客户端根据计算得到的分片数目,循环发送多个请求去下载每个分片。每个请求需要设置Range头字段,指定要下载的分片范围。
  5. 接收分片数据:客户端接收到服务器返回的每个分片数据,并将其保存在本地。
  6. 合并分片:当所有分片都下载完成后,客户端将所有分片按顺序合并成完整的文件。
  7. 下载完成:客户端完成文件的下载,并进行必要的处理(例如保存到指定位置、通知用户下载完成等)。

 

引入依赖


        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
        <!-- 发起服务器请求-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
        </dependency>
        <!-- 发起服务器请求-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

 

服务端的代码

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;

@Controller
public class DownLoadController {
    private final static String utf8 = "utf-8";

    /**
     * 客户端获取要下载文件信息
     * @param request
     * @param response
     * @throws IOException
     */
    @GetMapping("/getFileInfo")
    public void getFileInfo(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String targetFileName = request.getHeader("FileName");
        response.setCharacterEncoding(utf8);
        //定义文件路径
        File file = new File("C:\\Users\\zhang\\Downloads\\"+targetFileName);
        long fSize = file.length();//获取长度
        // URLEncoder.encode方法会将文件名中的空格、中文等特殊字符转换为对应的十六进制编码
        String fileName = URLEncoder.encode(file.getName(),utf8);
        //根据前端传来的Range  判断支不支持分片下载
        response.setHeader("Accept-Range","bytes");
        //获取文件大小
        response.setHeader("fSize",String.valueOf(fSize));
        response.setHeader("fName",fileName);

    }

    /**
     * 分片下载大文件
     * @param request
     * @param response
     * @throws IOException
     */
    @RequestMapping("/down")
    public void downLoadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setCharacterEncoding(utf8);
        //定义文件路径
        File file = new File("C:\\Users\\zhang\\Downloads\\CentOS-7-x86_64-DVD-2009.iso");
        InputStream is = null;
        OutputStream os = null;
        try {
            //分片下载
            long fSize = file.length();//获取长度
            response.setContentType("application/x-download");
            // URLEncoder.encode方法会将文件名中的空格、中文等特殊字符转换为对应的十六进制编码
            String fileName = URLEncoder.encode(file.getName(),utf8);
            response.addHeader("Content-Disposition","attachment;filename="+fileName);
            //根据前端传来的Range  判断支不支持分片下载
            response.setHeader("Accept-Range","bytes");
            //获取文件大小
            response.setHeader("fSize",String.valueOf(fSize));
            response.setHeader("fName",fileName);
            //定义断点
            long pos = 0,last = fSize-1,sum = 0;
            //判断前端需不需要分片下载
            if (null != request.getHeader("Range")){
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                // 使用replaceAll方法将"Range"字段中的"bytes="替换为空字符串,得到真实的字节范围。
                String numRange = request.getHeader("Range").replaceAll("bytes=","");
                // 使用split方法将字节范围字符串按照"-"进行分割,得到开始位置和结束位置的字符串数组strRange
                String[] strRange = numRange.split("-");
                // 如果strRange的长度为2,说明客户端请求了指定范围的文件内容
                if (strRange.length == 2){
                    pos = Long.parseLong(strRange[0].trim());
                    last = Long.parseLong(strRange[1].trim());
                    //若结束字节超出文件大小 取文件大小
                    if (last>fSize-1){
                        last = fSize-1;
                    }
                // 如果strRange的长度不为2,则说明客户端请求的是从某个位置开始一直到文件结束的字节范围。
                }else {
                    //若只给一个长度  开始位置一直到结束
                    pos = Long.parseLong(numRange.replaceAll("-","").trim());
                }
            }

            long rangeLenght = last-pos+1;
            // 举例:bytes 500-1000/2000
            String contentRange = new StringBuffer("bytes").append(pos).append("-").append(last).append("/").append(fSize).toString();
            // 服务器返回的文件内容的字节范围
            response.setHeader("Content-Range",contentRange);
            // 服务器将返回的文件内容的字节范围长度。
            response.setHeader("Content-Lenght",String.valueOf(rangeLenght));
            os = new BufferedOutputStream(response.getOutputStream());
            is = new BufferedInputStream(new FileInputStream(file));
            is.skip(pos);//跳过已读的文件
            byte[] buffer = new byte[1024];
            int lenght = 0;
            //相等证明读完
            while (sum < rangeLenght){
                // 从输入流is中读取文件内容,并将实际读取的字节数赋值给length
                lenght = is.read(buffer,0, (rangeLenght-sum)<=buffer.length? (int) (rangeLenght - sum) :buffer.length);
                // 用于更新已经读取的字节总数
                sum = sum+lenght;
                os.write(buffer,0,lenght);
 
            }
            System.out.println("下载完成");
        }finally {
            if (is!= null){
                is.close();
            }
            if (os!=null){
                os.close();
            }
        }
    }
}

 客户端的代码

import org.apache.commons.io.FileUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.*;
import java.net.URLDecoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
public class DownloadClient {
    private final static long per_page = 1024l*1024l*50l;
    // 分片存储临时目录 当分片下载完后在目录中找到文件合并
    // 先确保有此目录
    private final static String down_path="D:\\tmp\\file";
    //多线程下载
    ExecutorService pool =  Executors.newFixedThreadPool(10);
    //文件大小 分片数量 文件名称
    //使用探测 获取变量
    //使用多线程分片下载
    //最后一个分片下载完 开始合并
    @RequestMapping("/downloadFile")
    public String downloadFile() throws IOException {
        FileInfo fileInfo = getFileInfo(targetFileName);
        if (fileInfo!= null){
            long pages = fileInfo.fSize/per_page;
            for (int i = 0; i <= pages; i++) {
                // i*per_page表示当前页的起始位置,(i+1)*per_page-1表示当前页的结束位置(因为是左闭右开区间,需要减去1)
                pool.submit(new Download(i*per_page,(i+1)*per_page-1,i,fileInfo.fName));
            }
        }
 
        return "成功";
    }
    class Download implements Runnable{
        long start;
        long end;
        long page;
        String fName;
 
        public Download(long start, long end, long page, String fName) {
            this.start = start;
            this.end = end;
            this.page = page;
            this.fName = fName;
        }
 
        @Override
        public void run() {
            try {
                download(start,end,page,fName);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private String targetFileName="CentOS-7-x86_64-DVD-2009.iso";
    //返回文件名 跟大小
    private FileInfo getFileInfo(String targetFileName) throws IOException {

        //需要知道  开始-结束 = 分片大小
        HttpClient client = HttpClients.createDefault();
        //httpclient进行请求,服务端接口
        HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/getFileInfo");
        //告诉服务端获取的文件信息
        httpGet.setHeader("FileName",targetFileName);
        HttpResponse response = client.execute(httpGet);
        String fSize = response.getFirstHeader("fSize").getValue();
        String fName= URLDecoder.decode(response.getFirstHeader("fName").getValue(),"utf-8");
        return new FileInfo(Long.valueOf(fSize),fName);
    }



    // 下载分片
    private void download(long start,long end,long page,String fName) throws IOException {
        //断点下载 文件存在不需要下载
        File file = new File(down_path, page + "-" + fName);
        //探测必须放行 若下载分片只下载一半就锻炼需要重新下载所以需要判断文件是否完整
        if (file.exists()&&page != -1&&file.length()==per_page){
            return ;
        }
        //需要知道  开始-结束 = 分片大小
        HttpClient client = HttpClients.createDefault();
        //httpclient进行请求,服务端接口
        HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/down");
        //告诉服务端做分片下载
        httpGet.setHeader("Range","bytes="+start+"-"+end);
        HttpResponse response = client.execute(httpGet);
        String fSize = response.getFirstHeader("fSize").getValue();
        fName= URLDecoder.decode(response.getFirstHeader("fName").getValue(),"utf-8");
        HttpEntity entity = response.getEntity();//获取文件流对象
        InputStream is = entity.getContent();
        //临时存储分片文件
        FileOutputStream fos = new FileOutputStream(file);
        byte[] buffer = new byte[1024];//定义缓冲区
        int ch;
        while ((ch = is.read(buffer)) != -1){
            fos.write(buffer,0,ch);
        }
        is.close();
        fos.flush();
        fos.close();
        //判断是不是最后一个分片,end - Long.valueOf(fSize)来判断当前分片的结束位置是否超过了文件的总大小。如果结果大于0,说明当前分片是最后一个分片。
        if (end-Long.valueOf(fSize)>0){
            //合并
            try {
                mergeFile(fName,page);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
    // 合并文件
    private void mergeFile(String fName, long page) throws Exception {
        //归并文件位置
        File file = new File(down_path, fName);
        BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file));
        for (int i = 0; i <= page; i++) {
            File tempFile = new File(down_path, i + "-" + fName);
            //分片没下载或者没下载完需要等待
            while (!file.exists()||(i!=page&&tempFile.length()<per_page)){
                Thread.sleep(100);
            }
            byte[] bytes = FileUtils.readFileToByteArray(tempFile);
            os.write(bytes);
            os.flush();
            tempFile.delete();
        }
        os.flush();
        os.close();
    }
 
    //使用内部类实现
    class FileInfo{
        long fSize;
        String fName;
 
        public FileInfo(long fSize, String fName) {
            this.fSize = fSize;
            this.fName = fName;
        }
    }
}

测试

访问/localhost:8080/downloadFile

生成分片

合并文件完成

 

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

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

相关文章

【计算机网络面试题(62道)】

文章目录 计算机网络面试题&#xff08;62道&#xff09;基础1.**说下计算机网络体系结构2.说一下每一层对应的网络协议有哪些&#xff1f;3.那么数据在各层之间是怎么传输的呢&#xff1f; 网络综合4.**从浏览器地址栏输入 url 到显示主页的过程&#xff1f;5.说说 DNS 的解析…

Vscode配置C#编程环境(win10)

目录 1、安装好Vscode 2、下载安装.NetCore SDK 3、配置C#环境 3.1 打开Vscode并下载扩展 3.2 Vscode中打开文件夹并配置环境 3.3 调试运行 1、安装好Vscode 2、下载安装.NetCore SDK 官网如下&#xff0c;下载完成后双击打开一路走到底就行.NetCore SDK官网 软件显示安…

利用freesurfer6进行海马分割的环境配置和步骤,以及获取海马体积

利用freesurfer6进行海马分割的环境配置和步骤 Matlab Runtime 安装1. 运行recon-all&#xff1a;2. 利用 recon-all -s subj -hippocampal-subfields-T1 进行海马分割3. 结束后需要在/$SUBJECTS_DIR/subject/的文件夹/mri路径下输入下面的代码查看分割情况4. 在文件SUBJECTS_D…

连续波雷达相关内容简介

连续波雷达(CW雷达)连续发射高频信号。回波信号被永久接收和处理。 一、未调制连续波雷达 未调制的连续波雷达除了振荡的相位之外没有其他时间参考。它只能用于测量小于波长的距离。以上的一切都极为含糊。因此,它只能用作多普勒雷达或控制已知距离的恒定性。 二、调频连…

VUE3照本宣科——eslint与prettier

VUE3照本宣科——eslint与prettier VUE3照本宣科系列导航 前言一、eslint1.配置文件2.配置规则3.忽略错误 二、prettier三、总结 VUE3照本宣科系列导航 1.VUE3照本宣科——认识VUE3 2.VUE3照本宣科——应用实例API与setup 3.VUE3照本宣科——响应式与生命周期钩子 4.VUE3照本宣…

关于Go语言的底层,Channel

1.Channel 介绍一下Channel&#xff08;有缓冲和无缓冲&#xff09; Go 语言中&#xff0c;不要通过共享内存来通信&#xff0c;而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型&#xff0c;中文可以叫做通信顺序进程&#xff0c;是通过 gor…

剑指offer——JZ54 二叉树搜索树的第k个节点 解题思路与具体代码【C++】

一、题目描述与要求 二叉搜索树的第k个节点_牛客题霸_牛客网 (nowcoder.com) 题目描述 给定一棵结点数为n 二叉搜索树&#xff0c;请找出其中的第 k 小的TreeNode结点值。 1.返回第k小的节点值即可 2.不能查找的情况&#xff0c;如二叉树为空&#xff0c;则返回-1&#xf…

Sql server 使用DBCC Shrinkfile 收缩日志文件

磁盘空间有限&#xff0c;需要收缩日志文件释放空间。 数据库名称上右击属性->文件,逻辑名称日志文件默认名称为“_log”结尾。 alter database 数据库 set recovery simple dbcc shrinkfile(XXX_log,2,truncateonly) alter database 数据库 set recovery full

webp批量转换为png、jpg工具

webp批量转换为png、jpg工具 链接&#xff1a;https://pan.baidu.com/s/1NZArgbiF88_qBbAIiUR4qQ 提取码&#xff1a;2sqs –来自百度网盘超级会员V5的分享

IO流 之 转换流( InputStreamReader 字节输入转换流 和 OutputStreamWriter 字节输出转换流)

当文本文件和代码的编码不一致时&#xff0c;使用字符流会导致读取出来的文字乱码。如下&#xff1a; 读取文件的编码时GBK编码。 代码的编码时UTF-8的编码。 程序运行出来中文则是乱码。 这里就要使用到转换流了。 字节转换流 InputStreamReader 字节输入转换流 使用步骤…

一文搞懂时间序列ARIMA模型

文章目录 1 ARIMA的定义2 差分(differencing)2.1 Order&#xff1a;差分的阶数2.2 Lag&#xff1a;差分的滞后2.3 滞后运算/滞后算子/延迟算子2.4 关于差分的两个误解 3 ARIMA的平稳性4 ACF与PACF5 时序模型的选择与评估5.1 超参数p、q、d的确定5.2 时间序列的评估指标 1 ARIMA…

Linux自用笔记

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Linux相关 ✨特色专栏&#xff1a; My…

码支付添加银行转账功能,手动回调

在后台中通道列表先加上 路径&#xff1a;【后台】 - > 【通道管理】 - > 【新增】 总后台页面通道类型加上支付类型yh_pay和通道yhzz添加后存到数据库admin_chanel上 修改商户页面的【新增通道】页面 /view/index/channel/index.html 添加一个支付通道 <option…

【算法训练-数组 三】【数组矩阵】螺旋矩阵、旋转图像、搜索二维矩阵

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是螺旋矩阵&#xff0c;使用【二维数组】这个基本的数据结构来实现 螺旋矩阵【EASY】 二维数组的结构特性入手 题干 解题思路 根据题目示例 mat…

【DevOps】搭建你的第一个 Docker 应用栈

搭建你的第一个 Docker 应用栈 1.Docker 集群部署2.第一个 Hello World2.1 获取应用栈各节点所需镜像2.2 应用栈容器节点互联2.3 应用栈容器节点启动2.4 应用栈容器节点的配置2.4.1 Redis Master 主数据库容器节点的配置2.4.2 Redis Slave 从数据库容器节点的配置2.4.3 Redis 数…

sql server查询结果:行转列、XML形式

1.普通查询 SELECT M.name From Menu M INNER JOIN MenuRoleRelation MRR ON M.idmrr.MenuId AND MRR.RoleId1; 结果&#xff1a; 2.做xml字符串返回 最后面加上&#xff1a;FOR XML PATH() 结果&#xff1a; 3.可以改为逗号分隔 SELECT ,M.name From Menu M INNER JOIN Me…

深度学习——深度学习计算一

深度学习——深度学习计算一 文章目录 前言一、层和块1.1. 自定义块1.2. 顺序块1.3. 在前向传播函数中执行代码1.4. 小结 二、参数管理2.1. 参数访问2.1.1. 目标参数2.1.2. 一次性访问所有参数2.1.3. 从嵌套块收集参数 2.2. 参数初始化2.2.1. 内置初始化2.2.2. 自定义初始化 2.…

常用Redis界面化软件

对于Redis的操作&#xff0c;前期有过介绍【Centos 下安装 Redis 及命令行操作】。而在Redis的日常开发调试中&#xff0c;可使用可视化软件方便进行操作。 本篇主要介绍Redis可视化的两款工具&#xff1a;Redis Desktop Manager和AnotherRedisDesktopManager。 1、Redis Desk…

unity 控制玩家物体

创建场景 放上一个plane&#xff0c;放上一个球 sphere&#xff0c;假定我们的球就是我们的玩家&#xff0c;使用控制键w a s d 来控制球也就是玩家移动。增加一个材质&#xff0c;把颜色改成绿色&#xff0c;把材质赋给plane&#xff0c;区分我们增加的白球。 增加组件和脚…

POJ 2104 K-th Number 平方分割 / 线段树

一、题目大意 长度为n&#xff08;n<100000&#xff09;的数组&#xff0c;进行m次查询&#xff08;m<5000&#xff09;&#xff0c;每次查询时&#xff0c;输入为 i j k&#xff0c;返回为数组 [i,j] 的分片里第k大数字&#xff08;1<i<j<n,k<j-i1) 二、解…