如何将一个流的内容复制到另一个?


521

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


也许在这一点上更重要的是,您如何“以流方式”复制内容,这意味着它仅在消耗目标流的情况下才复制源流...?
drzaus

Answers:


694

从.NET 4.5开始,有Stream.CopyToAsync方法

input.CopyToAsync(output);

这将返回一个Task可以在完成时继续进行的操作,如下所示:

await input.CopyToAsync(output)

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

请注意,取决于在何处进行调用CopyToAsync,随后的代码可能会或可能不会在调用它的同一线程上继续。

SynchronizationContext调用时被抓获await将决定线程的延续将上执行。

此外,此调用(这是一个可能会更改的实现细节)仍然按顺序进行读写操作(它不会浪费线程阻塞I / O完成)。

从.NET 4.0开始,有Stream.CopyTo一种方法

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?因为该长度可能不可用!从文档

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


58
请注意,这不是最快的方法。在提供的代码段中,您必须等待写入完成才能读取新的块。异步进行读写时,这种等待将消失。在某些情况下,这会使副本速度快两倍。但是,这会使代码复杂得多,因此,如果速度不是问题,请使其保持简单并使用此简单循环。关于StackOverflow的问题有一些说明异步读写的代码:stackoverflow.com/questions/1540658/…关于 塞巴斯蒂安
塞巴斯蒂安M 2009年

16
FWIW,在我的测试中,我发现4096实际上比32K快。与CLR如何在特定大小上分配块有关。因此,Stream.CopyTo的.NET实现显然使用4096。–
Jeff

1
如果您想了解CopyToAsync的实现方式或进行与我一样的修改(我需要指定要复制的最大字节数),则可以在“使用.NET Framework并行编程的示例” code.msdn中将其作为CopyStreamToStreamAsync使用。 .microsoft.com / ParExtSamples-
迈克尔

1
FIY,最佳缓冲区大小iz 81920字节,不是32768
Alex Zhukovskiy

2
@Jeff 最新的referecnceSource显示它实际上使用了81920个字节的缓冲区。
Alex Zhukovskiy

66

MemoryStream具有.WriteTo(outstream);

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

.NET 4.0:

instream.CopyTo(outstream);

使用这些方法,我在网络上看不到很多示例。这是因为它们相当新还是存在一些局限性?
GeneS

3
这是因为它们是.NET 4.0中的新功能。Stream.CopyTo()基本上与批准的答案执行完全相同的for循环,并进行一些其他的完整性检查。默认缓冲区大小为4096,但是也有一个重载来指定更大的缓冲区。
Michael Edenfield

9
复制后需要倒带流:instream.Position = 0;
Draykos 2013年

6
除了倒退输入流外,我还发现需要倒退输出流:outstream.Position = 0;
JonH 2015年

32

我使用以下扩展方法。当一个流是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);
    }

1

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

  • 读取缓冲区的大小
  • 写入的大小
  • 我们可以使用多个线程吗(在阅读时进行写操作)。

这些问题的答案导致CopyStream的实现方式大不相同,并且取决于您拥有哪种流以及您要优化什么。“最佳”实现甚至需要知道流正在读取和写入的特定硬件。


1
...或最佳实现可能有重载以允许您指定缓冲区大小,写大小以及是否允许线程?
MarkJ

1

实际上,有一种不那么费力的方式来进行流复制。但是请注意,这意味着您可以将整个文件存储在内存中。如果要处理的文件大小达到数百兆字节甚至更多,请不要尝试并使用它,而无需小心。

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

注意:可能还有一些有关二进制数据和字符编码的问题。


6
StreamWriter的默认构造函数创建不带BOM的UTF8流(msdn.microsoft.com/en-us/library/fysy0a4b.aspx),因此不存在编码问题的危险。二进制数据几乎肯定不应该以这种方式复制。
坎普ͩ

14
可以轻易地争辩说,加载“整个文件在内存中”几乎不被认为是“不那么费力的”。
Seph 2012年

我因此而获得内存异常
ColacX

不是流到流。reader.ReadToEnd()将所有内容放入RAM
Bizhan

1

.NET Framework 4引入了System.IO命名空间的Stream Class的新“ 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/>");

提醒:CopyToAsync()鼓励使用。
加里·特尔基亚

0

不幸的是,没有真正简单的解决方案。您可以尝试类似的方法:

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类的不同实现的行为可能会有所不同。从本地硬盘驱动器读取文件的流可能会阻塞,直到读取操作已从磁盘读取足够的数据以填充缓冲区,并且仅在到达文件末尾时才返回较少的数据。另一方面,从网络读取的流可能返回较少的数据,即使还有更多的数据要接收。

在使用通用解决方案之前,请始终检查所使用的特定流类的文档。


5
通用解决方案将在这里工作-Nick的答案很好。当然,缓冲区大小是一个任意选择,但是32K听起来很合理。我认为尼克的解决方案是正确的,但是不要关闭流-将其留给所有者。
乔恩·斯基特

0

可能有一种方法可以更有效地执行此操作,具体取决于您使用的是哪种流。如果您可以将一个或两个流转换为MemoryStream,则可以使用GetBuffer方法直接处理代表数据的字节数组。这使您可以使用Array.CopyTo之类的方法,该方法可以抽象化fryguybob引发的所有问题。您可以信任.NET来了解复制数据的最佳方法。


0

如果您希望通过程序将流复制到其他已刻缺的流,但没有位置重置,则应该

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

3
您不应该更改输入流的位置,因为并非所有流都允许随机访问。例如,在网络流中,您不能更改位置,只能读取和/或写入。
R. Martinho Fernandes

0

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

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);
}

4
当然,如果第二次读取在第一次写入之前完成,那么您将在第一次写入之前覆盖第一次读取中的bufferForWrite的内容。
彼得·杰弗里

0

对于.NET 3.5和之前,请尝试:

MemoryStream1.WriteTo(MemoryStream2);

不过,这仅在您处理MemoryStreams时有效。
Nyerguds

0

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

    MemoryStream source = new MemoryStream(byteArray);
    MemoryStream copy = new MemoryStream(byteArray);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.