實如今 .net 中使用 HttpClient 下載文件時顯示進度

在 .net framework 中,要實現下載文件並顯示進度的話,最簡單的作法是使用 WebClient 類。訂閱 DownloadProgressChanged 事件就好了。git

可是很惋惜,WebClient 並不包含在 .net standard 當中。在 .net standard 中,要進行 http 網絡請求,咱們用得更多的是 HttpClient。另外還要注意的是,UWP 中也有一個 HttpClient,雖然用法差很少,可是命名空間是不同的,並且 UWP 的是能夠支持獲取下載進度的,這裏就再也不細說。github

若是要下載文件,咱們會使用到 HttpClient 的 GetByteArrayAsync 這個方法。要實現下載進度,那要怎麼辦呢?俗話說,不行就包一層。這裏咱們寫個擴展方法,定義以下:web

public static class HttpClientExtensions
{
    public static Task<byte[]> GetByteArrayAsync(this HttpClient client, Uri requestUri, IProgress<HttpDownloadProgress> progress, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

其中 HttpDownloadProgress 是我本身定義的結構體(不使用類的緣由我下面再說),代碼以下:c#

public struct HttpDownloadProgress
{
    public ulong BytesReceived { get; set; }

    public ulong? TotalBytesToReceive { get; set; }
}

BytesReceived 表明已經下載的字節數,TotalBytesToReceive 表明須要下載的字節數,由於 http 的響應頭不必定會返回長度(content-length),因此這裏設置爲可空。api

因爲咱們須要從 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 的實現。async

通過思考,能夠寫出下面的代碼:性能

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 會壓力至關大)。this

下面我本身的 Demo 的效果圖:spa

iiiw

相關文章
相關標籤/搜索