原文章地址:

源代码下载:

在前面的文章曾讨论了HTTP消息头的三个和断点继传有关的字段。一个是请求消息的字段Range,另两个是响应消息字段Accept-RangesContent-Range。其中Accept-Ranges用来断定Web服务器是否支持断点继传功能。在这里为了演示如何实现断点继传功能,假设Web服务器支持这个功能;因此,我们只使用RangeContent-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
所示。