當前位置: 妍妍網 > 碼農

Asp.Net Core 實作分片下載的最簡單方式

2024-06-02碼農

技術群裏的朋友遇到了這個問題,起初的原因是他對檔增加了一個內容配置

fileResult.EnableRangeProcessing = true;

這個內容我從未遇到過,然後,去F1檢視這個內容的描述資訊也依然少的可憐,只有簡單的描述為(獲取或設定為 啟用範圍處理 FileResult的值)。

範圍處理,很容易理解,可能就是實作分片下載的關鍵,但是,是不是就很簡單配置就可以了呢,我對此十分感興趣,就開始查詢它的實際資訊。

EnableRangeProcessing 內容的含義

經檢視Asp.net Core 源碼可以看到,它實際上只是配置了一個頭資訊。

具體的方法內部,只是一個賦值

賦值的是什麽呢,其實就是一個HTTP 頭標記

所以,實際上,它只做了一件事情,那就是把請求的頭變成以下的樣子。

是的,它只是做了這一件事情,那就是把請求頭 Accept-Ranges 的值設定為了 bytes 。

HTTP 協定頭之 Accept-Ranges

經查尋,此頭就是表示著 (Accept-Ranges HTTP 響應檔頭是伺服器使用的一個標記,用於向客戶端宣傳其對檔下載的部份請求的支持。此欄位的值表示可用於定義範圍的單位。 )

當存在 Accept-Ranges 檔頭時,瀏覽器可能會嘗試恢復中斷的下載,而不是嘗試重新啟動下載。

所以,設定它就相當於設定了Asp.net Core 支持分片下載。

Asp.net Core 分片下載實際案例

主要分為伺服端和客戶端,伺服端設定允許分片下載,客戶端則需要按照分片進行多執行緒下載,我這裏實際透過並列下載實作。 最後的驗證,能執行它或者開啟它即可(比如zip格式等)。

伺服端程式碼

伺服端異常的簡單,新建一個Asp.net Core 計畫即可,其他都預設。

我們要做的事情只是在 HomeController 裏增加以下程式碼

private FileExtensionContentTypeProvider provider = new FileExtensionContentTypeProvider();
public IActionResult Down()
{
var file = @"H:\百度網盤\ubuntu.zip";
provider.TryGetContentType(file, outvar contentType);
var result = PhysicalFile(file, contentType);
result.EnableRangeProcessing = true;
result.FileDownloadName = Path.GetFileName(file);
return result;
}

裏面有幾個點

  1. 1. 設定檔的型別可以用 FileExtensionContentTypeProvider 這個類來的,不用自己寫(特殊型別它不支持)

  2. 2. 設定EnableRangeProcessing 為 true ,才能實作分片下載

  3. 3. 設定FileDownloadName為具體的下載檔名,才可以在客戶端知道你下載的檔名的名字,很重要。

客戶端程式碼

客戶端稍微復雜一些,得獲取檔的大小和名字,然後,進行多執行緒下載,我這邊會進行一個簡單的模擬。

直接下載檔的邏輯

publicstaticasync Task DownUrl(string url)
{
var result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), HttpCompletionOption.ResponseHeadersRead);
if (result.IsSuccessStatusCode)
{
var filename = "temp.zip";
using (var contentStream = await result.Content.ReadAsStreamAsync())
{
using (var file = File.OpenWrite(filename))
{
await contentStream.CopyToAsync(file);
}
}
}
}

當然,如果不想分片下載,可以直接下載即可。

獲取檔大小以及名字

publicstaticasync Task<(long? length, string filename)> GetFileLengthandName(string url)
{
var result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), HttpCompletionOption.ResponseHeadersRead);
if (result.IsSuccessStatusCode)
{
return (result.Content.Headers.ContentLength, result.Content.Headers.ContentDisposition.FileName);
}
return (nullnull);
}

主要就是透過httpclient 內建的頭資訊,直接獲取即可。挺簡單的。

分片下載核心

首先是對獲取到的檔大小進行一個範圍的分割

