Как скопировать содержимое одного потока в другой?

521

Каков наилучший способ скопировать содержимое одного потока в другой? Есть ли для этого стандартный метод утилит?

Антон
источник
Может быть, еще более важно на этом этапе, как вы копируете содержимое «вразумительно», что означает, что он копирует только исходный поток, когда что-то потребляет целевой поток ...?
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: Этот метод позволит вам сообщать о прогрессе (пока прочитано х байтов ...)
Примечание 2: Почему используется фиксированный размер буфера, а не input.Length? Потому что эта длина может быть недоступна! Из документов :

Если класс, производный от Stream, не поддерживает поиск, вызовы Length, SetLength, Position и Seek выдают исключение NotSupportedException.

Ник
источник
58
Обратите внимание, что это не самый быстрый способ сделать это. В приведенном фрагменте кода необходимо дождаться завершения записи, прежде чем будет прочитан новый блок. При асинхронном чтении и записи это ожидание исчезнет. В некоторых ситуациях это сделает копию в два раза быстрее. Однако это сделает код намного более сложным, поэтому, если скорость не является проблемой, сделайте это простым и используйте этот простой цикл. Этот вопрос о StackOverflow имеет некоторый код, который иллюстрирует асинхронное чтение / запись: stackoverflow.com/questions/1540658/… С уважением, Себастьян
Себастьян М
16
FWIW, в моем тестировании я обнаружил, что 4096 на самом деле быстрее, чем 32K. Как-то связано с тем, как CLR распределяет порции по определенному размеру. Из-за этого .NET-реализация Stream.CopyTo, по-видимому, использует 4096.
Джефф
1
Если вы хотите знать, как реализован CopyToAsync, или вносить изменения, как я (мне нужно было указать максимальное количество байт для копирования), тогда он доступен как CopyStreamToStreamAsync в «Образцах для параллельного программирования с .NET Framework» code.msdn. .microsoft.com / ParExtSamples
Майкл,
1
FIY, оптимальный размер буфера из 81920байтов, а не32768
Алексей Жуковский
2
@Jeff последний referecnceSource показывает, что он на самом деле использует буфер 81920 байт.
Алексей Жуковский
66

MemoryStream имеет .WriteTo (outstream);

и .NET 4.0 имеет .CopyTo для обычного объекта потока.

.NET 4.0:

instream.CopyTo(outstream);
Джошуа
источник
Я не вижу много примеров в Интернете с использованием этих методов. Это потому, что они довольно новые или есть некоторые ограничения?
GeneS
3
Это потому, что они являются новыми в .NET 4.0. Stream.CopyTo () в основном делает то же самое для цикла, что и утвержденный ответ, с некоторыми дополнительными проверками работоспособности. Размер буфера по умолчанию - 4096, но есть и перегрузка, чтобы указать больший.
Майкл Иденфилд
9
Поток нужно перематывать после копирования: instream.Position = 0;
Драйкос
6
Помимо перемотки входного потока, я также обнаружил необходимость перематывать выходной поток: outstream.Position = 0;
Джон
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);
    }
Eloff
источник
1

Основные вопросы, которые отличают реализации «CopyStream»:

  • размер буфера чтения
  • размер записи
  • Можем ли мы использовать более одного потока (писать, пока мы читаем).

Ответы на эти вопросы приводят к совершенно разным реализациям CopyStream и зависят от того, какие потоки у вас есть и что вы пытаетесь оптимизировать. «Лучшая» реализация даже должна знать, на каком конкретном оборудовании потоки читают и записывают данные.

fryguybob
источник
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());
  }
}

ПРИМЕЧАНИЕ. Могут также возникнуть некоторые проблемы, касающиеся двоичных данных и кодировки символов.

SmokingRope
источник
6
Конструктор по умолчанию для StreamWriter создает поток UTF8 без спецификации ( msdn.microsoft.com/en-us/library/fysy0a4b.aspx ), поэтому нет опасности возникновения проблем с кодированием. Двоичные данные почти наверняка не должны копироваться таким образом.
kͩeͣmͮpͥ ͩ
14
Можно легко утверждать, что загрузка «всего файла в память» вряд ли считается «менее жесткой».
Seph
я получаю исключение из-за этого
ColacX
Это не поток в поток. reader.ReadToEnd()помещает все в оперативную память
Бижан
1

.NET Framework 4 представляет новый метод «CopyTo» для класса потока пространства имен System.IO. Используя этот метод, мы можем скопировать один поток в другой поток другого класса потока.

Вот пример для этого.

    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()приветствуется.
Яри
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 звучит разумно. Я думаю, что решение Ника - правильно не закрывать потоки - оставьте это владельцу.
Джон Скит
0

Может быть способ сделать это более эффективно, в зависимости от того, с каким потоком вы работаете. Если вы можете преобразовать один или оба ваших потока в MemoryStream, вы можете использовать метод GetBuffer для работы непосредственно с байтовым массивом, представляющим ваши данные. Это позволяет вам использовать такие методы, как Array.CopyTo, которые абстрагируют все проблемы, поднятые fryguybob. Вы можете просто доверять .NET, чтобы знать оптимальный способ копирования данных.

Coderer
источник
0

если вы хотите, чтобы процедура копировала поток в другой, который отправил ник, это нормально, но в нем отсутствует сброс позиции, это должно быть

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
}

но если он во время выполнения не использует процедуру, вы должны использовать поток памяти

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
источник
3
Вы не должны изменять позицию входного потока, потому что не все потоки допускают произвольный доступ. Например, в сетевом потоке вы не можете изменить положение, только чтение и / или запись.
Р. Мартиньо Фернандес
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);
}
mdonatas
источник
4
Конечно, если второе чтение завершается перед первой записью, то вы будете перезаписывать содержимое bufferForWrite с первого чтения, прежде чем оно будет записано.
Питер Джеффри
0

Для .NET 3.5 и до этого попробуйте:

MemoryStream1.WriteTo(MemoryStream2);
ntiago
источник
Это работает, только если вы имеете дело с MemoryStreams.
Nyerguds
0

Легко и безопасно - создайте новый поток из оригинального источника:

    MemoryStream source = new MemoryStream(byteArray);
    MemoryStream copy = new MemoryStream(byteArray);
Грэм Лайт
источник