使用java实现http多线程下载


关键字: java http 下载 多线程

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

我实现的这个http下载工具功能很简单,就是一个多线程以及一个断点恢复,当然下载是必不可少的。那么大概先整理一下要做的事情:
1、 连接资源服务器,获取资源信息,创建文件
2、 切分资源,多线程下载
3、 断点恢复功能
4、 下载速率统计

大概就这几点吧,那么首先要做的就是连接资源并获取资源信息,我这里使用了JavaSE自带的URLConnection进行资源连接,大致代码如下:
Java代码
  1. String urlStr = “http: //www.sourcelink.com/download/xxx”;   //资源地址,随便写的   
  2. URL url = new  URL(urlStr);                              //创建URL   
  3. URLConnection con = url.openConnection();               //建立连接   
  4. contentLen = con.getContentLength();                    //获得资源长度   
  5. 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这个类了,这个类提供了对文件的随机访问,可以指定向 文件中的某一个位置进行写入操作,大致代码如下:

Java代码
  1. long  subLen = contentLen / threadQut;                            //获取每块的大小   
  2.   
  3. //创建十个线程,并启动线程   
  4. for  ( int  i =  0 ; i < threadQut; i++) {  
  5.     DLThread thread = new  DLThread( this , i +  1 , subLen * i, subLen * (i +  1 ) -  1 );  //创建线程   
  6.     dlThreads[i] = thread;  
  7.     QSEngine.pool.execute(dlThreads[i]);                                //把线程交给线程池进行管理   
  8. }  
            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方法:

Java代码
  1. public   void  run() {  
  2.     System.out.println("线程"  + id +  "启动......" );  
  3.     BufferedInputStream bis = null ;                                              //创建一个buff   
  4.     RandomAccessFile fos = null ;                                                 
  5.     byte [] buf =  new   byte [BUFFER_SIZE];                                          //缓冲区大小   
  6.     URLConnection con = null ;  
  7.     try  {  
  8.         con = url.openConnection();                                             //创建连接,这里会为每个线程都创建一个连接   
  9.         con.setAllowUserInteraction(true );  
  10.         if  (isNewThread) {  
  11.             con.setRequestProperty("Range""bytes="  + startPos +  "-"  + endPos); //设置获取资源数据的范围,从startPos到endPos   
  12.             fos = new  RandomAccessFile(file,  "rw" );                              //创建RandomAccessFile   
  13.             fos.seek(startPos);                                                 //从startPos开始   
  14.         } else  {  
  15.             con.setRequestProperty("Range""bytes="  + curPos +  "-"  + endPos);  
  16.             fos = new  RandomAccessFile(dlTask.getFile(),  "rw" );  
  17.             fos.seek(curPos);  
  18.         }  
  19.         //下面一段向根据文件写入数据,curPos为当前写入的未知,这里会判断是否小于endPos,   
  20.         //如果超过endPos就代表该线程已经执行完毕   
  21.         bis = new  BufferedInputStream(con.getInputStream());                      
  22.         while  (curPos < endPos) {  
  23.             int  len = bis.read(buf,  0 , BUFFER_SIZE);                  
  24.             if  (len == - 1 ) {  
  25.                 break ;  
  26.             }  
  27.             fos.write(buf, 0 , len);  
  28.             curPos = curPos + len;  
  29.             if  (curPos > endPos) {  
  30.                 readByte += len - (curPos - endPos) + 1//获取正确读取的字节数   
  31.             } else  {  
  32.                 readByte += len;  
  33.             }  
  34.         }  
  35.         System.out.println("线程"  + id +  "已经下载完毕。" );  
  36.         this .finished =  true ;  
  37.         bis.close();  
  38.         fos.close();  
  39.     } catch  (IOException ex) {  
  40.         ex.printStackTrace();  
  41.         throw   new  RuntimeException(ex);  
  42.     }  
  43. }  
    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方法里有一段这样的代码:
Java代码
  1. if  (isNewThread) {                                               //判断是否断点,如果true,代表是一个新的下载线程,而不是断点恢复   
  2.     con.setRequestProperty("Range""bytes="  + startPos +  "-"  + endPos); //设置获取资源数据的范围,从startPos到endPos   
  3.     fos = new  RandomAccessFile(file,  "rw" );                              //创建RandomAccessFile   
  4.     fos.seek(startPos);                                                 //从startPos开始   
  5. else  {  
  6.     con.setRequestProperty("Range""bytes="  + curPos +  "-"  + endPos); //使用curPos替代startPos,其他都和新创建一个是一样的。   
  7.     fos = new  RandomAccessFile(dlTask.getFile(),  "rw" );  
  8.     fos.seek(curPos);  
  9. }  
            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这个类就是用来保存每个任务的信息的,所以我只要在需要恢复的时候,反序列化这个对象,就可以很 容易的实现了断点功能,我们来看看这个对象保存的信息:
Java代码
  1. public   class  DLTask  extends  Thread  implements  Serializable {  
  2.   
  3.     private   static   final   long  serialVersionUID = 126148287461276024L;  
  4.     private   final   static   int  MAX_DLTHREAD_QUT =  10 ;   //最大下载线程数量   
  5.     /**  
  6.      * 下载临时文件后缀,下载完成后将自动被删除  
  7.      */   
  8.     public   final   static  String FILE_POSTFIX =  ".tmp" ;  
  9.     private  URL url;                                      
  10.     private  File file;  
  11.     private  String filename;  
  12.     private   int  id;  
  13.     private   int  Level;  
  14.     private   int  threadQut;                               //下载线程数量,用户可定制                             
  15.     private   int  contentLen;                          //下载文件长度   
  16.     private   long  completedTot;                           //当前下载完成总数   
  17.     private   int  costTime;                                //下载时间计数,记录下载耗费的时间   
  18.     private  String curPercent;                           //下载百分比   
  19.     private   boolean  isNewTask;                       //是否新建下载任务,可能是断点续传任务   
  20.       
  21.     private  DLThread[] dlThreads;                        //保存当前任务的线程   
  22.   
  23. 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,这样子就可以很容易做 到断点的恢复了,让我重新写一个文件保存这些信息,然后在恢复的时候再根据这些信息创建一个对象,那简直是要我的命。这里创建了一个方法,用于断点恢复 用:
Java代码
  1. private   void  resumeTask() {  
  2.     listener = new  DLListener( this );  
  3.     file = new  File(filename);  
  4.     for  ( int  i =  0 ; i < threadQut; i++) {  
  5.         dlThreads[i].setDlTask(this );  
  6.         QSEngine.pool.execute(dlThreads[i]);  
  7.     }  
  8.     QSEngine.pool.execute(listener);  
  9. }  
    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进行序列化,保存任务信息,用于断点恢复,一个就是进行下载速率的统计,平均多长时间进行一个统计。我们先来看下它的代码,这 个类也是一个单独的线程:
Java代码
  1. public   void  run() {  
  2.   
  3.     int  i =  0 ;  
  4.     BigDecimal completeTot = null ;                                          //完成的百分比                
  5.     long  start = System.currentTimeMillis();                                //当前时间,用于记录开始统计时间   
  6.     long  end = start;  
  7.   
  8.     while  (!dlTask.isComplete()) {                                         //整个任务是否完成,没有完成则继续循环   
  9.         i++;  
  10.         String percent = dlTask.getCurPercent();                      //获取当前的完成百分数   
  11.   
  12.         completeTot = new  BigDecimal(dlTask.getCompletedTot());        //获取当前完成的总字节数   
  13.   
  14.                        //获得当前时间,然后与start时间比较,如果不一样,利用当前完成的总数除以所使用的时间,获得一个平均下载速度   
  15.         end = System.currentTimeMillis();                               
  16.         if  (end - start !=  0 ) {  
  17.             BigDecimal pos = new  BigDecimal(((end - start) /  1000 ) *  1024 );  
  18.             System.out.println("Speed :"   
  19.                     + completeTot  
  20.                             .divide(pos, 0 , BigDecimal.ROUND_HALF_EVEN)  
  21.                     + "k/s   "  + percent +  "% completed. " );  
  22.         }  
  23.         recoder.record();         //将任务信息记录到硬盘   
  24.         try  {  
  25.             sleep(3000 );  
  26.         } catch  (InterruptedException ex) {  
  27.             ex.printStackTrace();  
  28.             throw   new  RuntimeException(ex);  
  29.         }  
  30.   
  31.     }  
  32.                //以下是下载完成后打印整个下载任务的信息   
  33.     int  costTime =+ ( int )((System.currentTimeMillis() - start) /  1000 );  
  34.     dlTask.setCostTime(costTime);  
  35.     String time = QSDownUtils.changeSecToHMS(costTime);  
  36.       
  37.     dlTask.getFile().renameTo(new  File(dlTask.getFilename()));  
  38.     System.out.println("Download finished. "  + time);  
  39. }  
	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该方法的代码如下:

Java代码
  1. public   void  record() {  
  2.     ObjectOutputStream out = null ;  
  3.     try  {  
  4.         out = new  ObjectOutputStream( new  FileOutputStream(dlTask.getFilename() +  ".tsk" ));    
  5.         out.writeObject(dlTask);  
  6.         out.close();  
  7.     } catch  (IOException ex) {  
  8.         ex.printStackTrace();  
  9.         throw   new  RuntimeException(ex);  
  10.     } finally  {  
  11.         try  {  
  12.             out.close();  
  13.         } catch  (IOException ex) {  
  14.             ex.printStackTrace();  
  15.             throw   new  RuntimeException(ex);  
  16.         }  
  17.     }  
  18.   
  19. }  
    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);
            }
        }

    }



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值