天天看點

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

實作在 .net 中使用 HttpClient 下載下傳檔案時顯示進度

繼續閱讀