欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

实现在 .net 中使用 HttpClient 下载文件时显示进度

程序员文章站 2022-03-20 09:37:26
在 .net framework 中,要实现下载文件并显示进度的话,最简单的做法是使用 WebClient 类。订阅 DownloadProgressChanged 事件就行了。 但是很可惜,WebClient 并不包含在 .net standard 当中。在 .net standard 中,要进行 ......

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