【问题标题】:How do I copy the contents of one stream to another?如何将一个流的内容复制到另一个流?
【发布时间】:2010-09-18 19:27:41
【问题描述】:

将一个流的内容复制到另一个流的最佳方法是什么?有没有标准的实用方法?

【问题讨论】:

  • 也许在这一点上更重要的是,你如何“流式地”复制内容,这意味着它只复制源流,因为某些东西消耗了目标流......?

标签: c# stream copying


【解决方案1】:

从 .NET 4.5 开始,Stream.CopyToAsync method

input.CopyToAsync(output);

这将返回一个Task,完成后可以继续,如下所示:

await input.CopyToAsync(output)

// Code from here on will be run in a continuation.

请注意,根据调用CopyToAsync 的位置,后面的代码可能会或可能不会在调用它的同一线程上继续。

调用await 时捕获的SynchronizationContext 将决定继续在哪个线程上执行。

此外,此调用(这是一个可能会更改的实现细节)仍然对读取和写入进行排序(它只是不会浪费线程在 I/O 完成时阻塞)。

从 .NET 4.0 开始,Stream.CopyTo method

input.CopyTo(output);

适用于 .NET 3.5 及之前的版本

框架中没有任何东西可以帮助解决这个问题;您必须手动复制内容,如下所示:

public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[32768];
    int read;
    while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write (buffer, 0, read);
    }
}

注意 1:此方法将允许您报告进度(到目前为止已读取 x 个字节...)
注2:为什么使用固定缓冲区大小而不是input.Length?因为该长度可能不可用!来自docs

如果派生自 Stream 的类不支持查找,则对 Length、SetLength、Position 和 Seek 的调用将引发 NotSupportedException。

【讨论】:

  • 请注意,这不是最快的方法。在提供的代码 sn-p 中,您必须等待写入完成才能读取新块。当异步执行读取和写入时,这种等待将消失。在某些情况下,这会使复制速度提高一倍。但是它会使代码变得更加复杂,所以如果速度不是问题,保持简单并使用这个简单的循环。 StackOverflow 上的这个问题有一些说明异步读/写的代码:stackoverflow.com/questions/1540658/… Regards,Sebastiaan
  • FWIW,在我的测试中,我发现 4096 实际上比 32K 快。与 CLR 如何分配超过一定大小的块有关。因此,Stream.CopyTo 的 .NET 实现显然使用了 4096。
  • 如果您想知道 CopyToAsync 是如何实现的或像我一样进行修改(我需要能够指定要复制的最大字节数),那么它可以作为 CopyStreamToStreamAsync 在“用于并行编程的示例中使用.NET 框架”code.msdn.microsoft.com/ParExtSamples
  • FIY,最佳缓冲区大小 iz 81920 字节,而不是 32768
  • @Jeff latest referecnceSource 表明它实际上使用了 81920 字节的缓冲区。
【解决方案2】:

不幸的是,没有真正简单的解决方案。你可以试试这样的:

Stream s1, s2;
byte[] buffer = new byte[4096];
int bytesRead = 0;
while (bytesRead = s1.Read(buffer, 0, buffer.Length) > 0) s2.Write(buffer, 0, bytesRead);
s1.Close(); s2.Close();

但是,如果没有可读取的内容,Stream 类的不同实现可能会表现出不同的问题。从本地硬盘读取文件的流可能会阻塞,直到读取操作从磁盘读取足够的数据以填充缓冲区,并且只有在到达文件末尾时才返回较少的数据。另一方面,从网络读取的流可能会返回更少的数据,即使还有更多的数据需要接收。

在使用通用解决方案之前,请务必检查您正在使用的特定流类的文档。

【讨论】:

  • 通用解决方案将在这里工作 - 尼克的回答很好。缓冲区大小当然是任意选择,但 32K 听起来很合理。我认为尼克的解决方案是不关闭流是正确的 - 把它留给所有者。
【解决方案3】:

区分“CopyStream”实现的基本问题是:

  • 读取缓冲区的大小
  • 写入大小
  • 我们可以使用多个线程吗(边读边写)。

这些问题的答案导致 CopyStream 的实现截然不同,并且取决于您拥有什么样的流以及您要优化什么。 “最佳”实现甚至需要知道流正在读取和写入的具体硬件。

【讨论】:

  • ... 或者最好的实现可能有重载,允许您指定缓冲区大小、写入大小以及是否允许线程?
