关键字: java http 下载 多线程
下载工具我想没有几个人不会用的吧,前段时间比较无聊,花了点时间用java写了个简单的http多线程下载程序,纯粹是无聊才写的,只实现了几个简单的 功能,而且也没写界面,今天正好也是一个无聊日,就拿来写篇文章,班门弄斧一下,觉得好给个掌声,不好也不要喷,谢谢!
我实现的这个http下载工具功能很简单,就是一个多线程以及一个断点恢复,当然下载是必不可少的。那么大概先整理一下要做的事情:
1、 连接资源服务器,获取资源信息,创建文件
2、 切分资源,多线程下载
3、 断点恢复功能
4、 下载速率统计
大概就这几点吧,那么首先要做的就是连接资源并获取资源信息,我这里使用了JavaSE自带的URLConnection进行资源连接,大致代码如下:
很简单吧,没错就是这么简单,第一步做完了,那么接下来要做第二步,切分资源,实现多线程。在上一步我们已经获得了资源的长度 contentLen,那么如何根据这个对资源进行切分呢?假如我们要运行十个线程,那么我们就先把contentLen处以10,获得每块的大小,然后 在分别创建十个线程,每个线程负责其中一块的写入,这就需要利用到RandomAccessFile这个类了,这个类提供了对文件的随机访问,可以指定向 文件中的某一个位置进行写入操作,大致代码如下:
在这里使用到了DLThread这个类,我们先来看看这个类的构造方法的定义:
public DLThread(DLTask dlTask, int id, long startPos, long endPos)
第一个参数为一个DLTask,这个类就代表一个下载任务,里面主要保存这一个下载任务的信息,包括下载资源名,本地文件名等等的信息。第二个参 数就是一个标示线程的id,如果有10个线程,那么这个id就是从1到10,第三个参数startPos代表该线程从文件的哪个地方开始写入,最后一个参 数endPos代表写到哪里就结束。
我们再来看看,一个线程启动后,具体如何去下载,请看run方法:
上面的代码就是根据startPos和endPos对文件机型写操作,每个线程都有自己独立的一个资源块,从startPos到endPos。上 面的方式就是线程下载的核心,多线程搞定后,接下来就是实现断点恢复的功能,其实断点恢复无非就是记录下每个线程完成到哪个未知,在这里我就是使用 curPos进行的记录,大家在上面的代码就应该可以看到,我会记录下每个线程的curPos,然后在线程重新启动的时候,就把curPos当成是 startPos,而endPost则不变即可,大家有没注意到run方法里有一段这样的代码:
上面就是断点恢复的做法了,和新创建一个线程没什么不同,只是startPos不一样罢了,其他都一样,不过仅仅有这个还不够,因为如果程序关闭 的话,这些信息又是如何保存呢?例如文件名啊,每个线程的curPos啊等等,大家在使用下载软件的时候,相信都会发现在软件没下载完的时候,在目录下会 有两个临时文件,而其中一个就是用来保存下载任务的信息的,如果没有这些信息,程序是不知道该如何恢复下载进度的。而我这里又如何实现的呢?我这个人比较 懒,又不想再创建一个文件来保存信息,然后自己又要读取信息创建对象,那太麻烦了,所以我想到了java提供序列化机制,我的想法就是直接把整个 DLTask的对象序列化到硬盘上,上面说过DLTask这个类就是用来保存每个任务的信息的,所以我只要在需要恢复的时候,反序列化这个对象,就可以很 容易的实现了断点功能,我们来看看这个对象保存的信息:
如上代码,这个对象实现了Serializable接口,保存了任务的所有信息,还包括有每个线程对象dlThreads,这样子就可以很容易做 到断点的恢复了,让我重新写一个文件保存这些信息,然后在恢复的时候再根据这些信息创建一个对象,那简直是要我的命。这里创建了一个方法,用于断点恢复 用:
实际上就是减少了先连接资源,然后进行切分资源的代码,因为这些信息已经都被保存在DLTask的对象下了。
看到上面的代码,不知道大家注意到有一个对象DLListener没有,这个对象实际上就是用于监听整个任务的信息的,这里我主要用于两个目的, 一个是定时的对DLTask进行序列化,保存任务信息,用于断点恢复,一个就是进行下载速率的统计,平均多长时间进行一个统计。我们先来看下它的代码,这 个类也是一个单独的线程:
这个方法中的recoder.record()方法的调用就是用于序列化任务对象,其他的代码均为统计信息用的,具体可看注释,record该方法的代码如下:
到这里,大致的代码都完成了,不过以上的代码都是部分片段,只是作为一个参考给大家看下,而且由于本人水平有限,代码很多地方都没有经过过多的考 虑,没有经过优化,仅仅只是自娱自乐,所以可能有很多地方都写的很烂,这个程序也缺乏很多功能,连界面都没有,所以整个程序的代码就不上传了,免得丢人, 呵呵。希望对有兴趣的朋友尽到一点帮助吧。
我实现的这个http下载工具功能很简单,就是一个多线程以及一个断点恢复,当然下载是必不可少的。那么大概先整理一下要做的事情:
1、 连接资源服务器,获取资源信息,创建文件
2、 切分资源,多线程下载
3、 断点恢复功能
4、 下载速率统计
大概就这几点吧,那么首先要做的就是连接资源并获取资源信息,我这里使用了JavaSE自带的URLConnection进行资源连接,大致代码如下:
- String urlStr = “http: //www.sourcelink.com/download/xxx”; //资源地址,随便写的
- URL url = new URL(urlStr); //创建URL
- URLConnection con = url.openConnection(); //建立连接
- contentLen = con.getContentLength(); //获得资源长度
- File file = new File(filename); //根据filename创建一个下载文件,也会是我们最终下载所得的文件
String urlStr = “http://www.sourcelink.com/download/xxx”; //资源地址,随便写的
URL url = new URL(urlStr); //创建URL
URLConnection con = url.openConnection(); //建立连接
contentLen = con.getContentLength(); //获得资源长度
File file = new File(filename); //根据filename创建一个下载文件,也会是我们最终下载所得的文件
很简单吧,没错就是这么简单,第一步做完了,那么接下来要做第二步,切分资源,实现多线程。在上一步我们已经获得了资源的长度 contentLen,那么如何根据这个对资源进行切分呢?假如我们要运行十个线程,那么我们就先把contentLen处以10,获得每块的大小,然后 在分别创建十个线程,每个线程负责其中一块的写入,这就需要利用到RandomAccessFile这个类了,这个类提供了对文件的随机访问,可以指定向 文件中的某一个位置进行写入操作,大致代码如下:
- long subLen = contentLen / threadQut; //获取每块的大小
- //创建十个线程,并启动线程
- for ( int i = 0 ; i < threadQut; i++) {
- DLThread thread = new DLThread( this , i + 1 , subLen * i, subLen * (i + 1 ) - 1 ); //创建线程
- dlThreads[i] = thread;
- QSEngine.pool.execute(dlThreads[i]); //把线程交给线程池进行管理
- }
long subLen = contentLen / threadQut; //获取每块的大小
//创建十个线程,并启动线程
for (int i = 0; i < threadQut; i++) {
DLThread thread = new DLThread(this, i + 1, subLen * i, subLen * (i + 1) - 1); //创建线程
dlThreads[i] = thread;
QSEngine.pool.execute(dlThreads[i]); //把线程交给线程池进行管理
}
在这里使用到了DLThread这个类,我们先来看看这个类的构造方法的定义:
public DLThread(DLTask dlTask, int id, long startPos, long endPos)
第一个参数为一个DLTask,这个类就代表一个下载任务,里面主要保存这一个下载任务的信息,包括下载资源名,本地文件名等等的信息。第二个参 数就是一个标示线程的id,如果有10个线程,那么这个id就是从1到10,第三个参数startPos代表该线程从文件的哪个地方开始写入,最后一个参 数endPos代表写到哪里就结束。
我们再来看看,一个线程启动后,具体如何去下载,请看run方法:
- public void run() {
- System.out.println("线程" + id + "启动......" );
- BufferedInputStream bis = null ; //创建一个buff
- RandomAccessFile fos = null ;
- byte [] buf = new byte [BUFFER_SIZE]; //缓冲区大小
- URLConnection con = null ;
- try {
- con = url.openConnection(); //创建连接,这里会为每个线程都创建一个连接
- con.setAllowUserInteraction(true );
- if (isNewThread) {
- con.setRequestProperty("Range" , "bytes=" + startPos + "-" + endPos); //设置获取资源数据的范围,从startPos到endPos
- fos = new RandomAccessFile(file, "rw" ); //创建RandomAccessFile
- fos.seek(startPos); //从startPos开始
- } else {
- con.setRequestProperty("Range" , "bytes=" + curPos + "-" + endPos);
- fos = new RandomAccessFile(dlTask.getFile(), "rw" );
- fos.seek(curPos);
- }
- //下面一段向根据文件写入数据,curPos为当前写入的未知,这里会判断是否小于endPos,
- //如果超过endPos就代表该线程已经执行完毕
- bis = new BufferedInputStream(con.getInputStream());
- while (curPos < endPos) {
- int len = bis.read(buf, 0 , BUFFER_SIZE);
- if (len == - 1 ) {
- break ;
- }
- fos.write(buf, 0 , len);
- curPos = curPos + len;
- if (curPos > endPos) {
- readByte += len - (curPos - endPos) + 1 ; //获取正确读取的字节数
- } else {
- readByte += len;
- }
- }
- System.out.println("线程" + id + "已经下载完毕。" );
- this .finished = true ;
- bis.close();
- fos.close();
- } catch (IOException ex) {
- ex.printStackTrace();
- throw new RuntimeException(ex);
- }
- }
public void run() {
System.out.println("线程" + id + "启动......");
BufferedInputStream bis = null; //创建一个buff
RandomAccessFile fos = null;
byte[] buf = new byte[BUFFER_SIZE]; //缓冲区大小
URLConnection con = null;
try {
con = url.openConnection(); //创建连接,这里会为每个线程都创建一个连接
con.setAllowUserInteraction(true);
if (isNewThread) {
con.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);//设置获取资源数据的范围,从startPos到endPos
fos = new RandomAccessFile(file, "rw"); //创建RandomAccessFile
fos.seek(startPos); //从startPos开始
} else {
con.setRequestProperty("Range", "bytes=" + curPos + "-" + endPos);
fos = new RandomAccessFile(dlTask.getFile(), "rw");
fos.seek(curPos);
}
//下面一段向根据文件写入数据,curPos为当前写入的未知,这里会判断是否小于endPos,
//如果超过endPos就代表该线程已经执行完毕
bis = new BufferedInputStream(con.getInputStream());
while (curPos < endPos) {
int len = bis.read(buf, 0, BUFFER_SIZE);
if (len == -1) {
break;
}
fos.write(buf, 0, len);
curPos = curPos + len;
if (curPos > endPos) {
readByte += len - (curPos - endPos) + 1; //获取正确读取的字节数
} else {
readByte += len;
}
}
System.out.println("线程" + id + "已经下载完毕。");
this.finished = true;
bis.close();
fos.close();
} catch (IOException ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
上面的代码就是根据startPos和endPos对文件机型写操作,每个线程都有自己独立的一个资源块,从startPos到endPos。上 面的方式就是线程下载的核心,多线程搞定后,接下来就是实现断点恢复的功能,其实断点恢复无非就是记录下每个线程完成到哪个未知,在这里我就是使用 curPos进行的记录,大家在上面的代码就应该可以看到,我会记录下每个线程的curPos,然后在线程重新启动的时候,就把curPos当成是 startPos,而endPost则不变即可,大家有没注意到run方法里有一段这样的代码:
- if (isNewThread) { //判断是否断点,如果true,代表是一个新的下载线程,而不是断点恢复
- con.setRequestProperty("Range" , "bytes=" + startPos + "-" + endPos); //设置获取资源数据的范围,从startPos到endPos
- fos = new RandomAccessFile(file, "rw" ); //创建RandomAccessFile
- fos.seek(startPos); //从startPos开始
- } else {
- con.setRequestProperty("Range" , "bytes=" + curPos + "-" + endPos); //使用curPos替代startPos,其他都和新创建一个是一样的。
- fos = new RandomAccessFile(dlTask.getFile(), "rw" );
- fos.seek(curPos);
- }
if (isNewThread) { //判断是否断点,如果true,代表是一个新的下载线程,而不是断点恢复
con.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);//设置获取资源数据的范围,从startPos到endPos
fos = new RandomAccessFile(file, "rw"); //创建RandomAccessFile
fos.seek(startPos); //从startPos开始
} else {
con.setRequestProperty("Range", "bytes=" + curPos + "-" + endPos);//使用curPos替代startPos,其他都和新创建一个是一样的。
fos = new RandomAccessFile(dlTask.getFile(), "rw");
fos.seek(curPos);
}
上面就是断点恢复的做法了,和新创建一个线程没什么不同,只是startPos不一样罢了,其他都一样,不过仅仅有这个还不够,因为如果程序关闭 的话,这些信息又是如何保存呢?例如文件名啊,每个线程的curPos啊等等,大家在使用下载软件的时候,相信都会发现在软件没下载完的时候,在目录下会 有两个临时文件,而其中一个就是用来保存下载任务的信息的,如果没有这些信息,程序是不知道该如何恢复下载进度的。而我这里又如何实现的呢?我这个人比较 懒,又不想再创建一个文件来保存信息,然后自己又要读取信息创建对象,那太麻烦了,所以我想到了java提供序列化机制,我的想法就是直接把整个 DLTask的对象序列化到硬盘上,上面说过DLTask这个类就是用来保存每个任务的信息的,所以我只要在需要恢复的时候,反序列化这个对象,就可以很 容易的实现了断点功能,我们来看看这个对象保存的信息:
- public class DLTask extends Thread implements Serializable {
- private static final long serialVersionUID = 126148287461276024L;
- private final static int MAX_DLTHREAD_QUT = 10 ; //最大下载线程数量
- /**
- * 下载临时文件后缀,下载完成后将自动被删除
- */
- public final static String FILE_POSTFIX = ".tmp" ;
- private URL url;
- private File file;
- private String filename;
- private int id;
- private int Level;
- private int threadQut; //下载线程数量,用户可定制
- private int contentLen; //下载文件长度
- private long completedTot; //当前下载完成总数
- private int costTime; //下载时间计数,记录下载耗费的时间
- private String curPercent; //下载百分比
- private boolean isNewTask; //是否新建下载任务,可能是断点续传任务
- private DLThread[] dlThreads; //保存当前任务的线程
- transient private DLListener listener; //当前任务的监听器,用于即时获取相关下载信息
public class DLTask extends Thread implements Serializable {
private static final long serialVersionUID = 126148287461276024L;
private final static int MAX_DLTHREAD_QUT = 10; //最大下载线程数量
/**
* 下载临时文件后缀,下载完成后将自动被删除
*/
public final static String FILE_POSTFIX = ".tmp";
private URL url;
private File file;
private String filename;
private int id;
private int Level;
private int threadQut; //下载线程数量,用户可定制
private int contentLen; //下载文件长度
private long completedTot; //当前下载完成总数
private int costTime; //下载时间计数,记录下载耗费的时间
private String curPercent; //下载百分比
private boolean isNewTask; //是否新建下载任务,可能是断点续传任务
private DLThread[] dlThreads; //保存当前任务的线程
transient private DLListener listener; //当前任务的监听器,用于即时获取相关下载信息
如上代码,这个对象实现了Serializable接口,保存了任务的所有信息,还包括有每个线程对象dlThreads,这样子就可以很容易做 到断点的恢复了,让我重新写一个文件保存这些信息,然后在恢复的时候再根据这些信息创建一个对象,那简直是要我的命。这里创建了一个方法,用于断点恢复 用:
- private void resumeTask() {
- listener = new DLListener( this );
- file = new File(filename);
- for ( int i = 0 ; i < threadQut; i++) {
- dlThreads[i].setDlTask(this );
- QSEngine.pool.execute(dlThreads[i]);
- }
- QSEngine.pool.execute(listener);
- }
private void resumeTask() {
listener = new DLListener(this);
file = new File(filename);
for (int i = 0; i < threadQut; i++) {
dlThreads[i].setDlTask(this);
QSEngine.pool.execute(dlThreads[i]);
}
QSEngine.pool.execute(listener);
}
实际上就是减少了先连接资源,然后进行切分资源的代码,因为这些信息已经都被保存在DLTask的对象下了。
看到上面的代码,不知道大家注意到有一个对象DLListener没有,这个对象实际上就是用于监听整个任务的信息的,这里我主要用于两个目的, 一个是定时的对DLTask进行序列化,保存任务信息,用于断点恢复,一个就是进行下载速率的统计,平均多长时间进行一个统计。我们先来看下它的代码,这 个类也是一个单独的线程:
- public void run() {
- int i = 0 ;
- BigDecimal completeTot = null ; //完成的百分比
- long start = System.currentTimeMillis(); //当前时间,用于记录开始统计时间
- long end = start;
- while (!dlTask.isComplete()) { //整个任务是否完成,没有完成则继续循环
- i++;
- String percent = dlTask.getCurPercent(); //获取当前的完成百分数
- completeTot = new BigDecimal(dlTask.getCompletedTot()); //获取当前完成的总字节数
- //获得当前时间,然后与start时间比较,如果不一样,利用当前完成的总数除以所使用的时间,获得一个平均下载速度
- end = System.currentTimeMillis();
- if (end - start != 0 ) {
- BigDecimal pos = new BigDecimal(((end - start) / 1000 ) * 1024 );
- System.out.println("Speed :"
- + completeTot
- .divide(pos, 0 , BigDecimal.ROUND_HALF_EVEN)
- + "k/s " + percent + "% completed. " );
- }
- recoder.record(); //将任务信息记录到硬盘
- try {
- sleep(3000 );
- } catch (InterruptedException ex) {
- ex.printStackTrace();
- throw new RuntimeException(ex);
- }
- }
- //以下是下载完成后打印整个下载任务的信息
- int costTime =+ ( int )((System.currentTimeMillis() - start) / 1000 );
- dlTask.setCostTime(costTime);
- String time = QSDownUtils.changeSecToHMS(costTime);
- dlTask.getFile().renameTo(new File(dlTask.getFilename()));
- System.out.println("Download finished. " + time);
- }
public void run() {
int i = 0;
BigDecimal completeTot = null; //完成的百分比
long start = System.currentTimeMillis(); //当前时间,用于记录开始统计时间
long end = start;
while (!dlTask.isComplete()) { //整个任务是否完成,没有完成则继续循环
i++;
String percent = dlTask.getCurPercent(); //获取当前的完成百分数
completeTot = new BigDecimal(dlTask.getCompletedTot()); //获取当前完成的总字节数
//获得当前时间,然后与start时间比较,如果不一样,利用当前完成的总数除以所使用的时间,获得一个平均下载速度
end = System.currentTimeMillis();
if (end - start != 0) {
BigDecimal pos = new BigDecimal(((end - start) / 1000) * 1024);
System.out.println("Speed :"
+ completeTot
.divide(pos, 0, BigDecimal.ROUND_HALF_EVEN)
+ "k/s " + percent + "% completed. ");
}
recoder.record(); //将任务信息记录到硬盘
try {
sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
//以下是下载完成后打印整个下载任务的信息
int costTime =+ (int)((System.currentTimeMillis() - start) / 1000);
dlTask.setCostTime(costTime);
String time = QSDownUtils.changeSecToHMS(costTime);
dlTask.getFile().renameTo(new File(dlTask.getFilename()));
System.out.println("Download finished. " + time);
}
这个方法中的recoder.record()方法的调用就是用于序列化任务对象,其他的代码均为统计信息用的,具体可看注释,record该方法的代码如下:
- public void record() {
- ObjectOutputStream out = null ;
- try {
- out = new ObjectOutputStream( new FileOutputStream(dlTask.getFilename() + ".tsk" ));
- out.writeObject(dlTask);
- out.close();
- } catch (IOException ex) {
- ex.printStackTrace();
- throw new RuntimeException(ex);
- } finally {
- try {
- out.close();
- } catch (IOException ex) {
- ex.printStackTrace();
- throw new RuntimeException(ex);
- }
- }
- }
public void record() {
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(dlTask.getFilename() + ".tsk"));
out.writeObject(dlTask);
out.close();
} catch (IOException ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
} finally {
try {
out.close();
} catch (IOException ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
}
到这里,大致的代码都完成了,不过以上的代码都是部分片段,只是作为一个参考给大家看下,而且由于本人水平有限,代码很多地方都没有经过过多的考 虑,没有经过优化,仅仅只是自娱自乐,所以可能有很多地方都写的很烂,这个程序也缺乏很多功能,连界面都没有,所以整个程序的代码就不上传了,免得丢人, 呵呵。希望对有兴趣的朋友尽到一点帮助吧。