实如今 .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

相关文章
相关标签/搜索