Como copiar o conteúdo de um fluxo para outro?

521

Qual é a melhor maneira de copiar o conteúdo de um fluxo para outro? Existe um método utilitário padrão para isso?

Anton
fonte
Talvez o mais importante neste momento, como você copia o conteúdo "de maneira estrita", o que significa que ele apenas copia o fluxo de origem quando algo consome o fluxo de destino ...?
Drzaus 13/10/2015

Respostas:

694

A partir do .NET 4.5, existe o Stream.CopyToAsyncmétodo

input.CopyToAsync(output);

Isso retornará um Taskque pode ser continuado quando concluído, da seguinte forma:

await input.CopyToAsync(output)

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

Observe que, dependendo de onde a chamada CopyToAsyncé feita, o código a seguir pode ou não continuar no mesmo thread que a chamou.

O SynchronizationContextque foi capturado ao chamar awaitdeterminará em qual thread a continuação será executada.

Além disso, essa chamada (e este é um detalhe da implementação sujeito a alterações) ainda sequencia as leituras e gravações (ela simplesmente não desperdiça um encadeamento de bloqueios na conclusão de E / S).

A partir do .NET 4.0, existe o Stream.CopyTométodo

input.CopyTo(output);

Para .NET 3.5 e anteriores

Não há nada incorporado à estrutura para ajudar nisso; você precisa copiar o conteúdo manualmente, da seguinte forma:

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

Nota 1: Este método permitirá que você relate sobre o progresso (x bytes lidos até agora ...)
Nota 2: Por que usar um tamanho de buffer fixo e não input.Length? Porque esse comprimento pode não estar disponível! Dos documentos :

Se uma classe derivada de Stream não suportar busca, as chamadas Length, SetLength, Position e Seek lançam uma NotSupportedException.

Nick
fonte
58
Observe que essa não é a maneira mais rápida de fazer isso. No trecho de código fornecido, é necessário aguardar a gravação ser concluída antes que um novo bloco seja lido. Ao fazer a leitura e gravação de forma assíncrona, essa espera desaparecerá. Em algumas situações, isso fará a cópia duas vezes mais rápido. No entanto, isso tornará o código muito mais complicado, portanto, se a velocidade não for um problema, mantenha-o simples e use esse loop simples. Esta pergunta sobre StackOverflow tem algum código que ilustra o assíncrono Leitura / Gravação: stackoverflow.com/questions/1540658/... Regards, Sebastiaan
Sebastiaan M
16
FWIW, nos meus testes, descobri que o 4096 é realmente mais rápido que 32K. Algo a ver com a maneira como o CLR aloca pedaços sobre um determinado tamanho. Por esse motivo, a implementação .NET do Stream.CopyTo aparentemente usa 4096. #
Jeff Jeff
1
Se você quiser saber como o CopyToAsync é implementado ou fazer modificações como eu fiz (eu precisava poder especificar o número máximo de bytes a serem copiados), ele estará disponível como CopyStreamToStreamAsync em "Exemplos de programação paralela com o .NET Framework" code.msdn .microsoft.com / ParExtSamples
Michael
1
FIY, tamanho do buffer ideal iz 81920bytes, não32768
Alex Zhukovskiy
2
A referência mais recente do @Jeff mostra que na verdade ele usa um buffer de 81920 bytes.
precisa
66

MemoryStream possui .WriteTo (outstream);

e o .NET 4.0 possui .CopyTo no objeto de fluxo normal.

.NET 4.0:

instream.CopyTo(outstream);
Joshua
fonte
Não vejo muitos exemplos na web usando esses métodos. Isso ocorre porque eles são relativamente novos ou existem algumas limitações?
GeneS
3
É porque eles são novos no .NET 4.0. Stream.CopyTo () basicamente faz exatamente o mesmo loop for da resposta aprovada, com algumas verificações adicionais de integridade. O tamanho padrão do buffer é 4096, mas também há uma sobrecarga para especificar uma maior.
Michael Edenfield
9
O fluxo precisa ser rebobinado após a cópia: instream.Position = 0;
Draykos
6
Além de rebobinar o fluxo de entrada, também achei necessário rebobinar o fluxo de saída: outstream.Position = 0;
27415 JonH
32

Eu uso os seguintes métodos de extensão. Eles otimizaram sobrecargas para quando um fluxo é um 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);
    }
Eloff
fonte
1

As perguntas básicas que diferenciam as implementações do "CopyStream" são:

  • tamanho do buffer de leitura
  • tamanho das gravações
  • Podemos usar mais de um segmento (escrevendo enquanto estamos lendo).

