ฉันจะคัดลอกเนื้อหาของสตรีมหนึ่งไปยังสตรีมอื่นได้อย่างไร


521

วิธีที่ดีที่สุดในการคัดลอกเนื้อหาของสตรีมหนึ่งไปยังอีกสตรีมคืออะไร? มีวิธียูทิลิตี้มาตรฐานสำหรับสิ่งนี้หรือไม่?


ในตอนนี้อาจมีความสำคัญมากกว่านี้คุณจะคัดลอกเนื้อหา "streamably" ซึ่งหมายความว่าจะคัดลอกเฉพาะสตรีมต้นทางเป็นบางสิ่งที่ใช้กระแสข้อมูลปลายทาง ...
drzaus

คำตอบ:


694

จาก. NET 4.5 บนมีStream.CopyToAsyncวิธีการ

input.CopyToAsync(output);

สิ่งนี้จะคืนค่า a Taskที่สามารถดำเนินการต่อได้เมื่อดำเนินการเสร็จแล้วเช่น:

await input.CopyToAsync(output)

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

โปรดทราบว่าขึ้นอยู่กับตำแหน่งที่โทรไปCopyToAsyncรหัสที่ตามมาอาจจะหรืออาจไม่ดำเนินการกับเธรดเดียวกับที่เรียกว่า

SynchronizationContextที่ถูกจับเมื่อโทรawaitจะตรวจสอบสิ่งที่ด้ายต่อเนื่องจะได้รับการดำเนินการใน

นอกจากนี้การเรียกนี้ (และนี่คือการเปลี่ยนแปลงรายละเอียดการนำไปใช้) ยังคงเป็นลำดับอ่านและเขียน

จาก. NET 4.0 บนมีStream.CopyToวิธีเป็น

input.CopyTo(output);

สำหรับ. NET 3.5 ขึ้นไป

ไม่มีสิ่งใดถูกอบเข้ามาในกรอบเพื่อช่วยในเรื่องนี้ คุณต้องคัดลอกเนื้อหาด้วยตนเองเช่น:

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

หมายเหตุ 1: วิธีนี้จะช่วยให้คุณสามารถรายงานความคืบหน้า (x ไบต์อ่านจนถึง ... )
หมายเหตุ 2: ทำไมต้องใช้ขนาดบัฟเฟอร์คงที่และไม่input.Length? เพราะความยาวนั้นอาจไม่สามารถใช้ได้! จากเอกสาร :

ถ้าคลาสที่ได้รับจากสตรีมไม่รองรับการค้นหาให้โทรไปที่ความยาว, SetLength, ตำแหน่งและค้นหาการโยน NotSupportedException


58
โปรดทราบว่านี้ไม่ได้เป็นวิธีที่เร็วที่สุดที่จะทำมัน ในข้อมูลโค้ดที่ให้ไว้คุณต้องรอให้การเขียนเสร็จสมบูรณ์ก่อนที่จะอ่านบล็อกใหม่ เมื่อทำการอ่านและเขียนแบบอะซิงโครนัสการรอนี้จะหายไป ในบางสถานการณ์การทำสำเนาจะเร็วขึ้นสองเท่า อย่างไรก็ตามมันจะทำให้โค้ดมีความซับซ้อนมากขึ้นดังนั้นหากความเร็วไม่ใช่ปัญหาให้มันง่ายและใช้ลูปแบบง่าย คำถามเกี่ยวกับ StackOverflow มีรหัสบางอย่างที่แสดงถึงการอ่าน / เขียน async: stackoverflow.com/questions/1540658/… ขอแสดงความนับถือ Sebastiaan
Sebastiaan เมื่อ

16
FWIW จากการทดสอบของฉันฉันพบว่า 4096 นั้นเร็วกว่า 32K จริง ๆ สิ่งที่ต้องทำเกี่ยวกับวิธีที่ CLR จัดสรรชิ้นส่วนในขนาดที่กำหนด ด้วยเหตุนี้การใช้. NET ของ Stream.CopyTo เห็นได้ชัดว่าใช้ 4096
Jeff

1
หากคุณต้องการทราบว่ามีการใช้งาน CopyToAsync อย่างไรหรือทำการแก้ไขอย่างที่ฉันทำ (ฉันต้องสามารถระบุจำนวนไบต์สูงสุดที่จะคัดลอกได้) จากนั้นก็จะพร้อมใช้งานเป็น CopyStreamToStreamAsync ใน "Samples for Parallel Programming with. NET Framework" code.msdn .microsoft.com / ParExtSamples
Michael

1
FIY ขนาดบัฟเฟอร์ที่เหมาะสม iz 81920ไบต์ไม่ใช่32768
Alex Zhukovskiy

2
@Jeff referecnceSource ล่าสุดแสดงให้เห็นว่าจริง ๆ แล้วใช้บัฟเฟอร์ 81920 ไบต์
อเล็กซ์ Zhukovskiy

66

MemoryStream มี .WriteTo (outstream);

และ. NET 4.0 มี. คัดลอกไปยังวัตถุกระแสข้อมูลปกติ

.NET 4.0:

instream.CopyTo(outstream);

ฉันไม่เห็นตัวอย่างจำนวนมากบนเว็บโดยใช้วิธีการเหล่านี้ เป็นเพราะพวกเขาค่อนข้างใหม่หรือมีข้อ จำกัด บางอย่าง?
GeneS

3
เป็นเพราะพวกเขายังใหม่ใน. NET 4.0 Stream.CopyTo () โดยทั่วไปแล้วจะทำแบบเดียวกันกับลูปที่คำตอบที่ได้รับการอนุมัติมีการตรวจสุขภาพจิตเพิ่มเติม ขนาดบัฟเฟอร์เริ่มต้นคือ 4096 แต่ยังมีเกินพิกัดเพื่อระบุขนาดที่ใหญ่กว่า
Michael Edenfield

9
สตรีมจำเป็นต้องย้อนกลับหลังการคัดลอก: instream.Position = 0;
Draykos

6
นอกเหนือจากการกรอกลับสตรีมอินพุตแล้วฉันยังพบว่าจำเป็นต้องกรอเอาท์พุทสตรีมย้อนกลับ: outstream.Position = 0;
JonH

32

ฉันใช้วิธีการต่อไปนี้ พวกเขาได้ปรับปรุงการโอเวอร์โหลดเมื่อสตรีมเดียวคือ 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

คำถามพื้นฐานที่แยกความแตกต่างของการใช้งานของ "CopyStream" คือ:

  • ขนาดของบัฟเฟอร์การอ่าน
  • ขนาดของการเขียน
  • เราสามารถใช้มากกว่าหนึ่งเธรด (เขียนในขณะที่เรากำลังอ่าน)

คำตอบสำหรับคำถามเหล่านี้ส่งผลให้เกิดการใช้งานที่แตกต่างกันอย่างมากมายของ CopyStream และขึ้นอยู่กับประเภทของสตรีมที่คุณมีและสิ่งที่คุณพยายามปรับให้เหมาะสม การใช้งาน "ที่ดีที่สุด" จำเป็นต้องรู้ด้วยว่าฮาร์ดแวร์ใดที่สตรีมกำลังอ่านและเขียน


1
... หรือการนำไปปฏิบัติที่ดีที่สุดอาจมีโอเวอร์โหลดเพื่ออนุญาตให้คุณระบุขนาดบัฟเฟอร์ขนาดการเขียนและการอนุญาตเธรดหรือไม่
MarkJ

1

อันที่จริงแล้ววิธีการคัดลอกสตรีมที่ใช้งานหนักน้อยกว่า อย่างไรก็ตามโปรดทราบว่าสิ่งนี้บ่งบอกว่าคุณสามารถเก็บไฟล์ทั้งหมดไว้ในหน่วยความจำได้ อย่าพยายามใช้สิ่งนี้หากคุณทำงานกับไฟล์ที่มีขนาดหลายร้อยเมกะไบต์ขึ้นไปโดยไม่ต้องระมัดระวัง

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

หมายเหตุ: อาจมีปัญหาบางอย่างเกี่ยวกับข้อมูลไบนารีและการเข้ารหัสอักขระ


6
ตัวสร้างเริ่มต้นสำหรับ StreamWriter สร้างกระแส UTF8 โดยไม่มี BOM ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ) ดังนั้นจึงไม่มีอันตรายจากการเข้ารหัสปัญหา ข้อมูลไบนารีไม่ควรคัดลอกด้วยวิธีนี้
kͩeͣmͮpͥͩ

14
ใครสามารถยืนยันได้ว่าการโหลด "ไฟล์ทั้งหมดในหน่วยความจำ" นั้นแทบจะไม่ถือว่าเป็น
Seph

ฉันได้รับข้อยกเว้น outmemory เพราะสิ่งนี้
ColacX

นี่ไม่ใช่สตรีมไปยังสตรีม reader.ReadToEnd()ใส่ทุกอย่างใน RAM
Bizhan

1

.NET Framework 4 แนะนำวิธีการ "CopyTo" ใหม่ของ Stream Class ของ System.IO namespace การใช้วิธีการนี้เราสามารถคัดลอกสตรีมหนึ่งไปยังสตรีมอื่นของคลาสสตรีมอื่น

นี่คือตัวอย่างสำหรับสิ่งนี้

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

