如何在.NET中通过HTTP下载大文件?

24

我需要在C#控制台应用程序中通过HTTP下载一个文件(2GB)。问题在于,在下载了约1.2GB后,应用程序会耗尽内存。

这是我正在使用的代码:

WebClient request = new WebClient();
request.Credentials = new NetworkCredential(username, password);
byte[] fileData = request.DownloadData(baseURL + fName);

正如您所看到的...我直接将文件读入内存。如果我从HTTP中以块的形式读回数据并将其写入磁盘上的文件,我相信我可以解决这个问题。

我该如何做到这一点?

6个回答

39

太棒了!最终成功了。感谢你的帮助! - Nick Cartwright
FYI。对于像WebClient这样没有实现任何接口的类进行单元测试可能是一项挑战。 - Krishter
你能提供一个使用WebClient.DownloadFile直接保存到文件的示例代码吗? - TidyDev

37
WebClient类适用于简单场景。一旦您超过了简单的场景(而您已经做到了),您将不得不稍微退缩并使用WebRequest。

使用WebRequest,您将可以访问响应流,并且您将能够循环读取一点并写入一点,直到完成。

来自Microsoft文档:

我们不建议您在新开发中使用WebRequest或其派生类。相反,请使用System.Net.Http.HttpClient类。

来源:learn.microsoft.com/WebRequest


例子:

public void MyDownloadFile(Uri url, string outputFilePath)
{
    const int BUFFER_SIZE = 16 * 1024;
    using (var outputFileStream = File.Create(outputFilePath, BUFFER_SIZE))
    {
        var req = WebRequest.Create(url);
        using (var response = req.GetResponse())
        {
            using (var responseStream = response.GetResponseStream())
            {
                var buffer = new byte[BUFFER_SIZE];
                int bytesRead;
                do
                {
                    bytesRead = responseStream.Read(buffer, 0, BUFFER_SIZE);
                    outputFileStream.Write(buffer, 0, bytesRead);
                } while (bytesRead > 0);
            }
        }
    }
}

请注意,如果WebClient.DownloadFile正常工作,则我会认为它是最佳解决方案。在"DownloadFile"答案发布之前,我已经写了上面的内容。此外,我写得太早了,因此可能需要一些测试和谨慎考虑。

感谢您详细的回答和代码片段!这将在我想要处理数据随时到达的情况下非常有用! - Nick Cartwright
这段代码中异常处理和重试机制怎么样?网络断开等情况怎么处理? - Zain Shaikh
1
在大多数情况下,最好的异常处理是根本不需要。如果您处于网络非常不可靠的情况下,则可能需要添加重试逻辑。我住在美国,所以我想我被良好的网络连接宠坏了,通常情况下是这样的。当它们不起作用时,情况非常糟糕,重试并不是一个有用的选项。 - John Saunders
我想知道为什么你选择了16 * 1024的缓冲区大小。当我尝试增加大小时,它似乎仍然使用较小的块。你选择这个大小有什么理由吗?只是好奇。 - kns98
没有选择的理由。 - John Saunders
1
可以在这里使用HttpClient而不是WebRequest吗? - mtkachenko

9
你需要获取响应流,然后分块读取并将每个块写入文件中以便重复使用内存。
按照你现在的写法,整个响应,即2GB大小的数据,需要在内存中存储。即使在64位系统上,这也会达到单个.NET对象的2GB限制。
更新:更简单的方法是使用WebClient来完成工作,使用其DownloadFile方法可以直接将数据放入文件中。

3
WebClient.OpenRead返回一个Stream流,使用Read方法循环遍历其中的内容即可,这样数据不会在内存中缓存,而是可以分块写入文件。

2
我会使用类似于这个的东西。

0

连接可能会中断,因此最好分块下载文件。

Akka流可以使用多线程从System.IO.Stream中下载小块文件。 https://getakka.net/articles/intro/what-is-akka.html

Download方法将从长文件开始将字节附加到文件。 如果文件不存在,则fileStart值必须为0。

using Akka.Actor;
using Akka.IO;
using Akka.Streams;
using Akka.Streams.Dsl;
using Akka.Streams.IO;

private static Sink<ByteString, Task<IOResult>> FileSink(string filename)
{
    return Flow.Create<ByteString>()
        .ToMaterialized(FileIO.ToFile(new FileInfo(filename), FileMode.Append), Keep.Right);
}

private async Task Download(string path, Uri uri, long fileStart)
{
    using (var system = ActorSystem.Create("system"))
    using (var materializer = system.Materializer())
    {
       HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
       request.AddRange(fileStart);

       using (WebResponse response = request.GetResponse())
       {
           Stream stream = response.GetResponseStream();

           await StreamConverters.FromInputStream(() => stream, chunkSize: 1024)
               .RunWith(FileSink(path), materializer);
       }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接