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

568

如何最好地将一个流的内容复制到另一个流中?是否有标准的实用方法可以做到这一点?


也许更重要的是,在这一点上,如何“可流式地”复制内容,这意味着它只会在某些东西消耗目标流时才复制源流...? - drzaus
13个回答

741

.NET 4.5以后,有一个名为Stream.CopyToAsync的方法。

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 方法

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:为什么要使用固定的缓冲区大小而不是 ?因为该长度可能无法获得!文档如下:

如果从Stream派生的类不支持寻址,则调用Length、SetLength、Position和Seek会抛出NotSupportedException异常。


60
请注意,这不是最快的方法。在提供的代码片段中,必须等待写入完成后才能读取新块。当异步执行读取和写入操作时,这种等待将消失。在某些情况下,这将使复制速度加快一倍。但是,这将使代码变得更加复杂,因此如果速度不是问题,请保持简单并使用此简单循环。此StackOverflow上的问题包含了一些展示异步读/写的代码:https://dev59.com/8HI_5IYBdhLWcg3wEOvq问候,Sebastiaan - Sebastiaan M
16
就我所测试的结果而言,4096字节实际上比32K字节更快。这与CLR在超出某个特定大小的块时分配内存的方式有关。因此,.NET中的Stream.CopyTo实现显然使用4096字节。 - Jeff
1
如果您想了解CopyToAsync的实现方式或进行修改(就像我所做的那样,我需要能够指定要复制的最大字节数),那么可以在“使用.NET Framework进行并行编程的示例”中找到CopyStreamToStreamAsync。http://code.msdn.microsoft.com/ParExtSamples - Michael
1
顺便提一下,最佳缓冲区大小是 81920 字节,而不是 32768 - Alex Zhukovskiy
2
@Jeff,最新的referenceSource显示它实际上使用81920字节的缓冲区。 - Alex Zhukovskiy
显示剩余7条评论

73

MemoryStream.WriteTo(outstream);

而在 .NET 4.0 中,普通流对象有 .CopyTo 方法。

.NET 4.0:

instream.CopyTo(outstream);

我在网上并没有看到很多使用这些方法的示例。这是因为它们相对较新还是存在一些限制? - GeneS
3
这是因为它们是在.NET 4.0中新增的。Stream.CopyTo()基本上执行了与批准答案完全相同的循环,还增加了一些额外的安全检查。默认缓冲区大小为4096,但也有一种重载方法可以指定更大的缓冲区大小。 - Michael Edenfield
14
复制后需要倒回流: instream.Position = 0; - Draykos
8
除了倒带输入流之外,我还发现需要倒带输出流:outstream.Position = 0; - JonH

33

我使用以下扩展方法。它们针对一个流为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);
    }

3

.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/>");

2

实际上,有一种更轻量级的方式进行流复制。但要注意,这意味着您可以将整个文件存储在内存中。如果您处理的文件超过数百兆字节,请谨慎使用。

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

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


6
StreamWriter的默认构造函数创建一个没有BOM(字节顺序标记)的UTF8流(http://msdn.microsoft.com/en-us/library/fysy0a4b.aspx),因此不存在编码问题的风险。 几乎可以确定不应该以这种方式复制二进制数据。 - kͩeͣmͮpͥ ͩ
15
可以轻松地认为将整个文件加载到内存中并不算是“轻量级”的做法。 - Seph
我因为这个而遇到了内存溢出异常。 - ColacX
2
这不是流对流的操作。reader.ReadToEnd()会将所有内容放入内存中。 - Bizhan
我将方法从'CopyStream()'重命名为'CopySmallTextStream()'。也许这将有助于在下游代码库中更明显地显示此解决方案的注意事项。 - hannasm

1

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

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

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


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

0

对于 .NET 3.5 及之前版本,请尝试:

MemoryStream1.WriteTo(MemoryStream2);

只有在处理MemoryStreams时才有效。 - Nyerguds

0

如果你想要一个将流复制到另一个流的程序,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

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

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

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


6
通用解决方案在这里可行-尼克的答案很好。缓冲区大小当然是任意选择,但32K听起来合理。我认为尼克的解决方案是正确的,不要关闭流,留给所有者去处理。 - Jon Skeet

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的内容,在其被写出之前。 - Peter Jeffery

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