คำเตือน: CopyToAsync()สนับสนุนให้ใช้
Jari Turkia

0

น่าเสียดายที่ไม่มีวิธีแก้ปัญหาง่ายๆ คุณสามารถลองทำสิ่งต่อไปนี้:

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

แต่ปัญหาที่เกิดขึ้นกับการใช้งานคลาส Stream ที่แตกต่างกันอาจทำงานแตกต่างกันหากไม่มีอะไรให้อ่าน สตรีมที่อ่านไฟล์จากฮาร์ดไดรฟ์ในเครื่องอาจปิดกั้นจนกว่าตัวเลือกการอ่านจะอ่านข้อมูลเพียงพอจากดิสก์เพื่อเติมบัฟเฟอร์และจะส่งคืนข้อมูลน้อยลงหากถึงจุดสิ้นสุดของไฟล์ ในทางกลับกันสตรีมที่อ่านจากเครือข่ายอาจส่งคืนข้อมูลน้อยลงแม้ว่าจะมีข้อมูลเหลืออีกมากที่ต้องรับ

ตรวจสอบเอกสารประกอบของคลาสสตรีมเฉพาะที่คุณใช้ก่อนใช้โซลูชันทั่วไป


5
วิธีการแก้ปัญหาทั่วไปจะทำงานที่นี่ - คำตอบของนิคเป็นคำตอบที่ดี ขนาดบัฟเฟอร์เป็นตัวเลือกที่แน่นอน แต่ 32K ฟังดูสมเหตุสมผล ฉันคิดว่าทางออกของ Nick นั้นถูกต้องไม่ปิดกระแสข้อมูล - ปล่อยให้เจ้าของ
Jon Skeet

0

อาจมีวิธีการทำสิ่งนี้ได้อย่างมีประสิทธิภาพมากขึ้นทั้งนี้ขึ้นอยู่กับประเภทของสตรีมที่คุณทำงานด้วย หากคุณสามารถแปลงกระแสข้อมูลหนึ่งหรือทั้งสองของคุณเป็น MemoryStream คุณสามารถใช้วิธี GetBuffer เพื่อทำงานโดยตรงกับอาร์เรย์ไบต์ที่แทนข้อมูลของคุณ สิ่งนี้ช่วยให้คุณใช้วิธีต่างๆเช่น Array.CopyTo ซึ่งสรุปประเด็นทั้งหมดที่ fryguybob ให้ไว้ คุณสามารถเชื่อถือ. NET เพื่อทราบวิธีที่ดีที่สุดในการคัดลอกข้อมูล


0

หากคุณต้องการให้ procdure คัดลอกสตรีมไปยังสตรีมอื่นที่ Nick โพสต์นั้นใช้ได้ แต่หายไปจากการรีเซ็ตตำแหน่งมันควรจะเป็น

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
}

แต่ถ้ามันอยู่ในรันไทม์ไม่ได้ใช้โพรซีเดอร์คุณ shpuld ใช้หน่วยความจำสตรีม

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
คุณไม่ควรเปลี่ยนตำแหน่งของอินพุตสตรีมเนื่องจากสตรีมบางรายการไม่อนุญาตให้เข้าถึงแบบสุ่ม ในสตรีมเครือข่ายตัวอย่างเช่นคุณไม่สามารถเปลี่ยนตำแหน่งได้เพียงอ่านและ / หรือเขียน
R. Martinho Fernandes

0

เนื่องจากไม่มีคำตอบใดที่ครอบคลุมวิธีคัดลอกแบบอะซิงโครนัสจากสตรีมหนึ่งไปยังสตรีมอื่นนี่เป็นรูปแบบที่ฉันใช้ในแอปพลิเคชันการส่งต่อพอร์ตเพื่อคัดลอกข้อมูลจากสตรีมเครือข่ายหนึ่งไปยังอีก มันไม่มีข้อยกเว้นในการจัดการเพื่อเน้นรูปแบบ

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
แน่นอนถ้าการอ่านครั้งที่สองเสร็จสิ้นก่อนการเขียนครั้งแรกคุณจะเขียนทับเนื้อหาของ bufferForWrite ตั้งแต่การอ่านครั้งแรกก่อนที่จะถูกเขียนออกมา
Peter Jeffery

0

สำหรับ. NET 3.5 และก่อนลอง:

MemoryStream1.WriteTo(MemoryStream2);

ใช้งานได้เฉพาะในกรณีที่คุณจัดการกับ MemoryStreams เท่านั้น
Nyerguds

0

ง่ายและปลอดภัย - สร้างสตรีมใหม่จากแหล่งดั้งเดิม:

    MemoryStream source = new MemoryStream(byteArray);
    MemoryStream copy = new MemoryStream(byteArray);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.