Rexcj

做牛B的事,让傻B们说去吧。

使用java实现http多线程下载

 

       下载工具我想没有几个人不会用的吧,前段时间比较无聊,花了点时间用java写了个简单的http多线程下载程序,纯粹是无聊才写的,只实现了几个简单的功能,而且也没写界面,今天正好也是一个无聊日,就拿来写篇文章,班门弄斧一下,觉得好给个掌声,不好也不要喷,谢谢!

我实现的这个http下载工具功能很简单,就是一个多线程以及一个断点恢复,当然下载是必不可少的。那么大概先整理一下要做的事情:

1、 连接资源服务器,获取资源信息,创建文件

2、 切分资源,多线程下载

3、 断点恢复功能

4、 下载速率统计

大概就这几点吧,那么首先要做的就是连接资源并获取资源信息,我这里使用了JavaSE自带的URLConnection进行资源连接,大致代码如下:

 

 1                     String urlStr = “http://www.sourcelink.com/download/xxx”;   //资源地址,随便写的
 2
 3            URL url = new URL(urlStr);                             //创建URL
 4
 5            URLConnection con = url.openConnection();               //建立连接
 6
 7            contentLen = con.getContentLength();                    //获得资源长度
 8
 9File file = new File(filename);                                            //根据filename创建一个下载文件,也会是我们最终下载所得的文件
10

 

很简单吧,没错就是这么简单,第一步做完了,那么接下来要做第二步,切分资源,实现多线程。在上一步我们已经获得了资源的长度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]);                                
//把线程交给线程池进行管理
            }


 

在这里使用到了DLThread这个类,我们先来看看这个类的构造方法的定义:

public DLThread(DLTask dlTask, int id, long startPos, long endPos)

第一个参数为一个DLTask,这个类就代表一个下载任务,里面主要保存这一个下载任务的信息,包括下载资源名,本地文件名等等的信息。第二个参数就是一个标示线程的id,如果有10个线程,那么这个id就是从110,第三个参数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);
        }

    }


 

上面的代码就是根据startPosendPos对文件机型写操作,每个线程都有自己独立的一个资源块,从startPosendPos。上面的方式就是线程下载的核心,多线程搞定后,接下来就是实现断点恢复的功能,其实断点恢复无非就是记录下每个线程完成到哪个未知,在这里我就是使用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);
            }


 

上面就是断点恢复的做法了,和新创建一个线程没什么不同,只是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;            //当前任务的监听器,用于即时获取相关下载信息

 

如上代码,这个对象实现了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);
    }



 

实际上就是减少了先连接资源,然后进行切分资源的代码,因为这些信息已经都被保存在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);
    }


 

这个方法中的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);
            }

        }


    }



 

到这里,大致的代码都完成了,不过以上的代码都是部分片段,只是作为一个参考给大家看下,而且由于本人水平有限,代码很多地方都没有经过过多的考虑,没有经过优化,仅仅只是自娱自乐,所以可能有很多地方都写的很烂,这个程序也缺乏很多功能,连界面都没有,所以整个程序的代码就不上传了,免得丢人,呵呵。希望对有兴趣的朋友尽到一点帮助吧。



posted on 2008-07-27 12:21 Rexcj 阅读(21900) 评论(17)  编辑  收藏

Feedback

# re: 使用java实现http多线程下载 2008-07-27 22:46 xzqttt

看了您的文章,收到了很大的启发,谢谢分享,好文!
  回复  更多评论   

# re: 使用java实现http多线程下载 2008-07-28 09:30 pancras

非常棒。  回复  更多评论   

# re: 使用java实现http多线程下载 2008-07-28 23:17 SPARON[未登录]

好熟悉,JAVAEYE才看了这篇文章,不知是否是同一人。  回复  更多评论   

# re: 使用java实现http多线程下载 2008-07-29 08:34 Rexcj

@SPARON[未登录]
是同一人,呵呵,这边是新开的博客。  回复  更多评论   

# re: 使用java实现http多线程下载[未登录] 2008-07-30 16:32 32

QSEngine.pool.execute这个类是怎么写的??  回复  更多评论   

# re: 使用java实现http多线程下载[未登录] 2008-07-30 21:00 Rexcj

源码我上传到JE上了,你可以去http://calmness.javaeye.com下载,其实QSEngine.pool是使用了JDK5的线程池实现的,你可以看看JDK5多线程的相关资料,在这里你也可以不用这个,直接start线程就可以了。  回复  更多评论   

# re: 使用java实现http多线程下载[未登录] 2009-08-14 12:43

写得不错。。。

对大家很有帮助。

希望再出精品。  回复  更多评论   

# re: 使用java实现http多线程下载 2009-08-15 16:37 匿名


学习  回复  更多评论   

# re: 使用java实现http多线程下载 2009-12-05 09:15 Arnether

学习了  回复  更多评论   

# re: 使用java实现http多线程下载[未登录] 2012-03-04 20:58 匿名

不错,学习了  回复  更多评论   

# re: 使用java实现http多线程下载 2012-09-27 16:23 胡志波

@Rexcj
无法打开下载连接啊  回复  更多评论   

# re: 使用java实现http多线程下载 2013-01-30 15:01

lz,打不开你之前给的连接啊。  回复  更多评论   

# re: 使用java实现http多线程下载 2013-04-24 21:43 欧威

连接打不开 你能把源代码发给我吗?想看看  回复  更多评论   

# re: 使用java实现http多线程下载 2013-04-24 21:44 欧威

链接打不开 ,你能把源代码发给我吗?想看看  回复  更多评论   

# re: 使用java实现http多线程下载 2013-04-24 21:45 欧威

链接打不开 ,你能把源代码发给我吗?想看看,我的邮箱496997374@qq.com  回复  更多评论   

# re: 使用java实现http多线程下载[未登录] 2013-11-05 15:28 kkk

是这个地址http://calmness.iteye.com/blog/220075  回复  更多评论   

# re: 使用java实现http多线程下载[未登录] 2014-05-16 22:41 java学习者

BigDecimal pos = new BigDecimal(((end - start) / 1000) * 1024);
这个是什么意思呢?为什么要*1024呢,(end - start) / 1000)这个是获取到秒对吧  回复  更多评论   



只有注册用户登录后才能发表评论。


网站导航:
 

My Links

Blog Stats

常用链接

留言簿(1)

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