【解决方案4】:

可能有一种方法可以更有效地执行此操作,具体取决于您使用的流类型。如果您可以将一个或两个流转换为 MemoryStream,则可以使用 GetBuffer 方法直接处理表示数据的字节数组。这使您可以使用 Array.CopyTo 之类的方法,这些方法可以抽象掉由 fryguybob 提出的所有问题。您可以相信 .NET 知道复制数据的最佳方式。

【讨论】:

    【解决方案5】:

    如果您想要一个程序将流复制到另一个 nick 发布的流,但它缺少位置重置,它应该是

    public static void CopyStream(Stream input, Stream output)
    {
        byte[] buffer = new byte[32768];
        long TempPos = input.Position;
        while (true)    
        {
            int read = input.Read (buffer, 0, buffer.Length);
            if (read <= 0)
                return;
            output.Write (buffer, 0, read);
        }
        input.Position = TempPos;// or you make Position = 0 to set it at the start
    }
    

    但是如果它在运行时不使用你应该使用内存流的过程

    Stream output = new MemoryStream();
    byte[] buffer = new byte[32768]; // or you specify the size you want of your buffer
    long TempPos = input.Position;
    while (true)    
    {
        int read = input.Read (buffer, 0, buffer.Length);
        if (read <= 0)
            return;
        output.Write (buffer, 0, read);
     }
        input.Position = TempPos;// or you make Position = 0 to set it at the start
    

    【讨论】:

    • 你不应该改变输入流的位置,因为不是所有的流都允许随机访问。例如,在网络流中,您不能更改位置,只能读取和/或写入。
    【解决方案6】:

    实际上,有一种不那么严厉的流复制方式。但是请注意,这意味着您可以将整个文件存储在内存中。如果您正在处理数百兆字节或更多的文件,请不要尝试使用它,不要谨慎。

    public static void CopySmallTextStream(Stream input, Stream output)
    {
      using (StreamReader reader = new StreamReader(input))
      using (StreamWriter writer = new StreamWriter(output))
      {
        writer.Write(reader.ReadToEnd());
      }
    }
    

    注意:也可能存在一些关于二进制数据和字符编码的问题。

    【讨论】:

    • StreamWriter 的默认构造函数创建一个没有 BOM (msdn.microsoft.com/en-us/library/fysy0a4b.aspx) 的 UTF8 流,因此不存在编码问题的危险。二进制数据几乎肯定不应该以这种方式复制。
    • 人们很容易争辩说,加载“内存中的整个文件”很难被认为是“不那么笨拙”。
    • 因为这个我得到了内存溢出异常
    • 这是不是流到流。 reader.ReadToEnd() 将所有内容放入 RAM 中
    • 我将方法从“CopyStream()”重命名为“CopySmallTextStream()”。也许这将有助于使该解决方案的警告在下游代码库中更加明显。
    【解决方案7】:

    我使用以下扩展方法。当一个流是 MemoryStream 时,它们已经优化了重载。

        public static void CopyTo(this Stream src, Stream dest)
        {
            int size = (src.CanSeek) ? Math.Min((int)(src.Length - src.Position), 0x2000) : 0x2000;
            byte[] buffer = new byte[size];
            int n;
            do
            {
                n = src.Read(buffer, 0, buffer.Length);
                dest.Write(buffer, 0, n);
            } while (n != 0);           
        }
    
        public static void CopyTo(this MemoryStream src, Stream dest)
        {
            dest.Write(src.GetBuffer(), (int)src.Position, (int)(src.Length - src.Position));
        }
    
        public static void CopyTo(this Stream src, MemoryStream dest)
        {
            if (src.CanSeek)
            {
                int pos = (int)dest.Position;
                int length = (int)(src.Length - src.Position) + pos;
                dest.SetLength(length); 
    
                while(pos < length)                
                    pos += src.Read(dest.GetBuffer(), pos, length - pos);
            }
            else
                src.CopyTo((Stream)dest);
        }
    

    【讨论】:

      【解决方案8】:

      MemoryStream.WriteTo(outstream);

      .NET 4.0 在普通流对象上有.CopyTo

      .NET 4.0:

      instream.CopyTo(outstream);
      

      【讨论】:

      • 我在网上没有看到很多使用这些方法的示例。这是因为它们是相当新的还是有一些限制?
      • 这是因为它们是 .NET 4.0 中的新功能。 Stream.CopyTo() 基本上执行与批准的答案完全相同的 for 循环,并进行一些额外的完整性检查。默认缓冲区大小为 4096,但也有一个重载来指定更大的。
      • 流复制后需要倒带:instream.Position = 0;
      • 除了回退输入流,我还发现需要回退输出流:outstream.Position = 0;
      【解决方案9】:

      由于没有一个答案涉及从一个流复制到另一个流的异步方式,因此我已经在端口转发应用程序中成功使用了一种模式,将数据从一个网络流复制到另一个网络流。它缺乏强调模式的异常处理。

      const int BUFFER_SIZE = 4096;
      
      static byte[] bufferForRead = new byte[BUFFER_SIZE];
      static byte[] bufferForWrite = new byte[BUFFER_SIZE];
      
      static Stream sourceStream = new MemoryStream();
      static Stream destinationStream = new MemoryStream();
      
      static void Main(string[] args)
      {
          // Initial read from source stream
          sourceStream.BeginRead(bufferForRead, 0, BUFFER_SIZE, BeginReadCallback, null);
      }
      
      private static void BeginReadCallback(IAsyncResult asyncRes)
      {
          // Finish reading from source stream
          int bytesRead = sourceStream.EndRead(asyncRes);
          // Make a copy of the buffer as we'll start another read immediately
          Array.Copy(bufferForRead, 0, bufferForWrite, 0, bytesRead);
          // Write copied buffer to destination stream
          destinationStream.BeginWrite(bufferForWrite, 0, bytesRead, BeginWriteCallback, null);
          // Start the next read (looks like async recursion I guess)
          sourceStream.BeginRead(bufferForRead, 0, BUFFER_SIZE, BeginReadCallback, null);
      }
      
      private static void BeginWriteCallback(IAsyncResult asyncRes)
      {
          // Finish writing to destination stream
          destinationStream.EndWrite(asyncRes);
      }
      

      【讨论】:

      • 当然,如果第二次读取在第一次写入之前完成,那么您将在第一次读取之前写入 bufferForWrite 的内容,然后再写出。
      【解决方案10】:

      .NET Framework 4 引入了 System.IO 命名空间的 Stream 类的新“CopyTo”方法。使用这种方法,我们可以将一个流复制到不同流类的另一个流。

      这是一个例子。

          FileStream objFileStream = File.Open(Server.MapPath("TextFile.txt"), FileMode.Open);
          Response.Write(string.Format("FileStream Content length: {0}", objFileStream.Length.ToString()));
      
          MemoryStream objMemoryStream = new MemoryStream();
      
          // Copy File Stream to Memory Stream using CopyTo method
          objFileStream.CopyTo(objMemoryStream);
          Response.Write("<br/><br/>");
          Response.Write(string.Format("MemoryStream Content length: {0}", objMemoryStream.Length.ToString()));
          Response.Write("<br/><br/>");
      

      【讨论】:

        【解决方案11】:

        对于 .NET 3.5 及之前的尝试:

        MemoryStream1.WriteTo(MemoryStream2);
        

        【讨论】:

        • 只有在处理 MemoryStreams 时才有效。
        【解决方案12】:

        简单安全 - 从原始来源制作新流:

            MemoryStream source = new MemoryStream(byteArray);
            MemoryStream copy = new MemoryStream(byteArray);
        

        【讨论】:

          【解决方案13】:

          以下代码解决了使用 CopyTo 将 Stream 复制到 MemoryStream 的问题

          Stream stream = new MemoryStream();
          

          //任何函数都需要输入流。在 mycase 中将 PDF 文件另存为流 document.Save(流);

          MemoryStream newMs = (MemoryStream)stream;
          
          byte[] getByte = newMs.ToArray();
          

          //注意 - 请将流放在 finally 块中,而不是在 using 块中,因为它会抛出错误 'Access denied as the stream is closed'

          【讨论】:

          • 欢迎来到 Stack Overflow。在添加一个包含 12 个现有答案(包括已接受答案)的 11 岁问题的答案时,指出您的答案所针对的问题的哪些新方面非常重要。请对代码使用代码格式。如果包含对它们的工作方式和原因的解释,答案会更好。
          猜你喜欢
          • 2011-06-22
          • 2011-04-13
          • 2010-09-12
          • 2012-01-16
          • 2011-01-06
          • 1970-01-01
          • 2017-05-27
          • 1970-01-01
          相关资源
          最近更新 更多