文件分片下载、断点下载、学习总结附源码

该博客详细介绍了如何在Java中实现分片下载功能,通过多线程下载大文件并最终合并。后端首先探测文件信息,然后启动线程池进行分片下载,每个线程负责一部分文件的下载。当所有分片下载完成后,进行文件合并。整个过程中,通过HTTP头信息处理断点续传,并在下载完成后清理临时文件。
摘要由CSDN通过智能技术生成

源码下载
提取码:gh84

1.实际开发中我们遇到过大的文件下载时候便可以使用分片下载的功能,实质就是采用了多线程进行并行的文件分片下载,最后进行文件合并

2.后端总体实现思路

  • 第一次进行文件信息的探测请求获取文件的大小等信息,并且在目录生成一个文件这个文件大小为1kb。
  • 获取到文件名称和文件大小的时候,我们就可以开启多个线程进行分片文件的下载
  • 当最后一个分片文件下载完成时,我们进行合并文件的操作,这里的操作和文件分片上传类似,当文件进行合并时我们需要去判断是否该分片文件下载完成如果没有完成的话需要进行sleep等待,等待分片文件下载完成再进行文件合并
  • 文件合并完成时我们需要删除每一个分片文件
  • 所有文件完成时需要删除第一次进行探测请求时生成的文件
  • 最后关闭流的操作
    3.后端代码粘贴
package com.minjiang.client;

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.net.URLEncoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @auther guannw
 * @create 2021/9/2 9:06
 */

@RestController
public class DownLoadClient {

    private final static  long PER_PAGE = 1024l * 1024l * 50l;//定义每一个分片大小

    private final static  String DOWN_PATH = "F:\\fileDownTest";//文件下载位置

    ExecutorService pool = Executors.newFixedThreadPool(10);//设定10个大小的固定线程池


    //探测下载 获取文件信息 例如 文件大小 文件名称
    //多线程文件下载
    //最后一个文件下载完之后, 开始合并
    @RequestMapping(value = "/downLoadFile")
    public String downLoadFile() throws IOException, InterruptedException {
        FileInfo fileInfo = download(0, 10, -1, null);//探测下载获得文件信息

        if (fileInfo != null){//可能文件已经存在所以可能返回值为空 ,所以需要对文件进行非空判断
            long pages = fileInfo.fileSize / PER_PAGE;//计算需要分片的数量
            for (long i = 0 ; i <= pages ; i++){
                pool.submit(new DownLoad(i * PER_PAGE,(i+1) * PER_PAGE -1,i,fileInfo.fileName ));
            }


        }
        return "success";
    }

    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 | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    class FileInfo{
        long fileSize;
        String fileName;

        public FileInfo(long fileSize, String fileName) {
            this.fileSize = fileSize;
            this.fileName = fileName;
        }
    }

    //文件起始位置和文件结束位置
     private FileInfo download(long start , long end , long page ,String fName) throws IOException, InterruptedException {//文件 开始位置 结束位置 第几个分片 临时文件吗

        File file = new File(DOWN_PATH,page+"-"+fName);
         //断点下载
        if(file.exists() && page != -1 && file.length() == PER_PAGE){//文件存在 以及不是探测下载和文件不完整(即文件大小不符合分片大小)
            return null;
        }

         HttpClient httpClient = HttpClients.createDefault();//创建一个http客户端
         HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/download");
         httpGet.setHeader("Range","bytes="+start+"-"+end);//设置分片下载头部信息 Range bytes=起始位置-结束位置
         HttpResponse response = httpClient.execute(httpGet);//发出Http get请求
         String fSize = response.getFirstHeader("fSize").getValue();
         fName = URLEncoder.encode(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();
         if ((end - Long.valueOf(fSize)) > 0){//最后一个分片到达的时候进行文件合并
            mergeFile(fName,page);
         }
         return new FileInfo(Long.parseLong(fSize),fName);

     }

    private void mergeFile(String fName, long page) throws IOException, InterruptedException {
        File file = new File(DOWN_PATH,fName);
        BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file));

