源代码下载:
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
这个文本文件的每一行是一个下载项,这个下载项分为三部分:
使用至少一个空格来分隔这三部分。这个下载工具逐个下载这些文件,在这些文件全部下载完后程序退出。
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开发速学宝典》出版,欢迎定购