Java网络编程从入门到精通(24):实现HTTP断点续传下载工具(附源代码)

源代码下载: download.rar

    在前面的文章曾讨论了HTTP 消息头的三个和断点继传有关的字段。一个是请求消息的字段Range ,另两个是响应消息字段Accept-Ranges Content-Range 。其中Accept-Ranges 用来断定Web 服务器是否支持断点继传功能。在这里为了演示如何实现断点继传功能,假设Web 服务器支持这个功能;因此,我们只使用Range Content-Range 来完成一个断点继传工具的开发。
l         要实现一个什么样的断点续传工具?
这个断点续工具是一个单线程的下载工具。它通过参数传入一个文本文件。这个文件的格式如下
http://www.ishare.cc/d/ 1174254 - 2 / 106 .jpg  d: \ ok1.jpg  8192
http://www.ishare.cc/d/1174292-2/156.jpg   d:
\ ok2.jpg    12345
http://www.ishare.cc/d/
1174277 - 2 / 147 .jpg   d: \ ok3.jpg  3456
这个文本文件的每一行是一个下载项,这个下载项分为三部分:
  • 要下载的Web资源的URL
  • 要保存的本地文件名。
  • 下载的缓冲区大小(单位是字节)。
使用至少一个空格来分隔这三部分。这个下载工具逐个下载这些文件,在这些文件全部下载完后程序退出。
l         断点续传的工作原理
“断点续传”顾名思义,就是一个文件下载了一部分后,由于服务器或客户端的原因,当前的网络连接中断了。在中断网络连接后,用户还可以再次建立网络连接来继续下载这个文件还没有下完的部分。
要想实现单线程断点续传,必须在客户断保存两个数据。
1.       已经下载的字节数。
2.       下载文件的URL
一但重新建立网络连接后,就可以利用这两个数据接着未下载完的文件继续下载。在本下载工具中第一种数据就是文件已经下载的字节数,而第二个数据在上述的下载文件中保存。
在继续下载时检测已经下载的字节数,假设已经下载了3000 个字节,那么HTTP 请求消息头的Range 字段被设为如下形式:
Range: bytes = 3000 -
HTTP 响应消息头的Content-Range 字段被设为如下的形式:
Content-Range: bytes  3000 - 10000 / 10001
l         实现断点续传下载工具
一个断点续传下载程序可按如下几步实现:
1.       输入要下载文件的URL 和要保存的本地文件名,并通过Socket 类连接到这个URL
所指的服务器上。
2.       在客户端根据下载文件的URL 和这个本地文件生成HTTP 请求消息。在生成请求
消息时分为两种情况:
(1 )第一次下载这个文件,按正常情况生成请求消息,也就是说生成不包含Range
字段的请求消息。
2 )以前下载过,这次是接着下载这个文件。这就进入了断点续传程序。在这种情况生成的HTTP 请求消息中必须包含Range 字段。由于是单线程下载,因此,这个已经下载了一部分的文件的大小就是Range 的值。假设当前文件的大小是1234 个字节,那么将Range 设成如下的值:
Range:bytes = 1234 -
3.       向服务器发送HTTP 请求消息。
4.       接收服务器返回的HTTP 响应消息。
5.       处理HTTP 响应消息。在本程序中需要从响应消息中得到下载文件的总字节数。如
果是第一次下载,也就是说响应消息中不包含Content-Range 字段时,这个总字节数也就是Content-Length 字段的值。如果响应消息中不包含Content-Length 字段,则这个总字节数无法确定。这就是为什么使用下载工具下载一些文件时没有文件大小和下载进度的原因。如果响应消息中包含Content-Range 字段,总字节数就是Content-Range bytes m-n/k 中的k ,如Content-Range 的值为:
Content-Range:bytes  1000 - 5000 / 5001
则总字节数为5001 。由于本程序使用的Range 值类型是得到从某个字节开始往后的所有字节,因此,当前的响应消息中的Content-Range 总是能返回还有多少个字节未下载。如上面的例子未下载的字节数为5000-1000+1=4001
6. 开始下载文件,并计算下载进度(百分比形式)。如果网络连接断开时,文件仍未下载完,重新执行第一步。也果文件已经下载完,退出程序。
分析以上六个步骤得知,有四个主要的功能需要实现:
1. 生成HTTP 请求消息,并将其发送到服务器。这个功能由generateHttpRequest 方法来完成。
2. 分析HTTP 响应消息头。这个功能由analyzeHttpHeader 方法来完成。
3. 得到下载文件的实际大小。这个功能由getFileSize 方法来完成。
4. 下载文件。这个功能由download 方法来完成。
以上四个方法均被包含在这个断点续传工具的核心类HttpDownload.java 中。在给出HttpDownload 类的实现之前先给出一个接口DownloadEvent 接口,从这个接口的名字就可以看出,它是用来处理下载过程中的事件的。下面是这个接口的实现代码:
  package download;
  
  
