java开发断点续传,多线程下载工具
下载主类DownUtil
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.net.URLConnection.*;
import java.nio.file.*;
import java.util.regex.*;
public class DownUtil
{
private int startPosit;
private String path;
private String targetFile;
private int threadNum;
private DownThread[] threads;
private int fileSize;
public int fileSize0;
public DownUtil(String path,String targetFile,int threadNum)
{
this.path = path;
this.targetFile = targetFile;
this.threadNum = threadNum;
this.threads = new DownThread[threadNum];
}
public void download()
throws Exception
{
var url = new URL(path);
var conn = (HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5*1000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept-Ranges","bytes");
conn.setRequestProperty("Content-Length","7877");
conn.setRequestProperty("Content-Type","mage/png");
fileSize = conn.getContentLength();
fileSize0 = conn.getContentLength();
conn.disconnect();
var currentPartSize = fileSize / threadNum + 1;
var file = new RandomAccessFile(targetFile,"rw");
file.setLength(fileSize);
file.close();
for (var i = 0; i < threadNum; i++)
{
Thread.sleep(500);
int startPosit = i * currentPartSize;
var currentPart = new RandomAccessFile(targetFile,"rw");
currentPart.seek(startPosit);
//被分配到的文件快,该文件快大小,开始位置
threads[i] = new DownThread(currentPart,currentPartSize,startPosit,i);
var thread0 = threads[i];
//并发池
var pool = Executors.newFixedThreadPool(threadNum);
pool.submit(thread0);
pool.shutdown();
}
}
public double getCompleteRate()
{
int sum = 0;
for (var i = 0; i < threadNum; i++)
{
sum += threads[i].length;
}
return sum * 1.0 / fileSize;
}
private class DownThread implements Runnable
{
//保存指针文件
private File file;
//网络文件输入流
private InputStream inputStream;
//指针文件对应的RandomAccessFile
private RandomAccessFile raf;
//本次下载开始位置
private int startPosit;
//下载到本地的文件对应的RandomAccessFile
private RandomAccessFile currentPart;
//当前线程被分配到的总任务量
private int currentPartSize;
//当前线程已经完成的任务量(多次断点下载合计,非仅仅本次已经下载的任务量)
private int length;
//线程名
private int threadName;
private DownThread(RandomAccessFile currentPart,int currentPartSize,int startPosit,int threadName)
{
this.currentPartSize = currentPartSize;
this.currentPart = currentPart;
this.startPosit = startPosit;
this.threadName = threadName;
}
private class saveLastPosit implements Runnable
{
//定期保存读写位置,下次要是下载同一个文件应该先检索该文件的指针位置
@Override
public void run()
{
try
{
//提取文件名.前面的部分
String[] targetFileArr = targetFile.split("\\.");
//拼接完整的保存指针文件名
String positFileName = targetFileArr[0] + "_lastposit_" + threadName + ".txt";
file = new File(positFileName);
//检查是否已经有保存指针的位置的文件,如果有就把上次的指针位置记录下来
if(file.exists())
{
System.out.println("该文件已存在,继续下载");
raf = new RandomAccessFile(file,"rw");
//把指针移动开始
raf.seek(0);
var buffer = new byte[1024];
//开始读取输入流,把读取结果缓存在buffer,读到的最后一位数据的位置为hasRead
raf.read(buffer);
//把buffer转换成字符串,并且用正则表达式提取数字
var rafStr = new String(buffer);
rafStr = rafStr.replaceAll("\\D","");
var m = Pattern.compile("\\d*").matcher(rafStr);
System.out.println(m.find());
//返回匹配到的有效数字字符串
String seekPoist = m.group();
//把取到的指针位置存到startPosit
startPosit = Integer.valueOf(seekPoist);//1
//把本地文件指针指到开始位置
currentPart.seek(startPosit);
//计算该线程已经写入的文件大小
//本次下载的起始位置 减去 该线程负责负责下载的起始位置(不限于本次)
length = startPosit - (currentPartSize * threadName);
}
//如果是第一次下载本文件,就创建临时文件用来保存指针
else
{
System.out.println();
raf = new RandomAccessFile(positFileName,"rw");
raf.setLength(10000);
}
while(true)
{
//每一秒,记录一次指针位置,并且把该位置写入临时文件
Thread.sleep(1000);
raf.seek(0);
// 该线程负责负责下载的起始位置(不限于本次) 加上 已经写进去的长度
raf.writeBytes(String.valueOf((currentPartSize * threadName) + length));
}
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
@Override
public void run()
{
//正式下载之前先启动saveLastPosit
Thread t = new Thread(new saveLastPosit());
//设为Daemon线程
t.setDaemon(true);
t.start();
try
{
//该sleep主要是保证saveLastPosit必要操作已经完成
Thread.sleep(100);
var url = new URL(path);
var conn = (HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5*1000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept-Ranges","bytes");
conn.setRequestProperty("Content-Length","7877");
conn.setRequestProperty("Content-Type","mage/png");
//获取网络资源的二进制输入流
inputStream = conn.getInputStream();
//把指针指向网络资源起始位置(该起始位置等于本地文件起始位置)
inputStream.skip(this.startPosit);
var buffer = new byte[1024];
int hasRead = 0;
//开始读取输入流,把读取结果缓存在buffer,读到的最后一位数据的位置为hasRead
while ((hasRead = inputStream.read(buffer)) != -1
//如果已经写的长度没有超出本线程任务本分
&& length < currentPartSize)
{
//把读进buffer所有数据写到本地文件
currentPart.write(buffer,0,hasRead);
//对已经写的文件量进行累加
length += hasRead;
}
}
catch(Exception ex)
{
ex.printStackTrace();
}
finally
{
try
{
currentPart.close();
inputStream.close();
raf.close();
//删除临时文件currentPartSize可能会略大于length,但是不影响下载文件的质量
if ((currentPartSize -length) < 10)
{
file.delete();
}
}
catch(Exception ex)
{
System.out.println("资源关闭错误");
ex.printStackTrace();
}
}
}
}
}
下载主程序启动类DownUtilTest
import java.util.*;
public class DownUtilTest
{
public static void main(String[] args) throws Exception
{
var d1 = new Date().getTime()/1000;
final var du = new DownUtil("xxx(下载资源地址)xxx","xxx(指定文件名)xxx",xx(指定线程数)xx);
du.download();
new Thread(() ->
{
while(du.getCompleteRate() < 0.999)
{
System.out.printf("%s%4.2f%%","已完成 ",du.getCompleteRate()*100);
System.out.println("");
try
{
Thread.sleep(100);
}
catch(Exception ex){}
}
var d2 = new Date().getTime()/1000;
System.out.printf("%s%d%s","下载完成!,耗时",(d2 - d1),"秒。");
}).start();
}
}
大致是这样,很多细节处理还有欠缺
实测,下载国外资源时比一般浏览器还是要快不少。