为什么要使用多线程下载,断点续存,限制最大正在下载数
多线程下载已经提高了下载的效率,但是当一些特殊情况发生的时候,我们需要对程序进行处理,这样效率会更高。比如,断电断网等造成下载中断,那么我们下一次又要重新开始下载,这样效率底下,所以我们可以考虑使用断点下载。其原理主要是把每次每个线程的下载状况(已经下载的位置)保存到文件,下次读取出来,从上一次下载的位置继续下载,这样就大大提高了下载的效率。当我们最大正在下载数超过一定数量时,我们的下载网站会认为我们在攻击网站,会把我们的请求屏蔽掉。限制线程下载数量就可以解决此问题
实现说明
多线程下载
URL url = new URL("http://mirror.bit.edu.cn/apache/tomcat/tomcat-9/v9.0.22/bin/apache-tomcat-9.0.22.zip");
从url获取下载资源的连接 这里是从apache官网下载tomcat9.0
URLConnection connection = url.openConnection();//获取连接
long length = connection.getContentLengthLong();//得到下载文件总长度
int blockSize = 1024 * 1024 * 2;//分2M一个线程
int forCount = (int) (length % blockSize == 0 ? length / blockSize : length / blockSize + 1);//总共下载线程
分好下载part(部分)后,开启多线程下载
for (int i = 0; i < forCount; i++) {
int begin = i * blockSize;//第i part的开始位置
int end = begin + blockSize;//第i part的结束位置
URLConnection conn = url.openConnection();
int index = i;
//开启多线程下载
Thread t = new Thread() {
public void run() {
try {
downlown(conn, downloadDisk, begin, end, index);
} catch (Exception e) {
e.printStackTrace();
}
};
};
t.start();
}
断点续存
需要用到RandomAccessFile类,如果用FileOutputStream,文件会把已经下载的直接覆盖掉, RandomAccessFile可以通过seek方法将指针位置移动
//当此时程序突然停止,将已经下载的值存储下来
map.put("part"+i, downSize);
//“part”+i为第i部分的键名,downSize为i部分线程已经下载到的字节位置,把此信息存入map中我们就可以获取突然程序中断前的下载位置记录,断点续存时从此位置开始读
//将map的内容存入txt文件
MapFileConvert.mapToFile(saveMapPath, map);
//saveMapPath为存map的文件路径,map为记录中断前的下载记录
Map<String,Integer> fileToMap = MapFileConvert.fileToMap(saveMapPath);
//判断当前part是否下载 若已经下载则取出已经下载的记录值
指针从0开始 配合RandomAccessFile使用 begin为每一部分开始的字节数位置
RandomAccessFile saveFile = new RandomAccessFile( downloadDisk + "_" + i, "rw");
int pointer = 0;//如果没有已经下载记录的位置,从起始位置开始 表示需要跳过的字节数
if(fileToMap != null && fileToMap.get("part" + i) != null){
//pointer(已经下载过的部分字节位置需要跳过)
pointer = fileToMap.get("part" + i) - begin;
//设置要跳过已经下载的进度
begin = fileToMap.get("part" + i);//开始下载记录的字节数位置
}
//断点续存 seek inputStream跳过前面的begin部分 saveFile将已经下载的部分跳过 指针从pointer(已经下载过的部分)开始写
inputStream.skip(begin);
saveFile.seek(pointer);
下载
//每一part开始下载
while ((len = inputStream.read(buffer)) != -1 && downSize < end) {
if (downSize + len > end) {
len = 1024 - (downSize + len - end);
}
downSize += len;
//写入文件
saveFile.write(buffer, 0, len);//此位置不能使用fos.write(buffer, 0, len);会覆盖之前下载的内容
}
断点续存效果:我实现的是当正在下载的时候终止程序,再次启动程序的时候从上一次正在下载位置下载
限制最大正在下载数
static int downloadingCount = 0;// 定义一个正在下载的静态变量
刚开始下载
//下面代码都是在下载的方法中
downloadingCount++;//每次进入下载 正在下载数加1 做最大正在下载数统计
//当正在下载的数量大于最大正在下载数时停止下载 在此等待其他线程下载完成
while (downloadingCount > downloadingmaxNum) {
synchronized (Download.class) {
Download.class.wait();
}
}
下载完成后
//当一个线程完成下载后 正在下载的数量减1
synchronized (Download.class) {
downloadingCount--;
//通知所有等待线程 已经完成数 和正在下载数的发生变化
Download.class.notifyAll();
}
完整实现代码
先创建一个download.properties的配置文件,存相关配置信息
download.properties内容如下:
downloadingmaxNum=5
downloadPathPrefix=E:/downloadtest/
saveMapPath=E:/downloadtest/map.txt
默认存储位置需要换的可以改成你的目录,这里最大下载数5
准备一个map存入文件和取出的工具类 MapFileConvert
MapFileConvert.java内容如下:
package com.jt.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Map;
public class MapFileConvert {
public static void mapToFile(String filePath,Map map) throws Exception{
File file = fileIsExist(filePath);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(map);
oos.flush();
oos.close();
}
public static Map fileToMap(String filePath){
Map map = null;
try {
File file = fileIsExist(filePath);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
map = (Map) ois.readObject();
ois.close();
} catch (Exception e) {
//e.printStackTrace();
}
return map;
}
private static File fileIsExist(String filePath) {
File file = new File(filePath);
if (!file.exists()) {
System.out.println("文件不存在,现在创建");
try {
file.createNewFile();
} catch (IOException e) {
System.out.println("路径:" + filePath + "创建失败");
e.printStackTrace();
}
}
return file;
}
}
下面是实现多线程下载的主类Download
Download.java内容如下:
package com.jt.url;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import com.jt.utils.MapFileConvert;
public class Download {
static int downloadedCount = 0;
static int downloadedSize = 0;
static int downloadedRate = 0;
static int downloadingCount = 0;// 正在下载的个数
static ResourceBundle bundle;//读取配置文件的类
static String downloadPathPrefix;//下载的文件需要存入的路径前缀 默认E:/downloadtest/
static int downloadingmaxNum;//默认最大正在下载数
static String saveMapPath;//存储已经下载的文件的位置路径
static Map<String, Integer> map;//存取已经下载文件位置的map
static {
//读取src下的download.properties文件
bundle = ResourceBundle.getBundle("download");
}
public static void main(String[] args) throws Exception {
downloadPathPrefix = bundle.getString("downloadPathPrefix");
downloadingmaxNum = Integer.parseInt(bundle.getString("downloadingmaxNum"));
saveMapPath = bundle.getString("saveMapPath");
map = new HashMap<>();
//从url获取下载资源的连接
URL url = new URL("http://mirror.bit.edu.cn/apache/tomcat/tomcat-9/v9.0.22/bin/apache-tomcat-9.0.22.zip");
String[] split = url.getPath().split("/");
String fileName = split[split.length - 1];
String downloadDisk = downloadPathPrefix + fileName;
URLConnection connection = url.openConnection();
long length = connection.getContentLengthLong();//文件总长度
int blockSize = 1024 * 1024 * 2;//分2M一个线程
int forCount = (int) (length % blockSize == 0 ? length / blockSize : length / blockSize + 1);//总共线程
long ltime = System.currentTimeMillis();//计算下载完成的所有时间
for (int i = 0; i < forCount; i++) {
int begin = i * blockSize;//第i part的开始位置
int end = begin + blockSize;//第i part的结束位置
URLConnection conn = url.openConnection();
int index = i;
//开启多线程下载
Thread t = new Thread() {
public void run() {
try {
downlown(conn, downloadDisk, begin, end, index);
} catch (Exception e) {
e.printStackTrace();
}
};
};
t.start();
}
// 当已经下载数完成则结束等待
while (downloadedCount < forCount) {
synchronized (Download.class) {
Download.class.wait();
}
}
System.out.println("总下载共用时间:" + (System.currentTimeMillis() - ltime));
// 合并文件
FileOutputStream fos = new FileOutputStream(downloadDisk);
for (int i = 0; i < forCount; i++) {
FileInputStream fis = new FileInputStream(downloadDisk + "_" + i);
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fis.close();
}
fos.close();
}
private static void downlown(URLConnection conn, String downloadDisk, int begin, int end, int i) throws Exception {
InputStream inputStream = conn.getInputStream();
//如果用FileOutputStream,文件会把已经下载的直接覆盖掉, RandomAccessFile可以通过seek方法将指针位置移动
RandomAccessFile saveFile = new RandomAccessFile( downloadDisk + "_" + i, "rw");
//FileOutputStream fos = new FileOutputStream(downloadDisk + "_" + i);
int contentLength = conn.getContentLength();
//判断当前part是否下载 若已经下载则取出已经下载的记录值
Map<String,Integer> fileToMap = MapFileConvert.fileToMap(saveMapPath);
//指针从0开始配合RandomAccessFile使用
int pointer = 0;
if(fileToMap != null && fileToMap.get("part" + i) != null){
pointer = fileToMap.get("part" + i) - begin;
//将进度条定位到之前下载的进度
Download.downloadedSize += pointer;
//设置要跳过已经下载的进度
begin = fileToMap.get("part" + i);
}
//断点续存 seek inputStream跳过前面的begin部分 saveFile将已经下载的部分跳过 指针从pointer(已经下载过的部分)开始写
inputStream.skip(begin);
saveFile.seek(pointer);
int downSize = begin;
byte[] buffer = new byte[1024];
int len;
long time = System.currentTimeMillis();
System.out.println("第" + i + "块开始");
//每次进入下载 正在下载数加1 做最大正在下载数统计
Download.downloadingCount++;
//当正在下载的数量大于最大正在下载数时停止下载
while (Download.downloadingCount > Download.downloadingmaxNum) {
synchronized (Download.class) {
Download.class.wait();
}
}
//每一part开始下载
while ((len = inputStream.read(buffer)) != -1 && downSize < end) {
if (downSize + len > end) {
len = 1024 - (downSize + len - end);
}
downSize += len;
//计算进度 将进度显示在控制台 当前进度>=之前进度%1时 进度变化
synchronized (Download.class) {
Download.downloadedSize += len;
int rate = Download.downloadedSize * 100 / contentLength;
if (rate > Download.downloadedRate) {
System.out.println("当前文件下载进度:" + rate + "% 下载数:" + downloadingCount + " 下载位置" + i + ":" + downSize);
Download.downloadedRate = rate;
}
}
//写入文件
//fos.write(buffer, 0, len);
saveFile.write(buffer, 0, len);
//当此时程序突然停止,将已经下载的值存储下来
map.put("part"+i, downSize);
//将map的内容存入txt文件
MapFileConvert.mapToFile(saveMapPath, map);
}
System.out.println("第" + i + "块所需时间:" + (System.currentTimeMillis() - time));
//当一个线程完成下载后 已经完成的数量加1 正在下载的数量减1
synchronized (Download.class) {
Download.downloadedCount++;
Download.downloadingCount--;
//通知所有等待线程 已经完成数 和正在下载数的变化
Download.class.notifyAll();
}
saveFile.close();
//fos.close();
}
}
多线程的简单实现完成了