public   interface  DownloadEvent
  {
      
void  percent( long  n);              //  下载进度
       void  state(String s);               //  连接过程中的状态切换
       void  viewHttpHeaders(String s);     //  枚举每一个响应消息字段
  }
从上面的代码可以看出,DownloadEvent 接口中有三个事件方法。在以后的主函数中将实现这个接口,来向控制台输出相应的信息。下面给出了HttpDownload 类的主体框架代码:
   001    package download;
  
002   
  
003    import  java.net. * ;
  
004    import  java.io. * ;
  
005    import  java.util. * ;
  
006   
  
007    public   class  HttpDownload
  
008   {
  
009        private  HashMap httpHeaders  =   new  HashMap();
  
010        private  String stateCode;
  
011   
  
012        //  generateHttpRequest方法
   013       
  
014        /*   ananlyzeHttpHeader方法
  015       *  
  016       *  addHeaderToMap方法
  017       * 
  018       *  analyzeFirstLine方法
  019       
*/      
  
020   
  
021        //  getFileSize方法
   022   
  023        //  download方法
   024           
  
025        /*   getHeader方法
  026       *  
  027       *  getIntHeader方法
  028       
*/
  
029   }
上面的代码只是HttpDownload 类的框架代码,其中的方法并未直正实现。我们可以从中看出第012 014 021 023 行就是上述的四个主要的方法。在016 018 行的addHeaderToMap analyzeFirstLine 方法将在analyzeHttpHeader 方法中用到。而025 027 行的getHeader getIntHeader 方法在getFileSize download 方法都会用到。上述的八个方法的实现都会在后面给出。

   001    private   void  generateHttpRequest(OutputStream out, String host,
  
002           String path,  long  startPos)  throws  IOException
  
