本文为原创,如需转载,请注明作者和出处,谢谢!
上一篇:Java网络编程从入门到精通(23):HTTP消息头字段
源代码下载:download.rar
在前面的文章曾讨论了HTTP消息头的三个和断点继传有关的字段。一个是请求消息的字段Range,另两个是响应消息字段Accept-Ranges和Content-Range。其中Accept-Ranges用来断定Web服务器是否支持断点继传功能。在这里为了演示如何实现断点继传功能,假设Web服务器支持这个功能;因此,我们只使用Range和Content-Range来完成一个断点继传工具的开发。
l 要实现一个什么样的断点续传工具?
这个断点续工具是一个单线程的下载工具。它通过参数传入一个文本文件。这个文件的格式如下:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
http://www.ishare.cc/d/
1174254
-
2
/
106
.jpgd:
\
ok1.jpg8192
http://www.ishare.cc/d/1174292-2/156.jpgd:
\
ok2.jpg
12345
http://www.ishare.cc/d/
1174277
-
2
/
147
.jpgd:
\
ok3.jpg3456
这个文本文件的每一行是一个下载项,这个下载项分为三部分:
使用至少一个空格来分隔这三部分。这个下载工具逐个下载这些文件,在这些文件全部下载完后程序退出。
l 断点续传的工作原理
“断点续传”顾名思义,就是一个文件下载了一部分后,由于服务器或客户端的原因,当前的网络连接中断了。在中断网络连接后,用户还可以再次建立网络连接来继续下载这个文件还没有下完的部分。
要想实现单线程断点续传,必须在客户断保存两个数据。
1. 已经下载的字节数。
2. 下载文件的URL。
一但重新建立网络连接后,就可以利用这两个数据接着未下载完的文件继续下载。在本下载工具中第一种数据就是文件已经下载的字节数,而第二个数据在上述的下载文件中保存。
在继续下载时检测已经下载的字节数,假设已经下载了3000个字节,那么HTTP请求消息头的Range字段被设为如下形式:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
Range:bytes
=
3000
-
HTTP响应消息头的Content-Range字段被设为如下的形式:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
Content-Range:bytes
3000
-
10000
/
10001
l 实现断点续传下载工具
一个断点续传下载程序可按如下几步实现:
1. 输入要下载文件的URL和要保存的本地文件名,并通过Socket类连接到这个URL
所指的服务器上。
2. 在客户端根据下载文件的URL和这个本地文件生成HTTP请求消息。在生成请求
消息时分为两种情况:
(1)第一次下载这个文件,按正常情况生成请求消息,也就是说生成不包含Range
字段的请求消息。
(2)以前下载过,这次是接着下载这个文件。这就进入了断点续传程序。在这种情况生成的HTTP请求消息中必须包含Range字段。由于是单线程下载,因此,这个已经下载了一部分的文件的大小就是Range的值。假设当前文件的大小是1234个字节,那么将Range设成如下的值:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
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的值为:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
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(Strings);
//
连接过程中的状态切换
void
viewHttpHeaders(Strings);
//
枚举每一个响应消息字段
}
从上面的代码可以看出,DownloadEvent接口中有三个事件方法。在以后的主函数中将实现这个接口,来向控制台输出相应的信息。下面给出了HttpDownload类的主体框架代码:
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
001
package
download;
002
003
import
java.net.
*
;
004
import
java.io.
*
;
005
import
java.util.
*
;
006
007
public
class
HttpDownload
008
{
009
private
HashMaphttpHeaders
=
new
HashMap();
010
private
StringstateCode;
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方法都会用到。上述的八个方法的实现都会在后面给出。
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
001
private
void
generateHttpRequest(OutputStreamout,Stringhost,
002
Stringpath,
long
startPos)
throws
IOException
003
{
004
OutputStreamWriterwriter
=
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:MyFirstHttpDownload\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字段。
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
001
private
void
analyzeHttpHeader(InputStreaminputStream,DownloadEventde)
002
throws
Exception
003
{
004
Strings
=
""
;
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(Strings)
027
{
028
String[]ss
=
s.split(
"
[]+
"
);
029
if
(ss.length
>
1
)
030
stateCode
=
ss[
1
];
031
}
032
private
void
addHeaderToMap(Strings)
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方法从第一行得到响应状态码。
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
001
private
StringgetHeader(Stringheader)
002
{
003
return
(String)httpHeaders.get(header);
004
}
005
private
int
getIntHeader(Stringheader)
006
{
007
return
Integer.parseInt(getHeader(header));
008
}
这两个方法将会在getFileSize和download中被调用。它们的功能是从响应消息中根据字段字得到相应的字段值。getHeader得到字符串形式的字段值,而getIntHeader得到整数型的字段值。
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
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
(Exceptione)
014
{
015
}
016
return
length;
017
}
getFileSize方法的功能是得到下载文件的实际大小。首先在006行通过Content-Length得到了当前响应消息的实体内容大小。然后在009行得到了Content-Range字段值所描述的文件的实际大小("""后面的值)。如果Content-Range字段不存在,则文件的实际大小就是Content-Length字段的值。如果Content-Length字段也不存在,则返回-1,表示文件实际大小无法确定。
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
001
public
void
download(DownloadEventde,Stringurl,StringlocalFN,
002
int
cacheSize)
throws
Exception
003
{
004
Filefile
=
new
File(localFN);
005
long
finishedSize
=
0
;
006
long
fileSize
=
0
;
//
localFN所指的文件的实际大小
007
FileOutputStreamfileOut
=
new
FileOutputStream(localFN,
true
);
008
URLmyUrl
=
new
URL(url);
009
Socketsocket
=
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
InputStreaminputStream
=
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方法来返回下载进度。
<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->
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(Strings)
017
{
018
System.out.println(s);
019
}
020
public
void
viewHttpHeaders(Strings)
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
DownloadEventprogress
=
new
NewProgress();
032
if
(args.length
<
1
)
033
{
034
System.out.println(
"
用法:javaclass下载文件名
"
);
035
return
;
036
}
037
FileInputStreamfis
=
co
分享到:
评论