Bagaimana cara menyalin konten dari satu aliran ke yang lain?

521

Apa cara terbaik untuk menyalin konten dari satu aliran ke yang lain? Apakah ada metode utilitas standar untuk ini?

Anton
sumber
Mungkin yang lebih penting pada titik ini, bagaimana Anda menyalin konten "secara streaming", artinya hanya menyalin aliran sumber ketika sesuatu mengkonsumsi aliran tujuan ...?
drzaus

Jawaban:

694

Dari .NET 4.5 pada, ada Stream.CopyToAsyncmetode

input.CopyToAsync(output);

Ini akan mengembalikan Taskyang dapat dilanjutkan saat selesai, seperti:

await input.CopyToAsync(output)

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

Perhatikan bahwa tergantung pada tempat panggilan CopyToAsyncdibuat, kode yang mengikuti mungkin atau tidak dapat melanjutkan pada utas yang sama yang memanggilnya.

Yang SynchronizationContextditangkap saat memanggil awaitakan menentukan utas kelanjutan yang akan dieksekusi.

Selain itu, panggilan ini (dan ini adalah detail implementasi yang dapat berubah) masih berurutan membaca dan menulis (hanya saja tidak membuang utas yang memblokir penyelesaian I / O).

Dari .NET 4.0 pada, ada Stream.CopyTometode

input.CopyTo(output);

Untuk .NET 3.5 dan sebelumnya

Tidak ada yang dimasukkan ke dalam kerangka kerja untuk membantu dengan ini; Anda harus menyalin konten secara manual, seperti:

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

Catatan 1: Metode ini akan memungkinkan Anda untuk melaporkan kemajuan (baca x byte sejauh ini ...)
Catatan 2: Mengapa menggunakan ukuran buffer tetap dan tidak input.Length? Karena Panjang itu mungkin tidak tersedia! Dari dokumen :

Jika kelas yang berasal dari Stream tidak mendukung pencarian, panggilan ke Panjang, SetLength, Posisi, dan Cari melempar NotSupportedException.

Nick
sumber
58
Perhatikan bahwa ini bukan cara tercepat untuk melakukannya. Dalam cuplikan kode yang disediakan, Anda harus menunggu Tulis selesai sebelum blok baru dibaca. Ketika melakukan Baca dan Tulis secara tidak sinkron, penantian ini akan hilang. Dalam beberapa situasi ini akan membuat salinan dua kali lebih cepat. Namun itu akan membuat kode jauh lebih rumit sehingga jika kecepatan tidak menjadi masalah, buatlah tetap sederhana dan gunakan loop sederhana ini. Pertanyaan tentang StackOverflow ini memiliki beberapa kode yang menggambarkan async Baca / Tulis: stackoverflow.com/questions/1540658/... Salam, Sebastiaan
Sebastiaan M
16
FWIW, dalam pengujian saya, saya menemukan bahwa 4096 sebenarnya lebih cepat dari 32 ribu. Ada hubungannya dengan bagaimana CLR mengalokasikan potongan di atas ukuran tertentu. Karena itu, implementasi .NET dari Stream.CopyTo tampaknya menggunakan 4096.
Jeff
1
Jika Anda ingin tahu bagaimana CopyToAsync diimplementasikan atau membuat modifikasi seperti yang saya lakukan (saya harus dapat menentukan jumlah maksimum byte yang akan disalin) maka tersedia sebagai CopyStreamToStreamAsync di "Sampel untuk Pemrograman Paralel dengan .NET Framework" code.msdn .microsoft.com / ParExtSamples
Michael
1
FIY, ukuran buffer optimal 81920bytes, bukan32768
Alex Zhukovskiy
2
@ Jeff referecnceSource terbaru menunjukkan bahwa itu benar-benar menggunakan 81.920 byte penyangga.
Alex Zhukovskiy
66

MemoryStream memiliki .WriteTo (outstream);

dan .NET 4.0 memiliki .CopyTo pada objek stream normal.

.NET 4.0:

instream.CopyTo(outstream);
Joshua
sumber
Saya tidak melihat banyak sampel di web menggunakan metode ini. Apakah ini karena mereka cukup baru atau ada beberapa batasan?
GeneS
3
Itu karena mereka baru di. NET 4.0. Stream.CopyTo () pada dasarnya melakukan hal yang persis sama untuk loop yang jawaban yang disetujui lakukan, dengan beberapa pemeriksaan kewarasan tambahan. Ukuran buffer default adalah 4096, tetapi ada juga kelebihan untuk menentukan yang lebih besar.
Michael Edenfield
9
Aliran perlu diputar ulang setelah salinan: instream.Position = 0;
Draykos
6
Selain memundurkan aliran input, saya juga menemukan kebutuhan untuk memundurkan arus keluaran: outstream.Position = 0;
JonH
32

Saya menggunakan metode ekstensi berikut. Mereka telah mengoptimalkan kelebihan saat ketika satu aliran adalah 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
sumber
1

Pertanyaan dasar yang membedakan implementasi "CopyStream" adalah:

  • ukuran buffer membaca
  • ukuran tulisan
  • Bisakah kita menggunakan lebih dari satu utas (menulis saat kita membaca).