publicstruct BytesRange
{
publiclong Start { getset; }
publiclong End { getset; }
publiclong Length { get { return End - Start + 1; } }
publicoverridestringToString()
{
return$"{Start}{End} : {Length}";
}
publicstatic List<BytesRange> GetRanges(long length, long BufferSize = 1 * 1024 * 1024)
{
List<BytesRange> list = new List<BytesRange>();
long count = length / BufferSize;
long Lost = length - BufferSize * count;
if (Lost > 0)
{
list.Add(new BytesRange() { Start = count * BufferSize, End = count * BufferSize + Lost - 1 });
}
if (count > 0)
{
for (long i = 0; i < count; i++)
{
list.Add(new BytesRange() { Start = i * BufferSize, End = (i + 1) * BufferSize - 1 });
}
}
else
{
list.Add(new BytesRange() { Start = 0, End = Lost - 1 });
}
list.OrderByDescending(t => t.Start);
return list;
}
}

這樣就可以獲取具體的分片資訊,具體每片的大小。

publicstaticasync Task<byte[]> GetBytesAsync(string url, BytesRange range)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Range"$"bytes={range.Start}-{range.End}");
using (HttpResponseMessage response = await client.SendAsync(request))
{
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
if (range.Length != stream.Length)
{
thrownew Exception("數據不匹配!");
}
byte[] bytes = newbyte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
return bytes;
}
}
}

GetBytesAsync 就是按照指定的大小分為進行請求,並返回所需的檔大小。

實際程式碼

static HttpClient client = new HttpClient();
staticobject lockObj = newobject();
staticasync Task Main(string[] args)
{
var url = "http://localhost:5034/home/down";
Stopwatch stopwatch = Stopwatch.StartNew();
await DownUrl(url);
stopwatch.Stop();
Console.WriteLine($"單執行緒 直接下載耗時:{stopwatch.Elapsed.TotalSeconds}");
stopwatch.Restart();
(long? length, string filename) = await GetFileLengthandName(url);
if (length.HasValue)
{
var number = 10;
//獲取分片大小,預設1M 緩存區,太小又太慢 設定成5M。
var list = BytesRange.GetRanges(length.Value, 5 * 1024 * 1024);
Console.WriteLine($"分片數:{list.Count} 每片大小:5MB 並行數:{number}");
var path = Path.Combine(AppContext.BaseDirectory, filename);
using (var write = File.OpenWrite(path))
{
write.SetLength(length.Value);
await write.FlushAsync();
// 並列下載,每秒預設10並行
Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = number }, range =>
{
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {range}");
var bytes = GetBytesAsync(url, range).Result;
lock (lockObj)
{
write.Seek(range.Start, SeekOrigin.Begin);
write.Write(bytes);
}
});
}
Console.WriteLine("下載完畢,請驗證!");
}
else
{
Console.WriteLine("沒有獲取到下載檔的資訊!");
}
stopwatch.Stop();
Console.WriteLine($"並行下載 耗時:{stopwatch.Elapsed.TotalSeconds}秒");

Console.ReadLine();
}

驗證結果

先執行伺服端

再執行下載客戶端

看到結果後,有點差異

從結果上來看,直接下載是最快的,應該是少了分片的開銷,而且服務都是在本機上,各種IO的限制基本上只有檔IO,頻寬IO影響最小。

總結

雖然直接下載是最快的,但是,如果網路中斷的話,基本得重新下載,所以,它的風險反而是最高的,而分片下載雖然有了分片的開銷的,但是可以從斷點處繼續下載,風險反而最低,各有優勢。

程式碼地址

https://github.com/kesshei/WebDown.git

https://gitee.com/kesshei/WebDown.git

參考資料地址

【enableRangeProcessing 的程式碼地址】
https://github.com/dotnet/aspnetcore/blob/53db4d97d7c77d13e20e58a98f104e88d6af6040/src/Shared/ResultsHelpers/FileResultHelper.cs#L141
【Accept-Ranges 】
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Ranges

一鍵三連呦!,感謝大佬的支持,您的支持就是我的動力!