java断点下载文件(整合多线程)

发布时间 2023-10-03 10:38:43作者: 斌哥的小弟

技术介绍:

断点下载指的是在文件下载过程中,如果下载中断或失败,比如下载到一半的时候停电了、断网了、不小心退出下载界面了等等,下一次进入下载页面可以从中断或失败的位置继续下载,而无需重新开始下载整个文件。

 

(注意:本文通过本地文件的拷贝来模拟文件传输的断点过程)

 

核心想法:通过在redis中保存一个实时变量,来记录此时此刻文件下载到了哪个位置,手动制造错误后,重新启动下载程序,程序获取位置变量,再通过随机访问文件的特性从这个位置开始重新下载。

 

核心代码:

  主测试类:

@Resource
public RedisUtil redisUtil;

private static final String FILE_PATH = "被拷贝文件路径";
private static final String SAVE_PATH = "拷贝文件路径";
private static final int NUM_THREADS = 3;
private static int stopIndex;

@RequestMapping(value = "/")
public void EndPointTest() {
    try {
        //已经在redis中手动执行redisUtil.set("stopIndex",-1);当stopIndex为-1的时候代表没有发生过断点
        File file = new File(FILE_PATH);
        long fileSize = file.length();
        long chunkSize = fileSize / NUM_THREADS;

        stopIndex = Integer.parseInt(redisUtil.get("stopIndex"));
        if (stopIndex > 0) {
            System.out.println("执行断点下载"); //最好使用log.info
            isEndPoint = true;
            for (int i = 0; i < NUM_THREADS; i++) { 
                long startByte = (i * chunkSize);
                long endByte = (i == NUM_THREADS - 1) ? fileSize - 1 : (i + 1) * chunkSize - 1;
                Thread mergerThread = new MergerThread2(FILE_PATH, SAVE_PATH, startByte, endByte, this.redisUtil);
                mergerThread.start();
            }
        } else {
            System.out.println("执行正常下载");
            isEndPoint = false;
            for (int i = 0; i < NUM_THREADS; i++) {
                long startByte = i * chunkSize;
                long endByte = (i == NUM_THREADS - 1) ? fileSize - 1 : (i + 1) * chunkSize - 1;
                Thread mergerThread = new MergerThread2(FILE_PATH, SAVE_PATH, startByte, endByte, this.redisUtil);
                mergerThread.start();
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 

线程业务:

public class MergerThread2 extends Thread {
private String filePath; private String savePath; private long startByte; private long endByte; private RedisUtil redisUtil; RandomAccessFile inputFile; RandomAccessFile outputFile; public MergerThread2() { } public MergerThread2(String filePath, String savePath, long startByte, long endByte, RedisUtil redisUtil) { this.filePath = filePath; this.savePath = savePath; this.startByte = startByte; this.endByte = endByte; this.redisUtil = redisUtil; } public void run() { try { byte[] buffer = new byte[1]; // 不要设为1024,这样read方法如果只读了2个数据,那就会有1022个0填充数组,调用write(buffer)后, // 文件中会出现大量空白,设置为1是想读一个字节后立马写一个字节 long currentIndex = 0; //与buffer一样,千万不要写在MergerThread2实现类里面,而要写在run方法里面,不然三个线程在执行后会把 //buffer和currentIndex变一样,另外currentIndex用于记录已经在文件的什么地方了。 inputFile = new RandomAccessFile(filePath, "r"); outputFile = new RandomAccessFile(savePath, "rw"); int bytesRead = 0; //表示调用read方法后读到了多少字节,这个例子中这个值都是1,但是读到末尾时会变成-1。 long bytesToRead = endByte - startByte + 1;//表示一共有多少字节需要读 if (DemoController.isEndPoint) { currentIndex = Integer.parseInt(redisUtil.get("stopIndex")); inputFile.seek(startByte + currentIndex); //在重启程序后,startByte依然是三个线程三等分文件后分别的起始位置, // 要手动加上上一次发生断点时的位置,从那里开始读,并从被写入文件的同样位置开始写 outputFile.seek(startByte + currentIndex); while (bytesToRead > 0 && bytesRead!=-1) { //必须要加-1判断,不然读最后一部分数据的线程会死循环。 bytesRead = inputFile.read(buffer, 0, 1); outputFile.write(buffer, 0, 1); currentIndex++; redisUtil.set("stopIndex", String.valueOf(currentIndex)); bytesToRead -= bytesRead; //Thread.sleep(1000); } inputFile.close(); outputFile.close(); redisUtil.set("stopIndex", "-1"); //重置 } else { inputFile.seek(startByte); //正常下载时,三个线程的startByte是不一样的 outputFile.seek(startByte); while (bytesToRead > 0) { bytesRead = inputFile.read(buffer, 0, 1); outputFile.write(buffer, 0, 1); currentIndex++; redisUtil.set("stopIndex", String.valueOf(currentIndex)); bytesToRead -= bytesRead; Thread.sleep(1000); //十秒读一个字节,因为文件很小,不写这句的话文件会瞬间完成拷贝,来不及手动关闭程序设置断点,一定要这样读写文件,注意不要 for(int i=0;i<?;i++){ outputFile.write(buffer[i]); } } } inputFile.close(); outputFile.close(); } catch (Exception e) { e.printStackTrace(); } finally { System.exit(0); } } }

 

当然了,这只是一种最简单的案例,难免错误重重,仅供批评!