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


在 .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 的效果圖:

iiiw


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM