一、基本思想
利用 HTTP 请求的Range标头值,来向服务端传递请求数据的开始位置和结束位置。服务端获得这两个参数后,将指定范围内的数据传递给客户端。当客户端请求暂停或中断之后,待到客户端再次向服务器发起请求,继续下载数据时,客户端传递给服务端的Range值说明了向服务端请求数据的范围,即从上一次中断传输的位置开始直到最后。
二、示例代码
1 DownloadCore:完成下载任务
public class DownloadCore<T>
{
private HttpRequestMessage request;
private IFileProvider<T> fileProvider;
private T requestModel;
public DownloadCore(HttpRequestMessage request,IFileProvider<T> fileProvider,T requestModel)
{
this.request = request;
this.fileProvider = fileProvider;
this.requestModel = requestModel;
}
public HttpResponseMessage Download()
{
if (requestModel == null)
{
//抛出异常
}
if (!fileProvider.Exists(requestModel))
{
//抛出异常 }
long fileLength = fileProvider.GetLength(requestModel);
if (fileLength == 0)
{
//抛出异常
}
ContentInfo contentInfo = GetContentInfoFromRequest(fileLength);
Stream stream = PartialStream.GetPartialStream(fileProvider.Open(requestModel), contentInfo.From, contentInfo.To);
if (stream == null)
{
//抛出异常
}
HttpContent content = new StreamContent(stream, AppSettings.BufferSize);
return SetResponse(content, contentInfo, fileLength, fileProvider.GetFileName(requestModel));
}
private ContentInfo GetContentInfoFromRequest(long entityLength)
{
var result = new ContentInfo
{
From = 0,
To = entityLength - 1,
IsPartial = false,
Length = entityLength
};
RangeHeaderValue rangeHeader = request.Headers.Range;
if (rangeHeader != null && rangeHeader.Ranges.Count != 0)
{
//仅支持一个range
if (rangeHeader.Ranges.Count > 1)
{
//抛出异常
}
RangeItemHeaderValue range = rangeHeader.Ranges.First();
if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entityLength - 1)
{
//抛出异常
}
result.From = range.From ?? 0;
result.To = range.To ?? entityLength - 1;
result.IsPartial = true;
result.Length = entityLength;
if (range.From.HasValue && range.To.HasValue)
{
result.Length = range.To.Value - range.From.Value + 1;
}
else if (range.From.HasValue)
{
result.Length = entityLength - range.From.Value;
}
else if (range.To.HasValue)
{
result.Length = range.To.Value + 1;
}
}
return result;
}
private HttpResponseMessage SetResponse(HttpContent content, ContentInfo contentInfo, long entityLength, string fileNameWithExtend)
{
HttpResponseMessage response = new HttpResponseMessage() ;
response.Headers.AcceptRanges.Add("bytes");
response.StatusCode = contentInfo.IsPartial ? HttpStatusCode.PartialContent : HttpStatusCode.OK;
response.Content = content;
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = fileNameWithExtend;
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentLength = contentInfo.Length;
if (contentInfo.IsPartial)
{
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(contentInfo.From, contentInfo.To, entityLength);
}
return response;
}
}
网络请求参数解析
public static DownloadUriModel GetDownloadParam(HttpRequestMessage request)
{
var uriQuery = request.RequestUri.Query;
//针对中文重新编码
uriQuery = HttpUtility.UrlDecode(uriQuery);
var queryChildren = uriQuery.Substring(1, uriQuery.Length - 1).Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries).ToList();
Dictionary<string, string> queryDict = new Dictionary<string, string>();
queryChildren.ForEach(item =>
{
var child = item.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
queryDict.Add(child[0], child[1]);
});
string fileName = queryDict.ContainsKey("fileName") ? queryDict["fileName"] : null;
if (string.IsNullOrWhiteSpace(fileName))
{
//抛出异常
}
string extend = Path.GetExtension(fileName);
if (!string.IsNullOrWhiteSpace(extend))
{
fileName = Path.GetFileNameWithoutExtension(fileName);
}
string fileType = queryDict.ContainsKey("fileType") ? queryDict["fileType"] : null;
if (string.IsNullOrWhiteSpace(fileType))
{
//抛出异常
}
return new DownloadUriModel
{
FileNameNoExtend = fileName,
FileType = fileType,
};
}
部分流数据
public class PartialStream
{
public static Stream GetPartialStream(Stream fileStream, long start, long end)
{
if (fileStream == null)
{
return null;
}
if (start > 0)
{
fileStream.Seek(start, SeekOrigin.Begin);
}
return fileStream;
}
}
2 数据
下载数据的来源包括本地磁盘,网络,数据库等,这里只列举待下载数据在本地磁盘和网络的情形。
本地磁盘
public class DiskFileProvider : IFileProvider<DownloadRequestModel>
{
/// <summary>
/// android app 所在文件夹路径
/// </summary>
private readonly string filesDirectory;
public DiskFileProvider()
{
filesDirectory = AppSettings.AppLocation;
}
public bool Exists(DownloadRequestModel model)
{
string searchPattern = string.Format("{0}.{1}",model.FileNameNoExtend, model.FileType);
string file = Directory.GetFiles(filesDirectory, searchPattern, SearchOption.TopDirectoryOnly)
.FirstOrDefault();
return file != null;
}
public Stream Open(DownloadRequestModel model)
{
return File.Open(GetFilePath(model), FileMode.Open, FileAccess.Read,FileShare.Delete);
}
public long GetLength(DownloadRequestModel model)
{
return new FileInfo(GetFilePath(model)).Length;
}
private string GetFilePath(DownloadRequestModel model)
{
string searchPattern = string.Format("{0}.{1}", model.FileNameNoExtend, model.FileType);
return Path.Combine(filesDirectory, searchPattern);
}
public string GetFileName(DownloadRequestModel model)
{
return model.FileNameWithExtend;
}
}
网络
public class NetFileProvider: IFileProvider<DownloadUriModel>
{
public bool Exists(DownloadUriModel model)
{
//具体实现
}
public Stream Open(DownloadUriModel model)
{
//具体实现
}
......
}
统一的接口
public interface IFileProvider<T>
{
bool Exists(T model);
Stream Open(T model);
long GetLength(T model);
string GetFileName(T model);
}
3 数据模型
public class DownloadRequestModel
{
/// <summary>
/// 文件名(不含扩展名)
/// </summary>
public string FileNameNoExtend { get; set; }
/// <summary>
/// 文件类型(扩展名)
/// </summary>
public string FileType { get; set; }
/// <summary>
/// 文件名
/// </summary>
public string FileNameWithExtend
{
get
{
string extend = FileType.Contains(".") ? FileType.Substring(1) : FileType;
return string.Format("{0}.{1}", FileNameNoExtend, extend);
}
}
}
//扩展实现
public class DownloadUriModel:DownloadRequestModel
{
......
}
public class ContentInfo
{
public long From;
public long To;
public bool IsPartial;
public long Length;
}