Jak skopiować zawartość jednego strumienia do drugiego?


521

Jaki jest najlepszy sposób na skopiowanie zawartości jednego strumienia do drugiego? Czy istnieje standardowa metoda użyteczności?


Być może, co ważniejsze, w tym momencie, w jaki sposób kopiujesz zawartość „strumieniowo”, co oznacza, że ​​kopiuje ona tylko strumień źródłowy, ponieważ coś zużywa strumień docelowy ...?
drzaus

Odpowiedzi:


694

Począwszy od .NET 4.5 istnieje Stream.CopyToAsyncmetoda

input.CopyToAsync(output);

Zwróci wartość, Taskktórą można kontynuować po zakończeniu, np .:

await input.CopyToAsync(output)

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

Zauważ, że w zależności od tego, gdzie CopyToAsyncjest wykonywane połączenie, następujący kod może, ale nie musi, kontynuować w tym samym wątku, który go wywołał.

To, SynchronizationContextco zostało przechwycone podczas wywoływania await, określi, w jakim wątku będzie kontynuowana kontynuacja.

Ponadto to wywołanie (i jest to szczegół implementacji podlegający zmianom) nadal sekwencyjnie odczytuje i zapisuje (nie marnuje nic blokowania wątków po zakończeniu operacji we / wy).

Począwszy od .NET 4.0 istnieje Stream.CopyTometoda

input.CopyTo(output);

Dla .NET 3.5 i wcześniejszych

Nie ma w tym nic upieczonego, aby pomóc w tym; musisz skopiować zawartość ręcznie, tak jak:

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

Uwaga 1: Ta metoda pozwoli Ci raportować postęp (dotychczas odczytane bajty x ...)
Uwaga 2: Dlaczego warto używać stałego rozmiaru bufora, a nie input.Length? Ponieważ ta długość może być niedostępna! Z dokumentów :

Jeśli klasa wywodząca się ze strumienia nie obsługuje wyszukiwania, wywołania Length, SetLength, Position i Seek generują NotSupportedException.


58
Pamiętaj, że nie jest to najszybszy sposób na zrobienie tego. W dostarczonym fragmencie kodu musisz poczekać na zakończenie zapisu przed odczytaniem nowego bloku. Podczas asynchronicznego odczytu i zapisu to oczekiwanie zniknie. W niektórych sytuacjach spowoduje to, że kopia będzie dwa razy szybsza. Sprawi to jednak, że kod będzie znacznie bardziej skomplikowany, więc jeśli prędkość nie stanowi problemu, zachowaj prostotę i skorzystaj z tej prostej pętli. To pytanie na StackOverflow zawiera kod ilustrujący asynchroniczny odczyt / zapis: stackoverflow.com/questions/1540658/... Pozdrawiam, Sebastiaan
Sebastiaan M

16
FWIW, w moich testach stwierdziłem, że 4096 jest w rzeczywistości szybszy niż 32 KB. Ma to coś wspólnego z tym, jak CLR przydziela porcje o określonym rozmiarze. Z tego powodu implementacja .NET Stream.CopyTo .NET najwyraźniej używa 4096.
Jeff

1
Jeśli chcesz wiedzieć, jak zaimplementowano CopyToAsync lub dokonać modyfikacji tak jak ja (musiałem mieć możliwość określenia maksymalnej liczby bajtów do skopiowania), jest on dostępny jako CopyStreamToStreamAsync w „Próbkach do programowania równoległego z .NET Framework” code.msdn .microsoft.com / ParExtSamples
Michael

1
FIY, optymalny rozmiar bufora to 81920bajty, nie32768
Alex Zhukovskiy

2
@Jeff najnowsze referencje Źródło pokazuje, że faktycznie używa bufora 81920 bajtów.
Alex Zhukovskiy

66

MemoryStream ma .WriteTo (outstream);

a .NET 4.0 ma .CopyTo na normalnym obiekcie strumieniowym.

.NET 4.0:

instream.CopyTo(outstream);

Nie widzę wielu próbek w Internecie za pomocą tych metod. Czy to dlatego, że są dość nowe, czy są jakieś ograniczenia?
GeneS

3
To dlatego, że są nowe w .NET 4.0. Stream.CopyTo () zasadniczo robi dokładnie to samo dla pętli, co zatwierdzona odpowiedź, z kilkoma dodatkowymi sprawdzeniami poprawności. Domyślny rozmiar bufora to 4096, ale istnieje również przeciążenie, aby określić większy.
Michael Edenfield

9
Strumień należy przewinąć do tyłu po kopiowaniu: instream.Position = 0;
Draykos

6
Oprócz przewijania strumienia wejściowego zauważyłem także potrzebę przewijania strumienia wyjściowego: outstream.Position = 0;
JonH

32

Używam następujących metod rozszerzenia. Mają zoptymalizowane przeciążenia, gdy jeden strumień jest 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

Podstawowe pytania odróżniające implementacje „CopyStream” to:

  • rozmiar bufora odczytu
  • rozmiar pisze
  • Czy możemy użyć więcej niż jednego wątku (pisanie podczas czytania)?