003   {
  
004       OutputStreamWriter writer  =   new  OutputStreamWriter(out);
  
005       writer.write( " GET  "   +  path  +   "  HTTP/1.1\r\n " );
  
006       writer.write( " Host:  "   +  host  +   " \r\n " );
  
007       writer.write( " Accept: */*\r\n " );
  
008       writer.write( " User-Agent: My First Http Download\r\n " );
  
009        if  (startPos  >   0 //  如果是断点续传,加入Range字段
   010           writer.write( " Range: bytes= "   +  String.valueOf(startPos)  +   " -\r\n " );
  
011       writer.write( " Connection: close\r\n\r\n " );
  
012       writer.flush();
  
013   }
这个方法有四个参数:
1.   OutputStream out
使用Socket 对象的getOutputStream 方法得到的输出流。
2.  String host
下载文件所在的服务器的域名或IP
3.  String path
       下载文件在服务器上的路径,也就跟在GET 方法后面的部分。
4.  long startPos
       从文件的startPos 位置开始下载。如果startPos 0 ,则不生成Range 字段。
   001    private   void  analyzeHttpHeader(InputStream inputStream, DownloadEvent de)
  
002          throws  Exception
  
003   {
  
004       String s  =   "" ;
  
005        byte  b  =   - 1 ;
  
006        while  ( true )
  
007       {
  
008           b  =  ( byte ) inputStream.read();
  
009            if  (b  ==   ' \r ' )
  
010           {
  
011               b  =  ( byte ) inputStream.read();
  
012                if  (b  ==   ' \n ' )
  
013               {
  
014                    if  (s.equals( "" ))
  
015                        break ;
  
016                   de.viewHttpHeaders(s);
  
017                   addHeaderToMap(s);
  
018                   s  =   "" ;
  
019               }
  
020           }
  
021            else
  
022               s  +=  ( char ) b;
  
023       }
  
024   }
  
025
  
026    private   void  analyzeFirstLine(String s)
  
027   {
  
028       String[] ss  =  s.split( " [ ]+ " );
  
029        if  (ss.length  >   1 )
  
030           stateCode  =  ss[ 1 ];
  
031   }
  
032    private   void  addHeaderToMap(String s)
  
033   {
  
034        int  index  =  s.indexOf( " : " );
  
035        if  (index  >   0 )
  
036           httpHeaders.put(s.substring( 0 , index), s.substring(index  +   1 ) .trim());
  037        else
  
038           analyzeFirstLine(s);
  
039   }
001 024 行:analyzeHttpHeader 方法的实现。这个方法有两个参数。其中inputStream 是用Socket 对象的getInputStream 方法得到的输入流。这个方法是直接使用字节流来分析的HTTP 响应头(主要是因为下载的文件不一定是文本文件;因此,都统一使用字节流来分析和下载),每两个""r"n" 之间的就是一个字段和字段值对。在016 行调用了DownloadEvent 接口的viewHttpHeaders 事件方法来枚举每一个响应头字段。
026 031 行:analyzeFirstLine 方法的实现。这个方法的功能是分析响应消息头的第一行,并从中得到状态码后,将其保存在stateCode 变量中。这个方法的参数s 就是响应消息头的第一行。
032 039 行:addHeaderToMap 方法的实现。这个方法的功能是将每一个响应请求消息字段和字段值加到在HttpDownload 类中定义的httpHeaders 哈希映射中。在第034 行查找每一行消息头是否包含":" ,如果包含":" ,这一行必是消息头的第一行。因此,在第038 行调用了analyzeFirstLine 方法从第一行得到响应状态码。

   001    private  String getHeader(String header)
  
002   {
  
003        return  (String) httpHeaders.get(header);
  
004   }
  
005    private   int  getIntHeader(String header)
  
006   {
  
007        return  Integer.parseInt(getHeader(header));
  
008   }
    这两个方法将会在getFileSize download 中被调用。它们的功能是从响应消息中根据字段字得到相应的字段值。getHeader 得到字符串形式的字段值,而getIntHeader 得到整数型的字段值。
   001    public   long  getFileSize()
  
002   {
  
003        long  length  =   - 1 ;
  
004        try
  
005       {
  
006           length  =  getIntHeader( " Content-Length " );
  
007           String[] ss  =  getHeader( " Content-Range " ).split( " [/] " );
  
008            if  (ss.length  >   1 )
  
009               length  =  Integer.parseInt(ss[ 1 ]);
  
010            else
  
011               length  =   - 1 ;
  
012       }
  
013        catch  (Exception e)
  
014       {
  
015       }
  
016        return  length;
  
017   }
    getFileSize 方法的功能是得到下载文件的实际大小。首先在006 行通过Content-Length 得到了当前响应消息的实体内容大小。然后在009 行得到了Content-Range 字段值所描述的文件的实际大小(""" 后面的值) 。如果Content-Range 字段不存在,则文件的实际大小就是Content-Length 字段的值。如果Content-Length 字段也不存在,则返回-1 ,表示文件实际大小无法确定。
   001    public   void  download(DownloadEvent de, String url, String localFN,
  
002            int  cacheSize)  throws  Exception
  
003   {
  
004       File file  =   new  File(localFN); 
  
005        long  finishedSize  =   0 ;
  
006        long  fileSize  =   0 ;   //  localFN所指的文件的实际大小
   007       FileOutputStream fileOut  =   new  FileOutputStream(localFN,  true );
  
008       URL myUrl  =   new  URL(url);
  
009       Socket socket  =   new  Socket();
  
010        byte [] buffer  =   new   byte [cacheSize];  //  下载数据的缓冲
   011   
  
012        if  (file.exists())
  
013           finishedSize  =  file.length();        
  
014       
  
015        //  得到要下载的Web资源的端口号,未提供,默认是80
   016        int  port  =  (myUrl.getPort()  ==   - 1 ?   80  : myUrl.getPort();
  
017       de.state( " 正在连接 "   +  myUrl.getHost()  +   " : "   +  String.valueOf(port));
  
018       socket.connect( new  InetSocketAddress(myUrl.getHost(), port),  20000 );
  
019       de.state( " 连接成功! " );
  
020       
  
021        //  产生HTTP请求消息
   022       generateHttpRequest(socket.getOutputStream(), myUrl.getHost(), myUrl
  
023               .getPath(), finishedSize);
  
024         
  
025       InputStream inputStream  =  socket.getInputStream();
  
026        //  分析HTTP响应消息头
   027       analyzeHttpHeader(inputStream, de);
  
028       fileSize  =  getFileSize();   //  得到下载文件的实际大小
   029        if  (finishedSize  >=  fileSize)  
  
030            return ;
  
031        else
  
032       {
  
033            if  (finishedSize  >   0   &&  stateCode.equals( " 200 " ))
  
034                return ;
  
035       }
  
036        if  (stateCode.charAt( 0 !=   ' 2 ' )
  
037            throw   new  Exception( " 不支持的响应码 " );
  
038        int  n  =   0 ;
  
039        long  m  =  finishedSize;
  
040        while  ((n  =  inputStream.read(buffer))  !=   - 1 )
  
041       {
  
042           fileOut.write(buffer,  0 , n);
  
043           m  +=  n;
  
044            if  (fileSize  !=   - 1 )
  
045           {
  
046               de.percent(m  *   100   /  fileSize);
  
047           }
  
048       }
  
049       fileOut.close();
  
050       socket.close();
  
051   }
download 方法是断点续传工具的核心方法。它有四个参数:
1. DownloadEvent de
用于处理下载事件的接口。
2. String url
要下载文件的URL
3. String localFN
要保存的本地文件名,可以用这个文件的大小来确定已经下载了多少个字节。
4. int cacheSize
下载数据的缓冲区。也就是一次从服务器下载多个字节。这个值不宜太小,因为,频繁地从服务器下载数据,会降低网络的利用率。一般可以将这个值设为8192 8K )。
为了分析下载文件的url ,在008 行使用了URL 类,这个类在以后还会介绍,在这里只要知道使用这个类可以将使用各种协议的url (包括HTTP FTP 协议)的各个部分分解,以便单独使用其中的一部分。
029 行: 根据文件的实际大小和已经下载的字节数(finishedSize) 来判断是否文件是否已经下载完成。当文件的实际大小无法确定时,也就是fileSize 返回-1 时,不能下载。
033 行: 如果文件已经下载了一部分,并且返回的状态码仍是200 (应该是206 ),则表明服务器并不支持断点续传。当然,这可以根据另一个字段Accept-Ranges 来判断。
036 行: 由于本程序未考虑重定向( 状态码是3xx) 的情况,因此,在使用download 时,不要下载返回3xx 状态码的Web 资源。
040 048 行: 开始下载文件。第046 行调用DownloadEvent percent 方法来返回下载进度。
   001    package download;
  
002   
  
003    import  java.io. * ;
  
004   
  
005    class  NewProgress  implements  DownloadEvent
  
006   {
  
007        private   long  oldPercent  =   - 1 ;
  
008        public   void  percent( long  n)
  
009       {
  
010            if  (n  >  oldPercent)
  
011           {
  
012               System.out.print( " [ "   +  String.valueOf(n)  +   " %] " );
  
013               oldPercent  =  n;
  
014           }
  
015       }
  
016        public   void  state(String s)
  
017       {
  
018           System.out.println(s);
  
019       }
  
020        public   void  viewHttpHeaders(String s)
  
021       {
  
022           System.out.println(s);
  
023       }
  
024   }
  
025   
  
026    public   class  Main
  
027   {
  
028        public   static   void  main(String[] args)  throws  Exception
  
029       {
  
030           
  
031           DownloadEvent progress  =   new  NewProgress();
  
032            if  (args.length  <   1 )
  
033           {
  
034               System.out.println( " 用法:java class 下载文件名 " );
  
035                return ;
  
036           }
  
037           FileInputStream fis  =   new  FileInputStream(args[ 0 ]);
  
038           BufferedReader fileReader  =   new  BufferedReader( new  InputStreamReader(
  
039                           fis));
  
040           String s  =   "" ;
  
041           String[] ss;
  
042            while  ((s  =  fileReader.readLine())  !=   null )
  
043           {
  
044                try
  
045               {
  
046                   ss  =  s.split( " [ ]+ " );
  
047                    if  (ss.length  >   2 )
  
048                   {
  
049                       System.out.println( " \r\n--------------------------- " );
  
050                       System.out.println( " 正在下载: "   +  ss[ 0 ]);
  
051                       System.out.println( " 文件保存位置: "   +  ss[ 1 ]);
  
052                       System.out.println( " 下载缓冲区大小: "   +  ss[ 2 ]);
  
053                       System.out.println( " --------------------------- " );
  054                       HttpDownload httpDownload  =   new  HttpDownload();
  
055                       httpDownload.download( new  NewProgress(), ss[ 0 ], ss[ 1 ],
  
056                                       Integer.parseInt(ss[ 2 ]));
  
057                   }
  
058               }
  
059               catch  (Exception e)
  
060               {
  
061                   System.out.println(e.getMessage());
  
062               }
  
063           }
  
064           fileReader.close();
  
065       }
  
066   }
005 024 行: 实现DownloadEvent 接口的NewDownloadEvent 类。用于在Main 函数里接收相应事件传递的数据。
026 065 行: 下载工具的Main 方法。在这个Main 方法里,打开下载资源列表文件,逐行下载相应的Web 资源。
测试
假设download.txt 在当前目录中,内容如下:
http://files.cnblogs.com/nokiaguy/HttpSimulator.rar HttpSimulator.rar  8192
http://files.cnblogs.com/nokiaguy/designpatterns.rar designpatterns.rar 
4096
http://files.cnblogs.com/nokiaguy/download.rar download.rar 8192
这两个URL 是在本机的Web 服务器( IIS) 的虚拟目录中的两个文件,将它们下载在D 盘根目录。
运行下面的命令:
java download.Main download.txt
现在大部分的网站使用的是标准HTML的上传方式来上传文件。一般情况下标准HTML方式在网页中只能上传4MB左右的文件,如果访问的用户比较多的时侯这种方式容易上传失败。虽然标准HTML上传方式开发起来比较简单,但是这种方式用户体验比较差,上传的文件大小受到限制,所以如果我们需要上传上百或者更大的上G的文件时,HTML标准上传方式是无法满足我们的需求的。 而另一方面,随着互联网行业的发展用户产生的新的需求也越来越多,同时对用户体验也提出了更高的要求,传统的HTML方式也越来越难已满足新的用户需求。现在大部分的用户有文件批量上传的需求,希望只通过点击一次鼠标就能够批量的上传多张图片,而不是一张张的选择文件上传,这样操作即浪费时间又非常烦琐。 近年来,由于数码和影视行业的迅猛发展刺激了用户对大文件的上传需求,现在越来越多的用户希望能够通过WEB的方式上传更大的文件,比如电影和图片。这些类型的文件通常都非常大,一般都在500MB以上,高清的影视文件至少在1G以上。这样的大文件是根本无法通过标准HTML方式来上传的。 不仅如此,由于国内网络环境比较特殊,有许多地区的网络不够稳定,在上传文件的过程中可能会发生断网的情况。如果用户正在上传一个1000MB的文件,已经上传了500MB,这时网络出现问题上传中止了。那么下一次用户需要要重新上传前面的500MB,而不是从500MB开始上传,这将浪费用户的许多时间。 新颖网络HTTP文件断点续传控件是专门用于解决HTTP大文件上传的需求而开发的产品。通过我们的HttpPartition模块用户能够非常方便的一次性选择超过200个的文件。而且我们升级了用户体验,用户现在不仅能够通过点击按钮来选择多个文件,还可以通过HttpDroper来拖拽文件甚至是文件夹。 现在我们能够轻松支持2G左右的大文件上传。为了减轻服务器的压力在HttpUploader模块中我们并不是一次上传2G的数据,而是将2G化分为小的数据块,每次向服务器上传约128KB左右的数据。同时在每次上传的数据中带了文件大小,起始位置,文件MD5等信息。对于开发人员来说,有了这些信息,断点续传功能将会变的和普通的文件上传功能一样简单。 相信新颖网络HTTP断点续传控件能够帮助您赢利市场。 版权所有 2009-2012 北京新颖网络 保留所有权利 官方网站:http://www.ncmem.com/ 产品首页:http://www.ncmem.com/webplug/http-uploader3/index.aspx 在线演示:http://www.ncmem.com/products/http-uploader/demo/index.html 产品介绍:http://www.cnblogs.com/xproer/archive/2012/02/17/2355440.html 开发文档-ASP:http://www.cnblogs.com/xproer/archive/2012/02/17/2355458.html 开发文档-PHP:http://www.cnblogs.com/xproer/archive/2012/02/17/2355467.html 开发文档-JSP:http://www.cnblogs.com/xproer/archive/2012/02/17/2355462.html 开发文档-ASP.NET:http://www.cnblogs.com/xproer/archive/2012/02/17/2355469.html 升级日志:http://www.cnblogs.com/xproer/archive/2012/02/17/2355449.html 示例下载:http://www.ncmem.com/download/HttpUploader3-demo.rar 文档下载:http://www.ncmem.com/download/HttpUploader3-doc.rar 问题反馈:http://www.ncmem.com/blog/guestbook.asp Windows数字证书补丁:http://www.ncmem.com/download/rootsupd.rar Microsoft Visual C++ 2008 Redistributable Package (x86)http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值