Comment copier le contenu d'un flux dans un autre?

521

Quelle est la meilleure façon de copier le contenu d'un flux vers un autre? Existe-t-il une méthode d'utilité standard pour cela?

Anton
la source
Peut-être plus important encore à ce stade, comment copiez-vous le contenu "en continu", ce qui signifie qu'il ne copie le flux source que lorsque quelque chose consomme le flux de destination ...?
drzaus

Réponses:

694

À partir de .NET 4.5, il existe la Stream.CopyToAsyncméthode

input.CopyToAsync(output);

Cela renverra un Taskqui peut être poursuivi une fois terminé, comme ceci:

await input.CopyToAsync(output)

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

Notez que selon l'endroit où l'appel CopyToAsyncest effectué, le code qui suit peut ou non continuer sur le même thread qui l'a appelé.

Celui SynchronizationContextqui a été capturé lors de l'appel awaitdéterminera sur quel thread la continuation sera exécutée.

De plus, cet appel (et il s'agit d'un détail d'implémentation susceptible d'être modifié) encode toujours les lectures et les écritures (il ne gaspille tout simplement pas un blocage des threads à la fin des E / S).

À partir de .NET 4.0, il y a la Stream.CopyTométhode

input.CopyTo(output);

Pour .NET 3.5 et versions antérieures

Il n'y a rien de prévu dans le cadre pour aider à cela; vous devez copier le contenu manuellement, comme ceci:

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

Note 1: Cette méthode vous permettra de rendre compte de l'avancement (x octets lus jusqu'à présent ...)
Note 2: Pourquoi utiliser une taille de tampon fixe et non input.Length? Parce que cette longueur peut ne pas être disponible! De la documentation :

Si une classe dérivée de Stream ne prend pas en charge la recherche, les appels à Length, SetLength, Position et Seek lèvent une exception NotSupportedException.

Nick
la source
58
Notez que ce n'est pas le moyen le plus rapide de le faire. Dans l'extrait de code fourni, vous devez attendre la fin de l'écriture avant de lire un nouveau bloc. Lorsque vous effectuez la lecture et l'écriture de manière asynchrone, cette attente disparaîtra. Dans certaines situations, la copie sera deux fois plus rapide. Cependant, cela rendra le code beaucoup plus compliqué, donc si la vitesse n'est pas un problème, restez simple et utilisez cette boucle simple. Cette question sur StackOverflow contient du code qui illustre la lecture / écriture asynchrone: stackoverflow.com/questions/1540658/… Cordialement, Sebastiaan
Sebastiaan M
16
FWIW, dans mes tests, j'ai trouvé que 4096 est en fait plus rapide que 32K. Quelque chose à voir avec la façon dont le CLR alloue des morceaux sur une certaine taille. Pour cette raison, l'implémentation .NET de Stream.CopyTo utilise apparemment 4096.
Jeff
1
Si vous voulez savoir comment CopyToAsync est implémenté ou apporter des modifications comme je l'ai fait (je devais être en mesure de spécifier le nombre maximal d'octets à copier), il est disponible en tant que CopyStreamToStreamAsync dans "Samples for Parallel Programming with the .NET Framework" code.msdn .microsoft.com / ParExtSamples
Michael
1
FIY, taille de tampon optimale iz 81920octets, pas32768
Alex Zhukovskiy
2
@Jeff le dernier referecnceSource montre qu'il utilise en fait un tampon de 81920 octets.
Alex Zhukovskiy
66

MemoryStream a .WriteTo (outstream);

et .NET 4.0 a .CopyTo sur un objet de flux normal.

.NET 4.0:

instream.CopyTo(outstream);
Joshua
la source
Je ne vois pas beaucoup d'exemples sur le Web utilisant ces méthodes. Est-ce parce qu'ils sont assez récents ou y a-t-il des limites?
GeneS
3
C'est parce qu'ils sont nouveaux dans .NET 4.0. Stream.CopyTo () fait exactement la même chose pour la boucle que la réponse approuvée, avec quelques vérifications supplémentaires. La taille de mémoire tampon par défaut est 4096, mais il existe également une surcharge pour en spécifier une plus grande.
Michael Edenfield
9
Le flux doit être rembobiné après la copie: instream.Position = 0;
Draykos
6
En plus de rembobiner le flux d'entrée, j'ai également trouvé un besoin de rembobiner le flux de sortie: outstream.Position = 0;
JonH
32

J'utilise les méthodes d'extension suivantes. Ils ont optimisé les surcharges lorsqu'un flux est 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);
    }
Eloff
la source
1

Les questions de base qui différencient les implémentations de "CopyStream" sont:

  • taille du tampon de lecture
  • taille des écritures
  • Pouvons-nous utiliser plus d'un fil (écrire pendant que nous lisons).

