Avatar ·

Как создать массив байтов из потока (Stream) в C#? Полное руководство

Всем привет! Я новичок в C# и работаю над приложением, которое загружает файлы из интернета. Мне нужно прочитать все данные из NetworkStream или FileStream в массив byte[]. Проблема в том, что я не знаю заранее размер потока. Пытался использовать свойство Length, но для многих потоков оно выдает исключение NotSupportedException. Вот мой текущий код, который не работает:
// Пытаюсь прочитать поток в массив байтов
byte[] buffer = new byte[stream.Length]; // Ошибка: 'Stream' does not support seeking
stream.Read(buffer, 0, buffer.Length);
Эта ошибка возникает, когда я работаю с сетевыми потоками (NetworkStream) или потоками из HTTP-ответов. Я понимаю, что нужно читать данные частями, но не знаю, как правильно организовать цикл. Видел что-то про MemoryStream, но не уверен, как его использовать. Подскажите, пожалуйста, как правильно и эффективно преобразовать Stream в массив байтов? Желательно с примером кода для .NET 3.5 (у меня legacy проект). Заранее спасибо! Среда: C#, .NET 3.5, работа с потоками ввода-вывода. <<>>

Правильный способ: чтение потока с буферизацией и MemoryStream

Ваша проблема типична для начинающих: Stream.Length доступен не для всех потоков (например, NetworkStream или GZipStream его не поддерживает). Универсальное решение — читать данные блоками фиксированного размера в буфер и собирать их в MemoryStream. Вот проверенный метод для .NET 3.5:

public static byte[] ReadFully(Stream stream)
{
    // Используем using, чтобы гарантировать освобождение ресурсов
    using (var memoryStream = new MemoryStream())
    {
        // Буфер на 8 КБ — оптимальный размер для большинства сценариев
        byte[] buffer = new byte[8192];
        int bytesRead;

        // Читаем, пока поток не вернет 0 байт (конец данных)
        while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
        {
            // Записываем прочитанные байты в MemoryStream
            memoryStream.Write(buffer, 0, bytesRead);
        }

        // Возвращаем массив байтов из MemoryStream
        return memoryStream.ToArray();
    }
}

Как это работает

  • MemoryStream — это динамический поток в памяти, который автоматически увеличивается при записи. Он идеально подходит для сбора данных неизвестного размера.
  • Буфер 8 КБ — это золотая середина: уменьшает количество вызовов Read без излишнего потребления памяти.
  • Цикл while продолжается, пока Read возвращает положительное число. Когда поток заканчивается, метод возвращает 0, и цикл завершается.
  • using гарантирует, что MemoryStream будет корректно закрыт и освобожден, даже если возникнет исключение.

Улучшенная версия с начальной емкостью

Если вы знаете примерный размер данных (например, из заголовка Content-Length в HTTP), можно оптимизировать производительность:

public static byte[] ReadFully(Stream stream, int initialCapacity = 0)
{
    using (var memoryStream = new MemoryStream(initialCapacity))
    {
        byte[] buffer = new byte[8192];
        int bytesRead;

        while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
        {
            memoryStream.Write(buffer, 0, bytesRead);
        }

        return memoryStream.ToArray();
    }
}

Важные замечания для .NET 3.5

  • Метод Stream.CopyTo (который упрощает задачу) появился только в .NET 4.0. Для 3.5 нужно использовать ручной цикл, как показано выше.
  • Не пытайтесь использовать stream.Length для потоков без поддержки поиска — это вызовет исключение.
  • Для очень больших потоков (сотни мегабайт) подумайте о потоковой обработке, чтобы не загружать всю память сразу.

Пример использования

// Чтение файла
using (var fileStream = File.OpenRead("data.bin"))
{
    byte[] data = ReadFully(fileStream);
    Console.WriteLine($"Прочитано {data.Length} байт");
}

// Чтение из сети (например, WebResponse)
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://example.com/file.dat");
using (var response = request.GetResponse())
using (var responseStream = response.GetResponseStream())
{
    byte[] data = ReadFully(responseStream);
}

Этот подход гарантированно работает для любых потоков, поддерживающих чтение, и является стандартным решением в C#.

<<>>

Альтернативный подход: использование BinaryReader

Если вам нужно читать данные из потока без явного управления буфером, можно использовать BinaryReader в паре с MemoryStream. Этот способ короче, но менее гибкий:

public static byte[] ReadFullyWithBinaryReader(Stream stream)
{
    using (var memoryStream = new MemoryStream())
    {
        using (var reader = new BinaryReader(stream))
        {
            // Читаем все доступные байты
            // BinaryReader.ReadBytes(int) требует указания количества байт,
            // поэтому мы читаем блоками по 4 КБ
            byte[] buffer;
            const int bufferSize = 4096;
            
            do
            {
                buffer = reader.ReadBytes(bufferSize);
                if (buffer.Length > 0)
                    memoryStream.Write(buffer, 0, buffer.Length);
            } while (buffer.Length == bufferSize); // Пока читаем полные блоки
            
            return memoryStream.ToArray();
        }
    }
}

Когда это полезно

  • Если вы уже используете BinaryReader для чтения примитивных типов (int, double и т.д.) из того же потока.
  • Для простых сценариев, где не требуется тонкая настройка буферизации.

Недостатки

  • Метод ReadBytes создает новый массив при каждом вызове, что увеличивает нагрузку на сборщик мусора.
  • Логика определения конца потока менее надежна: если поток вернет количество байт, кратное размеру буфера, последний вызов может быть избыточным.

Поэтому для production-кода я рекомендую первый вариант с прямым чтением в буфер — он производительнее и предсказуемее.