ASP.NET Core斷點續傳
在ASP.NET WebAPi寫過完整的斷點續傳文章,目前我對ASP.NET Core僅止於整體上會用,對於原理還未去深入學習,由於有園友想看斷點續傳在ASP.NET Core中的具體實現,於是借助在家中休息時間看了下ASP.NET Core是否支持斷點續傳以及支持后具體實現以及相關APi,花了一點時間,本文而由此而生。
斷點續傳基礎
此前在ASP.NET WebAPi中對於一些基礎內容已經詳細講解過,同時也進行了封裝,所以再處理ASP.NET Core不過是APi使用不同罷了,斷點續傳重點在於AcceptRange和ContentRange以及對應響應請求頭設置,其余和ASP.NET WebAPi使用別無二致。在ASP.NET WebAPi中我們封裝了對文件的操作接口IFileProvider和具體實現FileProvider,在控制器中我們是直接實例化,在ASP.NET Core中有了依賴注入,我們可直接借助控制器構造函數注入接口。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<IFileProvider, FileProvider>();
}
當然前提是新建一個ASP.NET Core Web應用程序,然后我們新建一個DownLoadController控制器。在ASP.NET WebAPi中對於請求-響應機制對象是HttpRequestMessage和HttpResponseMessage,而在ASP.NET Core則是HttpRequest和HttpResponse對象。那么我們在控制器中如何獲取這兩個對象中呢?如果我們稍微有點經驗的話就能明了請求和響應對象必然存儲在上下文中,那么我們又如何獲取上下文呢?通過IHttpContextAccessor接口獲取。所以在控制器構造函數中獲取文件接口和上下文以及對應的常量如下:
private const int BufferSize = 80 * 1024;
private const string MimeType = "application/octet-stream";
public IFileProvider _fileProvider { get; set; }
private IHttpContextAccessor _contextAccessor;
private HttpContext _context { get { return _contextAccessor.HttpContext; } }
public FileDownloadController(
IFileProvider fileProvider,
IHttpContextAccessor contextAccessor)
{
_fileProvider = fileProvider;
_contextAccessor = contextAccessor;
}
在ASP.NET WebAPi中獲取請求頭Range利用請求中的Headers屬性獲取,但在ASP.NET Core中則需要通過請求中的GetTypedHeaders()方法獲取。ASP.NET Core對於請求頭中參數的獲取和值的設置更加友好,比如我們要獲取請求頭中的請求類型,在ASP.NET WebAPi中我們指定字符串Request.Headers["Content-Type"],而在ASP.NET Core中則對應的是Request.Headers[HeaderNames.ContentType],直接通過枚舉指定,如此一來則省事多了。最終在DownLoadController控制器中對於在ASP.NET Core中斷點續傳的整個邏輯如下:
public class FileDownloadController
{
private const int BufferSize = 80 * 1024;
private const string MimeType = "application/octet-stream";
public IFileProvider _fileProvider { get; set; }
private IHttpContextAccessor _contextAccessor;
private HttpContext _context { get { return _contextAccessor.HttpContext; } }
public FileDownloadController(
IFileProvider fileProvider,
IHttpContextAccessor contextAccessor)
{
_fileProvider = fileProvider;
_contextAccessor = contextAccessor;
}
/// <summary>
/// 下載文件
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
[HttpGet("api/download")]
public IActionResult GetFile(string fileName)
{
fileName = "cn_windows_8_1_x64_dvd_2707237.iso";
if (!_fileProvider.Exists(fileName))
{
return new StatusCodeResult(StatusCodes.Status404NotFound);
}
//獲取下載文件長度
var fileLength = _fileProvider.GetLength(fileName);
//初始化下載文件信息
var fileInfo = GetFileInfoFromRequest(_context.Request, fileLength);
//獲取剩余部分文件流
var stream = new PartialContentFileStream(_fileProvider.Open(fileName),
fileInfo.From, fileInfo.To);
//設置響應 請求頭
SetResponseHeaders(_context.Response, fileInfo, fileLength, fileName);
return new FileStreamResult(stream, new MediaTypeHeaderValue(MimeType));
}
/// <summary>
/// 根據請求信息賦予封裝的文件信息類
/// </summary>
/// <param name="request"></param>
/// <param name="entityLength"></param>
/// <returns></returns>
private FileInfo GetFileInfoFromRequest(HttpRequest request, long entityLength)
{
var fileInfo = new FileInfo
{
From = 0,
To = entityLength - 1,
IsPartial = false,
Length = entityLength
};
var requestHeaders = request.GetTypedHeaders();
if (requestHeaders.Range != null && requestHeaders.Range.Ranges.Count > 0)
{
var range = requestHeaders.Range.Ranges.FirstOrDefault();
if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entityLength - 1)
{
return null;
}
var start = range.From;
var end = range.To;
if (start.HasValue)
{
if (start.Value >= entityLength)
{
return null;
}
if (!end.HasValue || end.Value >= entityLength)
{
end = entityLength - 1;
}
}
else
{
if (end.Value == 0)
{
return null;
}
var bytes = Math.Min(end.Value, entityLength);
start = entityLength - bytes;
end = start + bytes - 1;
}
fileInfo.IsPartial = true;
fileInfo.Length = end.Value - start.Value + 1;
}
return fileInfo;
}
/// <summary>
/// 設置響應頭信息
/// </summary>
/// <param name="response"></param>
/// <param name="fileInfo"></param>
/// <param name="fileLength"></param>
/// <param name="fileName"></param>
private void SetResponseHeaders(HttpResponse response, FileInfo fileInfo,
long fileLength, string fileName)
{
response.Headers[HeaderNames.AcceptRanges] = "bytes";
response.StatusCode = fileInfo.IsPartial ? StatusCodes.Status206PartialContent
: StatusCodes.Status200OK;
var contentDisposition = new ContentDispositionHeaderValue("attachment");
contentDisposition.SetHttpFileName(fileName);
response.Headers[HeaderNames.ContentDisposition] = contentDisposition.ToString();
response.Headers[HeaderNames.ContentType] = MimeType;
response.Headers[HeaderNames.ContentLength] = fileInfo.Length.ToString();
if (fileInfo.IsPartial)
{
response.Headers[HeaderNames.ContentRange] = new ContentRangeHeaderValue(fileInfo.From, fileInfo.To, fileLength).ToString();
}
}
}
總結
ASP.NET Core中FileResult、FileStreamResult、FilePhsicalResult都已支持斷點續傳,如果對於很小的文件直接下載即可,如果稍微大一點文件則可利用斷點續傳即可,如果對於非常大的文件則需要自定義流來下載這樣更高效,比如對於獲取視頻流文件。上述我們依然是采取自定義流的形式來實現斷點續傳,若對其中封裝的自定義流和接口有疑惑請移步右上角我的github參看ASP.NET WebAPi具體實現。無論是ASP.NET WebAPi和ASP.NET Core斷點續傳都實現了核心邏輯,對於一些細節未考慮其中,希望對想學習斷點續傳的您有所幫助,祝您閱讀愉快,新年快樂!完整代碼,我會上傳到github!