As respostas a essas perguntas resultam em implementações muito diferentes do CopyStream e dependem do tipo de fluxo que você tem e do que está tentando otimizar. A "melhor" implementação precisaria até saber em qual hardware específico os fluxos estavam lendo e gravando.

fryguybob
fonte
1
... ou a melhor implementação pode ter sobrecargas para permitir que você especifique o tamanho do buffer, o tamanho da gravação e se os threads são permitidos?
MarkJ
1

Na verdade, existe uma maneira menos pesada de fazer uma cópia de fluxo. Observe, no entanto, que isso implica que você pode armazenar o arquivo inteiro na memória. Não tente usá-lo se estiver trabalhando com arquivos com centenas de megabytes ou mais, sem cautela.

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

NOTA: Também pode haver alguns problemas relacionados a dados binários e codificações de caracteres.

SmokingRope
fonte
6
O construtor padrão do StreamWriter cria um fluxo UTF8 sem uma lista técnica ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ), para que não haja perigo de problemas de codificação. Os dados binários quase certamente não devem ser copiados dessa maneira.
Kemp ͩ
14
pode-se argumentar facilmente que carregar "todo o arquivo na memória" dificilmente é considerado "menos pesado".
Seph
eu recebo exceção de memória por causa disso
ColacX
Este não é fluxo a fluxo.reader.ReadToEnd()coloca tudo na RAM
Bizhan
1

O .NET Framework 4 apresenta o novo método "CopyTo" do espaço para nome Stream Class of System.IO. Usando esse método, podemos copiar um fluxo para outro fluxo de classe de fluxo diferente.

Aqui está um exemplo disso.

    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/>");
Jayesh Sorathia
fonte
Lembrete: o uso CopyToAsync()é incentivado.
Jari Turkia
0

Infelizmente, não existe uma solução realmente simples. Você pode tentar algo assim:

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

Mas o problema é que essa implementação diferente da classe Stream pode se comportar de maneira diferente se não houver nada para ler. Um fluxo que lê um arquivo de um disco rígido local provavelmente bloqueará até que a operação de leitura tenha lido dados suficientes do disco para preencher o buffer e retornará apenas menos dados se chegar ao final do arquivo. Por outro lado, uma leitura de fluxo da rede pode retornar menos dados, embora ainda haja mais dados a serem recebidos.

Sempre verifique a documentação da classe de fluxo específica que você está usando antes de usar uma solução genérica.

Tamas Czinege
fonte
5
A solução genérica funcionará aqui - a resposta de Nick é ótima. O tamanho do buffer é uma escolha arbitrária, é claro, mas 32K parece razoável. Acho que a solução de Nick está certa em não fechar os fluxos - deixe isso para o proprietário.
Jon Skeet
0

Pode haver uma maneira de fazer isso de forma mais eficiente, dependendo do tipo de fluxo com o qual você está trabalhando. Se você pode converter um ou os dois fluxos em um MemoryStream, pode usar o método GetBuffer para trabalhar diretamente com uma matriz de bytes que representa seus dados. Isso permite que você use métodos como Array.CopyTo, que abstraem todos os problemas levantados pelo fryguybob. Você pode confiar no .NET para saber a maneira ideal de copiar os dados.

Coderer
fonte
0

se você deseja que um procedimento copie um fluxo para outro que nick postou, mas está faltando a redefinição de posição, deve ser

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
}

mas se estiver em tempo de execução não usando um procedimento, você deverá usar o fluxo de memória

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
Kronass
fonte
3
Você não deve alterar a posição do fluxo de entrada, porque nem todos os fluxos permitem acesso aleatório. Em um fluxo de rede, por exemplo, você não pode mudar de posição, apenas ler e / ou escrever.
R. Martinho Fernandes
0

Como nenhuma das respostas abordou uma maneira assíncrona de copiar de um fluxo para outro, eis um padrão que usei com sucesso em um aplicativo de encaminhamento de porta para copiar dados de um fluxo de rede para outro. Falta manipulação de exceção para enfatizar o padrão.

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);
}
mdonatas
fonte
4
Certamente, se a segunda leitura for concluída antes da primeira gravação, você escreverá sobre o conteúdo de bufferForWrite desde a primeira leitura, antes que ela seja gravada.
Peter Jeffery
0

Para o .NET 3.5 e antes da tentativa:

MemoryStream1.WriteTo(MemoryStream2);
ntiago
fonte
Isso só funciona se você estiver lidando com MemoryStreams.
Nyerguds
0

Fácil e seguro - crie um novo fluxo a partir da fonte original:

    MemoryStream source = new MemoryStream(byteArray);
    MemoryStream copy = new MemoryStream(byteArray);
Graham Laight
fonte