在 .net framework 中,要實作下載下傳檔案并顯示進度的話,最簡單的做法是使用 WebClient 類。訂閱 DownloadProgressChanged 事件就行了。
但是很可惜,WebClient 并不包含在 .net standard 當中。在 .net standard 中,要進行 http 網絡請求,我們用得更多的是 HttpClient。另外還要注意的是,UWP 中也有一個 HttpClient,雖然用法差不多,但是命名空間是不一樣的,而且 UWP 的是可以支援擷取下載下傳進度的,這裡就不再細說。
如果要下載下傳檔案,我們會使用到 HttpClient 的 GetByteArrayAsync 這個方法。要實作下載下傳進度,那要怎麼辦呢?俗話說,不行就包一層。這裡我們寫個擴充方法,定義如下:
public static class HttpClientExtensions
{
public static Task<byte[]> GetByteArrayAsync(this HttpClient client, Uri requestUri, IProgress<HttpDownloadProgress> progress, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
其中 HttpDownloadProgress 是我自己定義的結構體(不使用類的原因我下面再說),代碼如下:
public struct HttpDownloadProgress
{
public ulong BytesReceived { get; set; }
public ulong? TotalBytesToReceive { get; set; }
}
BytesReceived 代表已經下載下傳的位元組數,TotalBytesToReceive 代表需要下載下傳的位元組數,因為 http 的響應頭不一定會傳回長度(content-length),是以這裡設定為可空。
由于我們需要從 http 響應頭擷取到 content-length,而 HttpClient 自身的 GetByteArrayAsync 并沒有辦法實作,我們需要轉向使用 GetAsync 這個方法。GetAsync 這個方法有一個重載 https://docs.microsoft.com/zh-cn/dotnet/api/system.net.http.httpclient.getasync#System_Net_Http_HttpClient_GetAsync_System_Uri_System_Net_Http_HttpCompletionOption_System_Threading_CancellationToken_ 它的第二個參數是一個枚舉,代表是什麼時候可以得到 response。按照需求,我們這裡應該使用 HttpCompletionOption.ResponseHeadersRead 這個。
另外 HttpClient 的源碼也可以在 Github 上看得到。https://github.com/dotnet/corefx/blob/d69d441dfb0710c2a34155c7c4745db357b14c96/src/System.Net.Http/src/System/Net/Http/HttpClient.cs 我們可以參考一下 GetByteArrayAsync 的實作。
經過思考,可以寫出下面的代碼:
public static class HttpClientExtensions
{
private const int BufferSize = 8192;
public static async Task<byte[]> GetByteArrayAsync(this HttpClient client, Uri requestUri, IProgress<HttpDownloadProgress> progress, CancellationToken cancellationToken)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
using (var responseMessage = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
{
responseMessage.EnsureSuccessStatusCode();
var content = responseMessage.Content;
if (content == null)
{
return Array.Empty<byte>();
}
var headers = content.Headers;
var contentLength = headers.ContentLength;
using (var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false))
{
var buffer = new byte[BufferSize];
int bytesRead;
var bytes = new List<byte>();
var downloadProgress = new HttpDownloadProgress();
if (contentLength.HasValue)
{
downloadProgress.TotalBytesToReceive = (ulong)contentLength.Value;
}
progress?.Report(downloadProgress);
while ((bytesRead = await responseStream.ReadAsync(buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false)) > 0)
{
bytes.AddRange(buffer.Take(bytesRead));
downloadProgress.BytesReceived += (ulong)bytesRead;
progress?.Report(downloadProgress);
}
return bytes.ToArray();
}
}
}
}
這裡我将緩沖區設定為 8192 位元組(8 KB),相當于每讀取 8 KB 就彙報一次下載下傳進度,當然各位看官也可以把這個值調小,這樣效果會更好,但相對的性能就差一些。同時也因為這裡 Report 的頻率是比較高的,是以 HttpDownloadProgress 不适合用 class(否則 GC 會壓力相當大)。
下面我自己的 Demo 的效果圖: