Come copio il contenuto di uno stream su un altro?


521

Qual è il modo migliore per copiare i contenuti di uno stream su un altro? Esiste un metodo di utilità standard per questo?


Forse ancora più importante a questo punto, come si copiano i contenuti "in modo efficiente", il che significa che copia solo il flusso di origine mentre qualcosa consuma il flusso di destinazione ...?
drzaus,

Risposte:


694

Da .NET 4.5 in poi, c'è il Stream.CopyToAsyncmetodo

input.CopyToAsync(output);

Questo restituirà un Taskche può essere continuato quando completato, in questo modo:

await input.CopyToAsync(output)

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

Si noti che, a seconda di dove CopyToAsyncviene effettuata la chiamata , il codice che segue potrebbe continuare o meno sullo stesso thread che l'ha chiamato.

Quello SynchronizationContextche è stato catturato durante la chiamata awaitdeterminerà su quale thread verrà eseguita la continuazione.

Inoltre, questa chiamata (e questo è un dettaglio dell'implementazione soggetto a modifiche) continua a leggere e scrivere sequenze (semplicemente non spreca un blocco di thread al completamento dell'I / O).

Da .NET 4.0 in poi, c'è il Stream.CopyTometodo

input.CopyTo(output);

Per .NET 3.5 e precedenti

Non c'è nulla di cotto nel framework per aiutare con questo; devi copiare il contenuto manualmente, in questo modo:

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: questo metodo consente di generare rapporti sullo stato di avanzamento (x byte letti finora ...)
Nota 2: Perché utilizzare una dimensione del buffer fissa e non input.Length? Perché quella lunghezza potrebbe non essere disponibile! Dai documenti :

Se una classe derivata da Stream non supporta la ricerca, le chiamate a Length, SetLength, Position e Seek generano NotSupportedException.


58
Nota che questo non è il modo più veloce per farlo. Nello snippet di codice fornito, devi attendere il completamento della scrittura prima di leggere un nuovo blocco. Quando si esegue la lettura e la scrittura in modo asincrono, questa attesa scompare. In alcune situazioni questo renderà la copia due volte più veloce. Tuttavia renderà il codice molto più complicato, quindi se la velocità non è un problema, mantienilo semplice e usa questo semplice loop. Questa domanda su StackOverflow ha del codice che illustra la lettura / scrittura asincrona: stackoverflow.com/questions/1540658/… Saluti, Sebastiaan
Sebastiaan M

16
FWIW, nei miei test ho scoperto che 4096 è in realtà più veloce di 32K. Qualcosa a che fare con il modo in cui il CLR alloca blocchi di una certa dimensione. Per questo motivo, l'implementazione .NET di Stream.CopyTo utilizza apparentemente 4096.
Jeff

1
Se vuoi sapere come viene implementato CopyToAsync o apportare modifiche come ho fatto (avevo bisogno di poter specificare il numero massimo di byte da copiare), allora è disponibile come CopyStreamToStreamAsync in "Esempi per la programmazione parallela con .NET Framework" code.msdn .microsoft.com / ParExtSamples
Michael

1
FIY, dimensione ottimale del buffer iz 81920byte, non32768
Alex Zhukovskiy,

2
@Jeff ultime referecnceSource dimostra che utilizza effettivamente 81920 byte del buffer.
Alex Zhukovskiy,

66

MemoryStream ha .WriteTo (a monte);

e .NET 4.0 ha .CopyTo sul normale oggetto stream.

.NET 4.0:

instream.CopyTo(outstream);

Non vedo molti esempi sul web usando questi metodi. È perché sono abbastanza nuovi o ci sono delle limitazioni?
GeneS,

3
È perché sono nuovi in ​​.NET 4.0. Stream.CopyTo () sostanzialmente fa lo stesso per il loop della risposta approvata, con alcuni controlli di integrità aggiuntivi. La dimensione del buffer predefinita è 4096, ma esiste anche un sovraccarico per specificarne uno più grande.
Michael Edenfield,

9
Lo stream deve essere riavvolto dopo la copia: instream.Position = 0;
Draykos,

6
Oltre a riavvolgere il flusso di input, ho anche trovato la necessità di riavvolgere il flusso di output: outstream.Position = 0;
JonH,

32

Uso i seguenti metodi di estensione. Hanno sovraccarichi ottimizzati per quando un flusso è un 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

Le domande di base che differenziano le implementazioni di "CopyStream" sono:

  • dimensione del buffer di lettura
  • dimensione delle scritture
  • Possiamo usare più di un thread (scrivendo mentre stiamo leggendo).

Le risposte a queste domande comportano implementazioni molto diverse di CopyStream e dipendono dal tipo di stream che hai e da ciò che stai cercando di ottimizzare. L'implementazione "migliore" avrebbe anche bisogno di sapere su quale hardware specifico i flussi stessero leggendo e scrivendo.


1
... o l'implementazione migliore potrebbe presentare sovraccarichi per consentire all'utente di specificare la dimensione del buffer, la dimensione di scrittura e se i thread sono consentiti?
MarkJ,

1

In realtà, esiste un modo meno pesante di fare una copia stream. Si noti tuttavia che ciò implica che è possibile memorizzare l'intero file in memoria. Non provare a usarlo se stai lavorando con file che vanno a centinaia di megabyte o più, senza 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: potrebbero esserci anche alcuni problemi relativi ai dati binari e alla codifica dei caratteri.


6
Il costruttore predefinito per StreamWriter crea un flusso UTF8 senza una distinta base ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ), quindi non c'è pericolo di problemi di codifica. Quasi certamente i dati binari non dovrebbero essere copiati in questo modo.
kͩeͣmͮpͥ ͩ

14
si potrebbe facilmente sostenere che il caricamento di "l'intero file in memoria" non è considerato "meno pesante".
Seph,

ottengo un'eccezione di memoria a causa di ciò
ColacX,

Questo non è streaming per lo streaming. reader.ReadToEnd()mette tutto in RAM
Bizhan,

1

.NET Framework 4 introduce il nuovo metodo "CopyTo" della classe Stream dello spazio dei nomi System.IO. Utilizzando questo metodo possiamo copiare un flusso in un altro flusso di diversa classe di flusso.

Ecco un esempio per questo.

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

Promemoria: l'utilizzo CopyToAsync()è incoraggiato.
Jari Turkia,

0

Sfortunatamente, non esiste una soluzione davvero semplice. Puoi provare qualcosa del genere:

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

Ma il problema è che quella diversa implementazione della classe Stream potrebbe comportarsi diversamente se non c'è nulla da leggere. Un flusso che legge un file da un disco rigido locale probabilmente si bloccherà fino a quando l'operazione di lettura non avrà letto abbastanza dati dal disco per riempire il buffer e restituire meno dati solo se raggiunge la fine del file. D'altra parte, un flusso che legge dalla rete potrebbe restituire meno dati anche se sono rimasti più dati da ricevere.

Controllare sempre la documentazione della classe di flusso specifica che si sta utilizzando prima di utilizzare una soluzione generica.


5
La soluzione generica funzionerà qui: la risposta di Nick è ottima. La dimensione del buffer è ovviamente una scelta arbitraria, ma 32K suona ragionevole. Penso che la soluzione di Nick sia giusta non chiudere i flussi, ma lasciarlo al proprietario.
Jon Skeet,

0

Potrebbe esserci un modo per farlo in modo più efficiente, a seconda del tipo di flusso con cui stai lavorando. Se puoi convertire uno o entrambi i tuoi stream in MemoryStream, puoi utilizzare il metodo GetBuffer per lavorare direttamente con un array di byte che rappresenta i tuoi dati. Ciò ti consente di utilizzare metodi come Array.CopyTo, che astraggono tutti i problemi sollevati da fryguybob. Puoi semplicemente fidarti di .NET per conoscere il modo ottimale di copiare i dati.


0

se vuoi che un procdure copi un flusso su un altro che quello pubblicato da nick va bene ma manca il reset della posizione, dovrebbe essere

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
}

ma se è in fase di runtime che non utilizza una procedura, è necessario utilizzare il flusso di memoria

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
Non è necessario modificare la posizione del flusso di input, poiché non tutti i flussi consentono l'accesso casuale. In un flusso di rete, ad esempio, non è possibile cambiare posizione, solo leggere e / o scrivere.
R. Martinho Fernandes,

0

Poiché nessuna delle risposte ha coperto un modo asincrono di copiare da uno stream a un altro, ecco uno schema che ho usato con successo in un'applicazione port forwarding per copiare dati da uno stream di rete a un altro. Manca la gestione delle eccezioni per enfatizzare il modello.

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
Sicuramente se la seconda lettura si completa prima della prima scrittura, allora si scriverà il contenuto di bufferForWrite dalla prima lettura, prima che sia scritto.
Peter Jeffery,

0

Per .NET 3.5 e prima di provare:

MemoryStream1.WriteTo(MemoryStream2);

Funziona solo se hai a che fare con MemoryStreams.
Nyerguds,

0

Facile e sicuro: crea un nuovo stream dalla fonte originale:

    MemoryStream source = new MemoryStream(byteArray);
    MemoryStream copy = new MemoryStream(byteArray);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.