使用java实现http多线程下载

使用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
 9 File 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就是从 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);
        }

    }

 

 

上面的代码就是根据 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);
            }

 

 

上面就是断点恢复的做法了,和新创建一个线程没什么不同,只是 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);
            }

        }


    }


 

 

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

 

http://www.blogjava.net/Rexcj/archive/2008/07/27/217793.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值