Odpowiedzi na te pytania prowadzą do bardzo różnych implementacji CopyStream i zależą od rodzaju posiadanych strumieni i tego, co próbujesz zoptymalizować. „Najlepsza” implementacja musiałaby nawet wiedzieć, na jakim konkretnym sprzęcie czytały i zapisywały strumienie.


1
... lub najlepsza implementacja może mieć przeciążenia, które pozwalają określić rozmiar bufora, rozmiar zapisu i czy dozwolone są wątki?
MarkJ

1

W rzeczywistości istnieje mniej wymagający sposób wykonywania kopii strumieniowej. Pamiętaj jednak, że oznacza to, że możesz zapisać cały plik w pamięci. Nie próbuj tego używać, jeśli pracujesz z plikami, które przechodzą do setek megabajtów lub więcej, bez zachowania ostrożności.

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

UWAGA: Mogą również występować pewne problemy dotyczące danych binarnych i kodowania znaków.


6
Domyślny konstruktor StreamWriter tworzy strumień UTF8 bez BOM ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ), więc nie ma niebezpieczeństwa problemów z kodowaniem. Dane binarne prawie na pewno nie powinny być kopiowane w ten sposób.
kͩeͣmͮpͥ ͩ

14
można łatwo argumentować, że ładowanie „całego pliku do pamięci” nie jest uważane za „mniej obciążające”.
Seph

dostaję wyjątek pamięci z tego powodu
ColacX

To nie jest strumień po strumieniu. reader.ReadToEnd()umieszcza wszystko w pamięci RAM
Bizhan

1

.NET Framework 4 wprowadza nową metodę „CopyTo” w klasie strumieni przestrzeni nazw System.IO. Za pomocą tej metody możemy skopiować jeden strumień do innego strumienia innej klasy strumienia.

Oto przykład tego.

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

Przypomnienie: korzystanie CopyToAsync()jest zalecane.
Jari Turkia

0

Niestety nie ma naprawdę prostego rozwiązania. Możesz spróbować czegoś takiego:

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

Ale problem z tym, że inna implementacja klasy Stream może zachowywać się inaczej, jeśli nie ma nic do czytania. Strumień odczytujący plik z lokalnego dysku twardego prawdopodobnie zablokuje się, dopóki operacja odczytu nie odczyta wystarczającej ilości danych z dysku, aby wypełnić bufor i zwróci mniej danych, jeśli dotrze do końca pliku. Z drugiej strony odczyt strumienia z sieci może zwrócić mniej danych, nawet jeśli pozostało więcej danych do odebrania.

Zawsze sprawdzaj dokumentację konkretnej klasy strumienia, której używasz przed użyciem ogólnego rozwiązania.


5
Ogólne rozwiązanie będzie działać tutaj - odpowiedź Nicka jest dobra. Rozmiar bufora jest oczywiście arbitralnym wyborem, ale 32K brzmi rozsądnie. Myślę, że rozwiązaniem Nicka jest nie zamykać strumieni - pozostaw to właścicielowi.
Jon Skeet

0

Może istnieć sposób, aby to zrobić bardziej efektywnie, w zależności od rodzaju strumienia, z którym pracujesz. Jeśli możesz przekonwertować jeden lub oba strumienie na MemoryStream, możesz użyć metody GetBuffer do bezpośredniej pracy z tablicą bajtów reprezentującą twoje dane. Pozwala to na użycie metod takich jak Array.CopyTo, które usuwają wszystkie problemy podniesione przez fryguybob. Możesz po prostu zaufać platformie .NET, która zna optymalny sposób kopiowania danych.


0

jeśli chcesz, aby procedura skopiowała strumień na inny, który Nick opublikował, jest w porządku, ale brakuje mu resetowania pozycji, powinno być

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
}

ale jeśli jest w środowisku wykonawczym i nie korzysta z procedury, należy użyć strumienia pamięci

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
Nie należy zmieniać pozycji strumienia wejściowego, ponieważ nie wszystkie strumienie umożliwiają losowy dostęp. Na przykład w strumieniu sieciowym nie można zmieniać pozycji, tylko czytać i / lub pisać.
R. Martinho Fernandes

0

Ponieważ żadna z odpowiedzi nie obejmowała asynchronicznego sposobu kopiowania z jednego strumienia do drugiego, oto wzór, który z powodzeniem wykorzystałem w aplikacji przekierowującej porty do kopiowania danych z jednego strumienia sieciowego do drugiego. Brakuje obsługi wyjątków, aby podkreślić wzór.

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
Z pewnością, jeśli drugi odczyt zakończy się przed pierwszym zapisem, wówczas nadpisujesz zawartość buforaForWrite z pierwszego odczytu, zanim zostanie on zapisany.
Peter Jeffery,

0

W przypadku .NET 3.5 i przed wypróbowaniem:

MemoryStream1.WriteTo(MemoryStream2);

Działa to tylko wtedy, gdy masz do czynienia z MemoryStreams.
Nyerguds,

0

Łatwe i bezpieczne - utwórz nowy strumień z oryginalnego źródła:

    MemoryStream source = new MemoryStream(byteArray);
    MemoryStream copy = new MemoryStream(byteArray);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.