记录一些学习分片下载的细节~
首先准备一些工具类,文件内容获取,和目标源连接的一些方法
public class FileUtils {
public static long getFileSize(String url) {
File file = new File(url);
return file.exists() && file.isFile() ? file.length() : 0;
}
}/**
* @author Zhongzy
* @description
* @since 2023-10-19 9:42
*/
public class HttpUtils {
/**
* @param url 目标源
* @param startPoint 区域块起始位置
* @param endPoint 区域快结束位置
* @return
*/
public static HttpURLConnection httpURLConnection(String url, long startPoint, long endPoint) {
//链接目标源
HttpURLConnection httpURLConnection = httpURLConnection(url);
System.out.println(Thread.currentThread().getName()+"下载区间是"+startPoint+"-"+endPoint);
if (0 != endPoint) {//这里是向服务器申请分片下载的关键,将目标资源的数据区间传过去,可以下载对应的区间
httpURLConnection.setRequestProperty("RANGE", "bytes=" + startPoint + "-" + endPoint);
} else {//如果为最后一块时不传入end就是将剩余的全部下载
httpURLConnection.setRequestProperty("RANGE", "bytes=" + startPoint + "-");
}
return httpURLConnection;
}
/**
* 下载
*
* @param url 获取下载地址
* @return
*/
public static HttpURLConnection httpURLConnection(String url) {
try {
//创建目标源链接
URL httpUrl = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
//向服务器发送标识信息,这里的标识信息可以自行百度
httpURLConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
return httpURLConnection;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 文件名获取
*
* @param url 下载路径
* @return 文件名
*/
public static String getFileName(String url) {
int index = url.lastIndexOf("/");
return url.substring(index + 1);
}
/**
* 返回下载长度
*
* @param url
* @return
*/
public static long getFileLength(String url) {
int length;
HttpURLConnection httpURLConnection;
httpURLConnection = httpURLConnection(url);
length = httpURLConnection.getContentLength();
//关闭连接
httpURLConnection.disconnect();
return length;
}
}
写一个下载需要的参数的实体类
public class DownLoadTask implements Callable<Boolean> {//用callable是为了拿一个返回值
private String url;//下载地址
private long startPoint;//初始下载地址
private long endPoint;//结束下载地址
private int index;//当前标识位置
private DownLoadInfoThread downLoadInfoThread;//处理当前下载信息
private CountDownLatch countDownLatch;//线程计数器
public DownLoadTask(String url, long startPoint, long endPoint, int index, DownLoadInfoThread downLoadInfoThread, CountDownLatch countDownLatch) {
this.url = url;
this.startPoint = startPoint;
this.endPoint = endPoint;
this.index = index;
this.downLoadInfoThread = downLoadInfoThread;
this.countDownLatch = countDownLatch;
}
}
完整代码及call实现
public class DownLoadTask implements Callable<Boolean> {
private String url;//下载地址
private long startPoint;//初始下载地址
private long endPoint;//结束下载地址
private int index;//当前标识位置
private DownLoadInfoThread downLoadInfoThread;//处理当前下载信息
private CountDownLatch countDownLatch;//线程计数器
public DownLoadTask(String url, long startPoint, long endPoint, int index, DownLoadInfoThread downLoadInfoThread, CountDownLatch countDownLatch) {
this.url = url;
this.startPoint = startPoint;
this.endPoint = endPoint;
this.index = index;
this.downLoadInfoThread = downLoadInfoThread;
this.countDownLatch = countDownLatch;
}
@Override
public Boolean call() throws Exception {
//下载地址
String fileName = HttpUtils.getFileName(url);
//拼接下载路径
fileName = fileName + ".temp" + index;
fileName = PathEnum.Down_Path.getPath() + fileName;
//创建目标源链接
HttpURLConnection httpURLConnection = HttpUtils.httpURLConnection(url, startPoint, endPoint);
//文件流操作
try ( //获取输入流
InputStream is = httpURLConnection.getInputStream();
//包装为缓冲流
BufferedInputStream bis = new BufferedInputStream(is);
//断点下载需要用到随机文件,操作权限read-write
RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
) {
//文件流操作
int length = -1;
byte[] buffer = new byte[1024 * 1000];
while ((length = bis.read(buffer)) != -1) {
downLoadInfoThread.downSize.add(length);
raf.write(buffer, 0, length);
}
System.out.println("thread" + Thread.currentThread().getName() + "正在运行");
} catch (FileNotFoundException e) {
System.err.println("-error-文件没有找到");
return false;
} catch (IOException e) {
System.err.println("-error-下载错误");
return false;
} finally {
httpURLConnection.disconnect();
countDownLatch.countDown();
}
return true;
}
}
单独开一个线程记录下载信息,模拟下载中的速度,进度
public class DownLoadInfoThread implements Runnable {
private static final double MB = 1024d * 1024d;
private long totalSize;//总大小
public LongAdder finishedSize=new LongAdder();//本地已经下载大小
public double preSize;//前一次下载大小
public volatile LongAdder downSize=new LongAdder();//本次累计下载大小
public DownLoadInfoThread(long totalSize) {
this.totalSize = totalSize;
}
@Override
public void run() {
//文件总大小(MB)
String totalSize = String.format("%.2f", this.totalSize / MB);
//当前秒下载大小(kb)
int speed = (int) ((downSize.doubleValue() - preSize) / 1024d);
//赋值当前size给之前得记录
preSize = downSize.doubleValue();
//计算当前剩余时间->转换为kb
double remainSize = this.totalSize - finishedSize.doubleValue() - downSize.doubleValue();
//计算时间
String remainTime = String.format("%.1f", remainSize / (1024d * speed));
if ("infinity".equalsIgnoreCase(remainTime)) {//如果因为网速过慢导致文件下载时间为无限大,返回-
remainTime = "-";
}
//已下载
String currentSize = String.format("%.2f", (downSize.doubleValue() - finishedSize.doubleValue()) / MB);
String info = String.format("已经下载%sMB/%sMB,速度%s/kb,剩余时间%ss", currentSize, totalSize, speed, remainTime);
System.out.print("\r");
System.out.print(info+"------------");
}
}
分片下载顾名思义需要先把文件拆解,拆解下载下来的文件是不能用的,所以需要合并分解出来的几个文件,然后将这几个临时文件合并然后删除掉,下面是下载器具体实现和拆分合并,删除临时文件的操作。
public class DownLoader {
//日志线程
private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
//分片下载线程
private ThreadPoolExecutor executorTask = new ThreadPoolExecutor(Thread_Num, Thread_Num, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(Thread_Num));
//计数器
private CountDownLatch countDownLatch = new CountDownLatch(Thread_Num);
/**
* 下载器
*
* @param url 下载地址
*/
public void downLoad(String url) {
HttpURLConnection httpURLConnection = null;
try {
//文件名
String fileName = HttpUtils.getFileName(url);
//路径名
fileName = Down_Path.getPath() + fileName;
//创建目标源链接
httpURLConnection = HttpUtils.httpURLConnection(url);
//查看本地是否有该文件
long fileSize = FileUtils.getFileSize(fileName);
//文件大小
int contentLength = httpURLConnection.getContentLength();
//判断文件是否下载
if (fileSize >= contentLength) {
System.out.println("文件已经下载过了,请到该路径下查看" + fileName);
return;
}
//创建文件info
DownLoadInfoThread downLoadInfoThread = new DownLoadInfoThread(contentLength);
//打印下载信息
executorService.scheduleAtFixedRate(downLoadInfoThread, 1, 1, TimeUnit.SECONDS);
//分片
ArrayList<Future> list = new ArrayList<>();
splitFile(url, list, downLoadInfoThread,countDownLatch);
countDownLatch.await();
//合并文件
if (merge(fileName)) {
deleteFile(fileName);
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//关闭链接
httpURLConnection.disconnect();
executorTask.shutdown();
//关闭线程
executorService.shutdownNow();
System.out.print("\b");
System.out.print("-info-下载完成");
}
}
/**
* 分片下载器
*
* @param url 目标下载地址
* @param futureList 任务
* @param downLoadInfoThread
* @param countDownLatch
*/
public void splitFile(String url, ArrayList<Future> futureList, DownLoadInfoThread downLoadInfoThread, CountDownLatch countDownLatch) {
//文件大小
long fileLength = HttpUtils.getFileLength(url);
//分片每个的大小
long size = fileLength / Thread_Num;
//计算开始和结束位置
for (int i = 0; i < Thread_Num; i++) {
long startPos = size * i;
long endPos;
if (i == Thread_Num - 1) {//当最后一个分片时,结束位置为文件大小
endPos = 0;
} else {
endPos = startPos + size;
}
if (startPos != 0) {
startPos++;
}
//创建任务
DownLoadTask downLoadTask = new DownLoadTask(url, startPos, endPos, i, downLoadInfoThread,countDownLatch);
Future<Boolean> submit = executorTask.submit(downLoadTask);
futureList.add(submit);
}
}
public boolean merge(String fileName) throws IOException {
System.out.println(Thread.currentThread().getName() + "合并文件" + fileName);
//创建输出流
int length = -1;
byte[] buffer = new byte[1024 * 1000];
try (
RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
) {
for (int i = 0; i < Thread_Num; i++) {
//循环获取分片文件
BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(Paths.get(fileName + ".temp" + i)));
while ((length = bis.read(buffer)) != -1) {
accessFile.write(buffer, 0, length);
}
}
System.out.println("-success-合并成功" + fileName);
} catch (Exception e) {
System.out.println("合并失败");
return false;
}
return true;
}
public void deleteFile(String fileName) {
for (int i = 0; i < Thread_Num; i++) {
File file = new File(fileName + ".temp" + i);
file.deleteOnExit();
System.out.println("删除临时文件" + fileName + "成功");
}
}
}
测试及最后效果
public class test {
public static void main(String[] args) {
DownLoader loader = new DownLoader();
loader.downLoad("https://dldir1.qq.com/qqfile/qq/PCQQ9.7.17/QQ9.7.17.29230.exe");
}
}