在前面的文章曾讨论了HTTP消息头的三个和断点继传有关的字段一个是请求消息的字段Range另两个是响应消息字段AcceptRanges和ContentRange其中AcceptRanges用来断定Web服务器是否支持断点继传功能在这里为了演示如何实现断点继传功能假设Web服务器支持这个功能因此我们只使用Range和ContentRange来完成一个断点继传工具的开发
● 要实现一个什么样的断点续传工具?
这个断点续工具是一个单线程的下载工具它通过参数传入一个文本文件这个文件的格式如下
/jpgd:\okjpg
/jpgd:\okjpg
/jpgd:\okjpg
这个文本文件的每一行是一个下载项这个下载项分为三部分
● 要下载的Web资源的URL
● 要保存的本地文件名
● 下载的缓沖区大小(单位是字节)
使用至少一个空格来分隔这三部分这个下载工具逐个下载这些文件在这些文件全部下载完后程序退出
● 断点续传的工作原理
断点续传顾名思义就是一个文件下载了一部分后由于服务器或客户端的原因当前的网络连接中断了在中断网络连接后用户还可以再次建立网络连接来继续下载这个文件还没有下完的部分
要想实现单线程断点续传必须在客户断保存两个数据
已经下载的字节数
下载文件的URL
一但重新建立网络连接后就可以利用这两个数据接着未下载完的文件继续下载在本下载工具中第一种数据就是文件已经下载的字节数而第二个数据在上述的下载文件中保存
在继续下载时检测已经下载的字节数假设已经下载了个字节那么HTTP请求消息头的Range字段被设为如下形式
Range:bytes=
HTTP响应消息头的ContentRange字段被设为如下的形式
ContentRange:bytes/
● 实现断点续传下载工具
一个断点续传下载程序可按如下几步实现
输入要下载文件的URL和要保存的本地文件名并通过Socket类连接到这个URL
所指的服务器上
在客户端根据下载文件的URL和这个本地文件生成HTTP请求消息在生成请求
消息时分为两种情况
()第一次下载这个文件按正常情况生成请求消息也就是说生成不包含Range
字段的请求消息
()以前下载过这次是接着下载这个文件这就进入了断点续传程序在这种情况生成的HTTP请求消息中必须包含Range字段由于是单线程下载因此这个已经下载了一部分的文件的大小就是Range的值假设当前文件的大小是个字节那么将Range设成如下的值
Rangebytes=
向服务器发送HTTP请求消息
接收服务器返回的HTTP响应消息
处理HTTP响应消息在本程序中需要从响应消息中得到下载文件的总字节数如
果是第一次下载也就是说响应消息中不包含ContentRange字段时这个总字节数也就是ContentLength字段的值如果响应消息中不包含ContentLength字段则这个总字节数无法确定这就是为什么使用下载工具下载一些文件时没有文件大小和下载进度的原因如果响应消息中包含ContentRange字段总字节数就是ContentRangebytes mn/k中的k如ContentRange的值为
ContentRangebytes/
则总字节数为由于本程序使用的Range值类型是得到从某个字节开始往后的所有字节因此当前的响应消息中的ContentRange总是能返回还有多少个字节未下载如上面的例子未下载的字节数为+=
开始下载文件并计算下载进度(百分比形式)如果网络连接断开时文件仍未下载完重新执行第一步也果文件已经下载完退出程序
分析以上六个步骤得知有四个主要的功能需要实现
生成HTTP请求消息并将其发送到服务器这个功能由generateHttpRequest方法来完成
分析HTTP响应消息头这个功能由analyzeHttpHeader方法来完成
得到下载文件的实际大小这个功能由getFileSize方法来完成
下载文件这个功能由download方法来完成
以上四个方法均被包含在这个断点续传工具的核心类HttpDownloadjava中在给出HttpDownload类的实现之前先给出一个接口DownloadEvent接口从这个接口的名字就可以看出它是用来处理下载过程中的事件的下面是这个接口的实现代码
package download;
publicinterfaceDownloadEvent
{
voidpercent(longn);//下载进度
voidstate(Strings);//连接过程中的状态切换
voidviewHttpHeaders(Strings);//枚举每一个响应消息字段
}
从上面的代码可以看出DownloadEvent接口中有三个事件方法在以后的主函数中将实现这个接口来向控制台输出相应的信息下面给出了HttpDownload类的主体框架代码
package download;
import*;
importjavaio*;
importjavautil*;
publicclassHttpDownload
{
privateHashMaphttpHeaders=newHashMap();
privateStringstateCode;
//generateHttpRequest方法
/*ananlyzeHttpHeader方法
*
*addHeaderToMap方法
*
*analyzeFirstLine方法
*/
//getFileSize方法
//download方法
/*getHeader方法
*
*getIntHeader方法
*/
}
上面的代码只是HttpDownload类的框架代码其中的方法并未直正实现我们可以从中看出第和行就是上述的四个主要的方法在和行的addHeaderToMap和analyzeFirstLine方法将在analyzeHttpHeader方法中用到而和行的getHeader和getIntHeader方法在getFileSize和download方法都会用到上述的八个方法的实现都会在后面给出
privatevoidgenerateHttpRequest(OutputStreamoutStringhost
StringpathlongstartPos)throwsIOException
{
OutputStreamWriterwriter=newOutputStreamWriter(out);
writerwrite(GET+path+HTTP/\r\n);
writerwrite(Host:+host+\r\n);
writerwrite(Accept:*/*\r\n);
writerwrite(UserAgent:MyFirstHttpDownload\r\n);
if(startPos>)//如果是断点续传加入Range字段
writerwrite(Range:bytes=+StringvalueOf(startPos)+\r\n);
writerwrite(Connection:close\r\n\r\n);
writerflush();
}
这个方法有四个参数
OutputStream out
使用Socket对象的getOutputStream方法得到的输出流
String host
下载文件所在的服务器的域名或IP
String path
下载文件在服务器上的路径也就跟在GET方法后面的部分
long startPos
从文件的startPos位置开始下载如果startPos为则不生成Range字段
privatevoidanalyzeHttpHeader(InputStreaminputStreamDownloadEventde)
throwsException
{
Strings=;
byteb=;
while(true)
{
b=(byte)inputStreamread();
if(b==\r)
{
b=(byte)inputStreamread();
if(b==\n)
{
if(sequals())
break;
deviewHttpHeaders(s);
addHeaderToMap(s);
s=;
}
}
else
s+=(char)b;
}
}
privatevoidanalyzeFirstLine(Strings)
{
String[]ss=ssplit([]+);
if(sslength>)
stateCode=ss[];
}
privatevoidaddHeaderToMap(Strings)
{
intindex=sindexOf(:);
if(index>)
(ssubstring(index)ssubstring(index+)trim());
else
analyzeFirstLine(s);
}
第 ;行analyzeHttpHeader方法的实现这个方法有两个参数其中inputStream是用Socket对象的getInputStream方法得到的输入流这个方法是直接使用字节流来分析的HTTP响应头(主要是因为下载的文件不一定是文本文件因此都统一使用字节流来分析和下载)每两个rn之间的就是一个字段和字段值对在行调用了DownloadEvent接口的viewHttpHeaders事件方法来枚举每一个响应头字段
第 ;行analyzeFirstLine方法的实现这个方法的功能是分析响应消息头的第一行并从中得到状态码后将其保存在stateCode变量中这个方法的参数s就是响应消息头的第一行
第 ;行addHeaderToMap方法的实现这个方法的功能是将每一个响应请求消息字段和字段值加到在HttpDownload类中定义的httpHeaders哈希映射中在第行查找每一行消息头是否包含:如果包含:这一行必是消息头的第一行因此在第行调用了analyzeFirstLine方法从第一行得到响应状态码
privateStringgetHeader(Stringheader)
{
return(String)(header);
}
privateintgetIntHeader(Stringheader)
{
returnIntegerparseInt(getHeader(header));
}
这两个方法将会在getFileSize和download中被调用它们的功能是从响应消息中根据字段字得到相应的字段值getHeader得到字符串形式的字段值而getIntHeader得到整数型的字段值
publiclonggetFileSize()
{
longlength=;
try
{
length=getIntHeader(ContentLength);
String[]ss=getHeader(ContentRange)split([/]);
if(sslength>)
length=IntegerparseInt(ss[]);
else
length=;
}
catch(Exceptione)
{
}
returnlength;
}
getFileSize方法的功能是得到下载文件的实际大小首先在行通过ContentLength得到了当前响应消息的实体内容大小然后在行得到了ContentRange字段值所描述的文件的实际大小(后面的值)如果ContentRange字段不存在则文件的实际大小就是ContentLength字段的值如果ContentLength字段也不存在则返回表示文件实际大小无法确定
publicvoiddownload(DownloadEventdeStringurlStringlocalFN
intcacheSize)throwsException
{
Filefile=newFile(localFN);
longfinishedSize=;
longfileSize=;//localFN所指的文件的实际大小
FileOutputStreamfileOut=newFileOutputStream(localFNtrue);
URLmyUrl=newURL(url);
Socketsocket=newSocket();
byte[]buffer=newbyte[cacheSize];//下载数据的缓沖
if(fileexists())
finishedSize=filelength();
//得到要下载的Web资源的端口号未提供默认是
intport=(myUrlgetPort()==)?:myUrlgetPort();
destate(正在连接+myUrlgetHost()+:+StringvalueOf(port));
nnect(newInetSocketAddress(myUrlgetHost()port));
destate(连接成功!);
//产生HTTP请求消息
generateHttpRequest(socketgetOutputStream()myUrlgetHost()myUrl
getPath()finishedSize);
InputStreaminputStream=socketgetInputStream();
//分析HTTP响应消息头
analyzeHttpHeader(inputStreamde);
fileSize=getFileSize();//得到下载文件的实际大小
if(finishedSize>=fileSize)
return;
else
{
if(finishedSize>&&stateCodeequals())
return;
}
if(stateCodecharAt()!=)
thrownewException(不支持的响应码);
intn=;
longm=finishedSize;
while((n=inputStreamread(buffer))!=)
{
fileOutwrite(buffern);
m+=n;
if(fileSize!=)
{
depercent(m*/fileSize);
}
}
fileOutclose();
socketclose();
}
download方法是断点续传工具的核心方法它有四个参数
DownloadEvent de
用于处理下载事件的接口
String url
要下载文件的URL
String localFN
要保存的本地文件名可以用这个文件的大小来确定已经下载了多少个字节
int cacheSize
下载数据的缓沖区也就是一次从服务器下载多个字节这个值不宜太小因为频繁地从服务器下载数据会降低网络的利用率一般可以将这个值设为(K)
为了分析下载文件的url在行使用了URL类这个类在以后还会介绍在这里只要知道使用这个类可以将使用各种协议的url(包括HTTP和FTP协议)的各个部分分解以便单独使用其中的一部分
第行根据文件的实际大小和已经下载的字节数(finishedSize)来判断是否文件是否已经下载完成当文件的实际大小无法确定时也就是fileSize返回时不能下载
第行如果文件已经下载了一部分并且返回的状态码仍是(应该是)则表明服务器并不支持断点续传当然这可以根据另一个字段AcceptRanges来判断
第行由于本程序未考虑重定向(状态码是xx)的情况因此在使用download时不要下载返回xx状态码的Web资源
第 ;行开始下载文件第行调用DownloadEvent的percent方法来返回下载进度
package download;
importjavaio*;
classNewProgressimplementsDownloadEvent
{
privatelongoldPercent=;
publicvoidpercent(longn)
{
if(n>oldPercent)
{
Systemoutprint([+StringvalueOf(n)+%]);
oldPercent=n;
}
}
publicvoidstate(Strings)
{
Systemoutprintln(s);
}
publicvoidviewHttpHeaders(Strings)
{
Systemoutprintln(s);
}
}
publicclassMain
{
publicstaticvoidmain(String[]args)throwsException
{
DownloadEventprogress=newNewProgress();
if(argslength<)
{
Systemoutprintln(用法javaclass下载文件名);
return;
}
FileInputStreamfis=newFileInputStream(args[]);
BufferedReaderfileReader=newBufferedReader(newInputStreamReader(
fis));
Strings=;
String[]ss;
while((s=fileReaderreadLine())!=null)
{
try
{
ss=ssplit([]+);
if(sslength>)
{
Systemoutprintln(\r\n);
Systemoutprintln(正在下载:+ss[]);
Systemoutprintln(文件保存位置:+ss[]);
Systemoutprintln(下载缓沖区大小:+ss[]);
Systemoutprintln();
HttpDownloadhttpDownload=newHttpDownload();
(newNewProgress()ss[]ss[]
IntegerparseInt(ss[]));
}
}
catch(Exceptione)
{
Systemoutprintln(egetMessage());
}
}
fileReaderclose();
}
}
第 ;行实现DownloadEvent接口的NewDownloadEvent类用于在Main函数里接收相应事件传递的数据
第 ; 行下载工具的Main方法在这个Main方法里打开下载资源列表文件逐行下载相应的Web资源
测试
假设downloadtxt在当前目录中内容如下
;HttpSimulatorrar
;designpatternsrar
downloadrar
这两个URL是在本机的Web服务器(如IIS)的虚拟目录中的两个文件将它们下载在D盘根目录
运行下面的命令
javadownloadMaindownloadtxt
运行的结果如图所示
图