        for (int i = 0; i <= page ; i++ ){
            File fileTemp = new File(DOWN_PATH,i+"-"+fName);
            while (!fileTemp.exists() || (i != page && fileTemp.length() < PER_PAGE)){
                Thread.sleep(100);
            }
            byte[] bytes = FileUtils.readFileToByteArray(fileTemp);
            os.write(bytes);
            os.flush();
            fileTemp.delete();
        }
        File file1 = new File(DOWN_PATH,-1+"-null");
        file1.delete();//删除探测文件
        os.flush();
        os.close();
    }
}

package com.minjiang.controller;

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

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

/**
 * @auther guannw
 * @create 2021/8/30 11:21
 */

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

    @RequestMapping(value="/download")
    public void downLoadFile(HttpServletRequest request , HttpServletResponse response){
        File file = new File("F:\\fileItem\\Idea 2019.zip");//自定义一个需要下载的文件
        response.setCharacterEncoding(utf8);//设置响应编码
        InputStream is = null;//输入文件流
        OutputStream os = null;//输出文件流

        try{
            //分片下载
            long fSize = file.length();
            response.setContentType("application/x-download");//告知前端这是一个下载请求
            String fileName = URLEncoder.encode(file.getName(),utf8);//设置文件名,预防中文乱码
            response.addHeader("Content-Disposition","attachment;filename="+fileName);
            response.setHeader("Accept-Range","bytes");//告知前端支持分片操作

            response.setHeader("fSize",String.valueOf(fSize));//告知客户端文件大小有多大
            response.setHeader("fName",fileName);//告知客户端文件大小有多大
            long pos = 0;  //设置文件分片pos 起始位置 last 结束位置 sum 总数
            long last = fSize - 1;
            long sum = 0;
            if(null != request.getHeader("Range")){//判断前端是否需要进行分片下载
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);// SC_PARTIAL_CONTENT 告知前端是进行分片下载

                //从Header 中获取 Range 信息 格式: bytes=1000-10000  也就是分片数量从1000到10000所以我们截取字符串
                //bytes=100-   意思是100到末尾
                String numRange = request.getHeader("Range").replaceAll("bytes=", "");
                String[] strRange = numRange.split("-");//拆分数组
                if(strRange.length == 2){
                    pos = Long.parseLong(strRange[0].trim());//设置起始位置
                    last = Long.parseLong(strRange[1].trim());//设置结束位置
                    if(fSize < last){//最后一片的时候很可能超出文件大小,因此需要判断结束位置是否超出文件大小
                        last = fSize-1;
                    }
                }else {//如果分片是从起始位置到结束,也就是我们从Range 拿到的信息是 bytes=100-这样的格式
                    pos = Long.parseLong(numRange.replaceAll("-","").trim());//设置起始位置
                    last = fSize-1;
                }
            }
            long rangeLength = last - pos +1;//获取文件需要读取的文件大小,也就是字节数
            String contentRange = new StringBuffer("bytes ").append(pos).append("-").append(last).append("/").append(fSize).toString();
            response.setHeader("Content-Range",contentRange);
            response.setHeader("Content-Length", String.valueOf(rangeLength));

            os = new BufferedOutputStream(response.getOutputStream());
            is = new BufferedInputStream(new FileInputStream(file));
            is.skip(pos);//跳转到我们需要的起始位置流
            byte[] buffer = new byte[1024];
            int length = 0;
            while (sum < rangeLength){
                length = is.read(buffer,0, (rangeLength - sum)<= buffer.length ? ((int) ( rangeLength - sum)) : buffer.length);
                sum += length;
                os.write(buffer,0,length);
            }
            System.out.println("下载完成");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭输入输出流
            if(is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.端点下载的实现

  • 当我们请求某一个分片临时文件下载的时候需要去判断该文件文件名是否存在,还需要判断该分片临时文件大小是否和我们分片大小一致(主要预防断网时文件流写入不完整),当然最后一个分片文件大概率和我们的分片大小不一致,所以我们就不对最后一个分片文件进行断点下载也就是不管最后一个分片文件存不存在都对其进行文件流的写入

5.代码的执行过程
直接进行downLoadFile接口的访问即可,需要注意,下载的文件路径是我们自定义模仿的,所以使用时需要更改下载文件路径

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值