Les réponses à ces questions se traduisent par des implémentations très différentes de CopyStream et dépendent du type de flux dont vous disposez et de ce que vous essayez d'optimiser. La «meilleure» implémentation aurait même besoin de savoir sur quel matériel spécifique les flux lisaient et écrivaient.

fryguybob
la source
1
... ou la meilleure implémentation pourrait avoir des surcharges pour vous permettre de spécifier la taille du tampon, la taille d'écriture et si les threads sont autorisés?
MarkJ
1

Il existe en fait une façon moins lourde de faire une copie de flux. Notez cependant que cela implique que vous pouvez stocker l'intégralité du fichier en mémoire. N'essayez pas de l'utiliser si vous travaillez avec des fichiers de plusieurs centaines de mégaoctets ou plus, sans précaution.

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

REMARQUE: il peut également y avoir des problèmes concernant les données binaires et les encodages de caractères.

SmokingRope
la source
6
Le constructeur par défaut de StreamWriter crée un flux UTF8 sans nomenclature ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ), il n'y a donc aucun danger de problèmes de codage. Les données binaires ne devraient certainement pas être copiées de cette façon.
kͩeͣmͮpͥ
14
on pourrait facilement affirmer que le chargement de «tout le fichier en mémoire» n'est guère considéré comme «moins lourd».
Seph
je reçois une exception de mémoire pour cette raison
ColacX
Ce n'est pas un flux à diffuser. reader.ReadToEnd()met tout en RAM
Bizhan
1

.NET Framework 4 présente la nouvelle méthode "CopyTo" de l'espace de noms Stream Class of System.IO. En utilisant cette méthode, nous pouvons copier un flux vers un autre flux de classe de flux différente.

Voici un exemple pour cela.

    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
la source
Rappel: l'utilisation CopyToAsync()est encouragée.
Jari Turkia
0

Malheureusement, il n'y a pas de solution vraiment simple. Vous pouvez essayer quelque chose comme ça:

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

Mais le problème avec cette implémentation différente de la classe Stream peut se comporter différemment s'il n'y a rien à lire. Un flux lisant un fichier à partir d'un disque dur local se bloquera probablement jusqu'à ce que l'opération de lecture ait lu suffisamment de données du disque pour remplir le tampon et ne renvoie moins de données que si elle atteint la fin du fichier. D'un autre côté, une lecture de flux depuis le réseau peut renvoyer moins de données même s'il reste plus de données à recevoir.

Vérifiez toujours la documentation de la classe de flux spécifique que vous utilisez avant d'utiliser une solution générique.

Tamas Czinege
la source
5
La solution générique fonctionnera ici - la réponse de Nick est bonne. La taille du tampon est un choix arbitraire bien sûr, mais 32K semble raisonnable. Je pense que la solution de Nick est juste de ne pas fermer les flux - laissez cela au propriétaire.
Jon Skeet
0

Il peut y avoir un moyen de le faire plus efficacement, selon le type de flux avec lequel vous travaillez. Si vous pouvez convertir l'un de vos flux ou les deux en un MemoryStream, vous pouvez utiliser la méthode GetBuffer pour travailler directement avec un tableau d'octets représentant vos données. Cela vous permet d'utiliser des méthodes comme Array.CopyTo, qui résument tous les problèmes soulevés par fryguybob. Vous pouvez simplement faire confiance à .NET pour connaître la façon optimale de copier les données.

Coderer
la source
0

si vous voulez qu'un procdure copie un flux vers un autre que celui que nick a posté est correct mais qu'il manque la réinitialisation de position, il devrait être

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
}

mais s'il est en cours d'exécution n'utilisant pas une procédure, vous devez utiliser le flux de mémoire

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
la source
3
Vous ne devez pas modifier la position du flux d'entrée, car tous les flux ne permettent pas un accès aléatoire. Dans un flux réseau, par exemple, vous ne pouvez pas changer de position, seulement lire et / ou écrire.
R. Martinho Fernandes
0

Puisqu'aucune des réponses n'a couvert une manière asynchrone de copier d'un flux à un autre, voici un modèle que j'ai utilisé avec succès dans une application de redirection de port pour copier des données d'un flux réseau à un autre. Il manque une gestion des exceptions pour souligner le motif.

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
la source
4
Certes, si la deuxième lecture se termine avant la première écriture, vous écraserez le contenu de bufferForWrite à partir de la première lecture, avant qu'il ne soit écrit.
Peter Jeffery
0

Pour .NET 3.5 et avant, essayez:

MemoryStream1.WriteTo(MemoryStream2);
ntiago
la source
Cela ne fonctionne que si vous traitez avec MemoryStreams.
Nyerguds
0

Facile et sûr - créez un nouveau flux à partir de la source d'origine:

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