在C#中部分下载并序列化大文件?

4
作为我大学即将要做的一个项目,我需要编写一个客户端从服务器下载媒体文件并将其写入本地磁盘。由于这些文件可能非常大,因此我需要实现分段下载和序列化以避免过度使用内存。
我的解决方案是:
namespace PartialDownloadTester
{
    using System;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Net;
    using System.Text;

    public class DownloadClient
    {
        public static void Main(string[] args)
        {
            var dlc = new DownloadClient(args[0], args[1], args[2]);
            dlc.DownloadAndSaveToDisk();
            Console.ReadLine();
        }

        private WebRequest request;

        // directory of file
        private string dir;

        // full file identifier
        private string filePath;

        public DownloadClient(string uri, string fileName, string fileType)
        {
            this.request = WebRequest.Create(uri);
            this.request.Method = "GET";
            var sb = new StringBuilder();
            sb.Append("C:\\testdata\\DownloadedData\\");
            this.dir = sb.ToString();
            sb.Append(fileName + "." + fileType);
            this.filePath = sb.ToString();
        }

        public void DownloadAndSaveToDisk()
        {
            // make sure directory exists
            this.CreateDir();

            var response = (HttpWebResponse)request.GetResponse();
            Console.WriteLine("Content length: " + response.ContentLength);
            var rStream = response.GetResponseStream();
            int bytesRead = -1;
            do
            {
                var buf = new byte[2048];
                bytesRead = rStream.Read(buf, 0, buf.Length);
                rStream.Flush();
                this.SerializeFileChunk(buf);
            }
            while (bytesRead != 0);
        }

        private void CreateDir()
        {
            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }
        }

        private void SerializeFileChunk(byte[] bytes)
        {
            Contract.Requires(!Object.ReferenceEquals(bytes, null));
            FileStream fs = File.Open(filePath, FileMode.Append);
            fs.Write(bytes, 0, bytes.Length);
            fs.Flush();
            fs.Close();
        }
    }
}

为了测试目的,我使用了以下参数:

"http://itu.dk/people/janv/mufc_abc.jpg" "mufc_abc" "jpg"

然而,图片是不完整的(只有前面10%左右是正确的),即使内容长度打印为63780,这是图像的实际大小。

所以我的问题是:

  1. 这是部分下载和序列化的正确方法,还是有更好/更简单的方法?
  2. 响应流中的全部内容是否存储在客户端内存中?如果是这种情况,我是否需要使用HttpWebRequest.AddRange从服务器部分下载数据以保留客户端的内存?
  3. 为什么序列化失败并且我得到了一张损坏的图片?
  4. 当我使用FileMode.Append时,是否引入了很多开销?(msdn表明此选项“定位到文件的末尾”)

提前感谢。


有没有完整源代码的解决方案? - Kiquenet
3个回答

4

您可以使用WebClient来简化您的代码:

class Program
{
    static void Main()
    {
        DownloadClient("http://itu.dk/people/janv/mufc_abc.jpg", "mufc_abc.jpg");
    }

    public static void DownloadClient(string uri, string fileName)
    {
        using (var client = new WebClient())
        {
            using (var stream = client.OpenRead(uri))
            {
                // work with chunks of 2KB => adjust if necessary
                const int chunkSize = 2048;
                var buffer = new byte[chunkSize];
                using (var output = File.OpenWrite(fileName))
                {
                    int bytesRead;
                    while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        output.Write(buffer, 0, bytesRead);
                    }
                }
            }
        }
    }
}

请注意,我只写入了实际从套接字读取的字节数到输出文件中,而不是整个2KB缓冲区。

2
为什么不直接使用 client.DownloadFile 呢? - L.B
1
@L.B 可能教授希望他们学习和理解流处理。 - Chris Shain
1
也许教授希望他们使用Range头部(在问题中:我需要实现部分下载)。 - L.B
谢谢,我之前不知道WebClient类 - 看起来非常方便。我认为你说的有道理,损坏的图像问题是因为我写入了整个缓冲区。 @L.B:msdn没有说明如何实现这一点。它确实部分下载并写入文件吗? (我不必使用流。我选择使用流,因为这是我到目前为止学习数据传输的方式。) - Janus Varmarken

1

我不确定这是否是问题的根源,但我会像这样更改循环

const int ChunkSize = 2048;
var buf = new byte[ChunkSize];
var rStream = response.GetResponseStream();
do {
    int bytesRead = rStream.Read(buf, 0, ChunkSize);
    if (bytesRead > 0) {
        this.SerializeFileChunk(buf, bytesRead);
    }
} while (bytesRead == ChunkSize);

serialize方法将获得一个附加参数

private void SerializeFileChunk(byte[] bytes, int numBytes)

然后写入正确数量的字节

fs.Write(bytes, 0, numBytes);

更新:

我认为没有必要每次都关闭和重新打开文件。我还会使用using语句,即使发生异常,也会关闭资源。using语句在结束时调用资源的Dispose()方法,这反过来会在文件流的情况下调用Close()using可以应用于所有实现IDisposable的类型。

var buf = new byte[2048];
using (var rStream = response.GetResponseStream()) {
    using (FileStream fs = File.Open(filePath, FileMode.Append)) {
        do {
            bytesRead = rStream.Read(buf, 0, buf.Length);
            fs.Write(bytes, 0, bytesRead);
        } while (...);
    }
}

using语句的作用类似于这样

{
    var rStream = response.GetResponseStream();
    try
    {
        // do some work with rStream here.
    } finally {
        if (rStream != null) {
            rStream.Dispose();
        }
    }
}

1
你是对的;我确实需要将readBytes作为参数传递给SerializeFileChunk,以便我可以修复fs.Write调用,让它在readBytes而不是字节数组的长度上工作。谢谢!(编辑:在引入SerializeFileChunk的第二个参数后,循环并不一定需要您提出的更改) - Janus Varmarken
我添加了一些解释并将字节缓冲区的创建放在循环之前,因为在每次迭代中创建新缓冲区没有任何意义。 - Olivier Jacot-Descombes

0

页面未找到。 - Elshan

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