Jawaban atas pertanyaan-pertanyaan ini menghasilkan implementasi yang sangat berbeda dari CopyStream dan bergantung pada jenis aliran apa yang Anda miliki dan apa yang Anda coba optimalkan. Implementasi "terbaik" bahkan perlu tahu perangkat keras spesifik apa yang dibaca dan ditulis stream.

fryguybob
sumber
1
... atau implementasi terbaik dapat mengalami kelebihan beban untuk memungkinkan Anda menentukan ukuran buffer, ukuran tulis, dan apakah utas diizinkan?
MarkJ
1

Sebenarnya, ada cara yang tidak terlalu berat dalam melakukan copy aliran. Namun perhatikan, ini menunjukkan bahwa Anda dapat menyimpan seluruh file dalam memori. Jangan coba dan gunakan ini jika Anda bekerja dengan file yang masuk ke ratusan megabyte atau lebih, tanpa hati-hati.

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

CATATAN: Mungkin juga ada beberapa masalah tentang data biner dan pengkodean karakter.

SmokingRope
sumber
6
Konstruktor default untuk StreamWriter membuat aliran UTF8 tanpa BOM ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ) sehingga tidak ada bahaya masalah penyandian. Data biner hampir pasti tidak boleh disalin dengan cara ini.
keͣmͮpͥ ͩ
14
orang dapat dengan mudah berpendapat bahwa memuat "seluruh file dalam memori" hampir tidak dianggap "kurang berat".
Seph
saya mendapatkan pengecualian karena ini
ColacX
Ini bukan streaming untuk streaming. reader.ReadToEnd()menempatkan semuanya dalam RAM
Bizhan
1

.NET Framework 4 memperkenalkan metode "CopyTo" baru dari Stream Class of System.IO namespace. Dengan menggunakan metode ini kita dapat menyalin satu aliran ke aliran lain dari kelas aliran yang berbeda.

Ini adalah contoh untuk ini.

    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
sumber
Pengingat: CopyToAsync()dianjurkan menggunakan .
Jari Turkia
0

Sayangnya, tidak ada solusi yang sangat sederhana. Anda dapat mencoba sesuatu seperti itu:

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

Tetapi masalah dengan implementasi yang berbeda dari kelas Stream mungkin berperilaku berbeda jika tidak ada yang dibaca. Aliran yang membaca file dari hard drive lokal mungkin akan diblokir sampai operasi baca telah membaca cukup data dari disk untuk mengisi buffer dan hanya mengembalikan lebih sedikit data jika mencapai akhir file. Di sisi lain, pembacaan aliran dari jaringan mungkin mengembalikan lebih sedikit data meskipun ada lebih banyak data yang tersisa untuk diterima.

Selalu periksa dokumentasi kelas aliran spesifik yang Anda gunakan sebelum menggunakan solusi generik.

Tamas Czinege
sumber
5
Solusi generik akan bekerja di sini - jawaban Nick adalah solusi yang bagus. Ukuran buffer tentu saja merupakan pilihan sewenang-wenang, tetapi 32K terdengar masuk akal. Saya pikir solusi Nick benar untuk tidak menutup aliran - serahkan itu kepada pemilik.
Jon Skeet
0

Mungkin ada cara untuk melakukan ini dengan lebih efisien, tergantung pada jenis aliran yang Anda gunakan. Jika Anda dapat mengonversi satu atau kedua aliran Anda ke MemoryStream, Anda dapat menggunakan metode GetBuffer untuk bekerja secara langsung dengan array byte yang mewakili data Anda. Ini memungkinkan Anda menggunakan metode seperti Array.CopyTo, yang memisahkan semua masalah yang diangkat oleh fryguybob. Anda bisa mempercayai .NET untuk mengetahui cara optimal untuk menyalin data.

Pembuat kode
sumber
0

jika Anda ingin procdure untuk menyalin aliran ke yang lain yang nick diposting baik-baik saja tetapi tidak ada posisi reset, itu harus

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
}

tetapi jika dalam runtime tidak menggunakan prosedur Anda shpuld menggunakan aliran memori

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
sumber
3
Anda tidak boleh mengubah posisi aliran input, karena tidak semua aliran mengizinkan akses acak. Dalam aliran jaringan, misalnya, Anda tidak dapat mengubah posisi, hanya membaca dan / atau menulis.
R. Martinho Fernandes
0

Karena tidak ada jawaban yang membahas cara menyalin secara tidak sinkron dari satu aliran ke aliran lain, berikut adalah pola yang saya berhasil gunakan dalam aplikasi penerusan port untuk menyalin data dari satu aliran jaringan ke aliran yang lain. Tidak ada penanganan pengecualian untuk menekankan pola.

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
sumber
4
Tentunya jika pembacaan kedua selesai sebelum penulisan pertama maka Anda akan menulis lebih dari isi bufferForWrite dari pembacaan pertama, sebelum ditulis.
Peter Jeffery
0

Untuk .NET 3.5 dan sebelum mencoba:

MemoryStream1.WriteTo(MemoryStream2);
ntiago
sumber
Itu hanya bekerja jika Anda berurusan dengan MemoryStreams.
Nyerguds
0

Mudah dan aman - buat streaming baru dari sumber asli:

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