在本系列的前两篇文章中,分别向大家介绍了用于完成下载任务的 WebClinet 和 WinINet 的基本用法和一些实用技巧。
今天来为大家讲述下载过程中最常遇到的断点续传问题。
首先明确一点,本文所说的断点续传特指 HTTP 协议中的断点续传,文章中讲述了实现断点续传的方法思路和关键代码,想了解更多细节的同学,请下载并查看本文附带的 demo。
工作原理
http 协议中定义了一些请求/响应头,通过组合使用这些头信息,即可实现分批下载同一文件的目的。例如,在一次 http 请求中只请求文件中的一部分数据,然后将请求到的数据保存起来,下次只需请求剩余部分的数据,当全部数据都下载到本地后再完成数据的合并工作。
http 协议指出,可以通过 http 请求中的 Range 头来指定请求数据的范围。
Range 头的使用很简单,按照如下的格式使用即可:
Range: bytes=500-999
上述意思为:只请求目标文件的第500至第999,这500个字节。
举例说明,有一个1000 字节大小的文件需要下载,第一次请求时不指定 Range 头,表示下载整个文件;但在下载完第499个字节后,下载被中断了,那么在下一次请求剩余文件时,只需要下载第500个至第999个字节的数据即可。
原理看上去很简单,但是需要考虑以下几个问题:
1. 是不是所有的 web 服务器都支持 Range 头?
2. 多次请求之间可能会间隔很长的时间,服务器上的文件发生了变化怎么办?
3. 如何保存下载的部分数据和相关信息?
4. 当我们通过字节操作把一个文件拼成原始大小后,如何验证它和源文件是一模一样的?
接下来,本文分别针对以上问题,给出解决方法。
一、如何检查服务器端是否支持 Range头?
在服务器响应请求时,会在响应头中通过 Accept-Ranges 指明是否接受请求资源的一部分数据,这里似乎有个小问题,就是不同的服务器可能返回不同的值来指明是否接受下载部分资源的请求。比较统一的做法是:当服务器不支持请求部分数据时,都会返回 Accept-Ranges: none,所以只需判断返回值是否等于 none 就可以了。
代码如下:
private static bool IsAcceptRanges ( WebResponse res )
{
if ( res.Headers["Accept-Ranges"] != null )
{
string s = res.Headers["Accept-Ranges"];
if ( s == "none" )
{
return false;
}
}
return true;
}
二、如何检查服务器端的文件是否发生了变化?
当我们在下载文件的过程中,由于网络故障等原因中断了下载过程,这时如果服务器上的文件已经变化了,那么无论如何都需要重新从头开始下载,只有当服务器上的文件没有发生变化的情况下,断点续传才有意义。
当下次需要继续下载文件时,如何确定服务器上的文件还是当初下载了一半的文件?
对于这个问题,http 响应头为我们提供了两种选择,使用 ETag 和 Last-Modified 都能完成下载任务。
先看 ETag:
The ETag response-header field provides the current value of the entity tag for the requested variant. (引自RFC2616 14.19 ETag)
简单点说 ETag 就是一个标识当前请求内容的字符串,当请求的资源发生变化后,对应的 ETag 也会变化,所以最简单的办法是,第一次请求时把响应头中的 ETag 保存下来,下次请求时做相应的比较。
代码如下:
string newEtag = GetEtag( response );
// tempFileName指已经下载到本地的部分文件内容
// tempFileInfoName指保存了Etag内容的临时文件
if ( File.Exists(tempFileName) && File.Exists(tempFileInfoName) )
{
string oldEtag = File.ReadAllText( tempFileInfoName );
if ( !string.IsNullOrEmpty(oldEtag) && !string.IsNullOrEmpty(newEtag) && newEtag == oldEtag )
{
// Etag没有变化,可以断点续传
resumeDowload = true;
}
}
else
{
if ( !string.IsNullOrEmpty(newEtag) )
{
File.WriteAllText( tempFileInfoName, newEtag );
}
}
//GetEtag函数
private static string GetEtag( WebResponse res )
{
if ( res.Headers["ETag"] != null )
{
return res.Headers["ETag"];
}
return null;
}
再看 Last-Modified:
The Last-Modified entity-header field indicates the date and time at which the origin server believes the variant was last modified. (引自RFC2616 14.29 Last-Modified)
Last-Modified 就是所请求的资源在服务器上最后一次的修改时间,使用方法和 ETag 大体相同。
不论是使用 ETag 还是 Last-Modified,都能达到检测服务器端文件是否发生变化的目的。
当然也可以同时使用这两种方法,做 double check,以便更好的实现检测目的。
三、如何保存下载的部分数据和相关信息?
这里主要是指使用 C# 进行数据和相关信息的保存操作,大体思路是如果有未下载完的文件,先将已下载数据保存在某一路径下,然后将后下载的字节数据添加到已下载文件的末尾。
详细的实现方法,请查看 demo 代码。
四、如何验证下载文件与源文件的一致性?
在断点续传的过程中,我们以 byte 为单位进行文件的下载和合并,如果下载的整个过程中出现了异常,可能最后得到的文件就和源文件不一样了,因此最好能够对下载好的文件进行一次与源文件一致性的校验,这是很重要的一步,也是最难实现的部分。之所以难以实现,是因为需要服务器端的支持,例如要求服务器端不但提供了可供下载的文件,同时还需要提供该文件的 MD5 hush。
当然,如果服务器端也是我们自己创建的,我们就可以实现服务器端方面的支持。目前已有部分产品在下载过程中提供断点续传的能力,Spread Studio表格控件就是其中之一。