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
    运行的结果如图1 所示。

图1
国内最棒的Google Android技术社区(eoeandroid),欢迎访问!

《银河系列原创教程》发布

《Java Web开发速学宝典》出版,欢迎定购

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