HttpContent для отправки списка через HttpClient

Вопрос или проблема

Мне нужно отправлять HTTP API-запросы, тело которых представлено в виде List<ArraySegment<byte>> (обычный фиксированный заголовок + переменная середина + фиксированный хвост). Socket в .NET может отправлять List<ArraySegment<byte>> с незапамятных времен, но я не могу найти аналогичную функциональность в контексте HttpClient.

Чтобы отправить тело, HttpClient требует HttpRequestMessage, в котором тело для отправки представлено в свойстве Content (типа HttpContent).

Ни HttpContent, ни один из его наследников не имеют конструктора, который принимает List<ArraySegment<byte>> или что-то подобное; они работают только с отдельными массивами. То же самое касается MemoryStream, который иначе мог бы использоваться через один из наследников HttpContent.

На данный момент ситуация выглядит безнадежно: я могу либо объединить фрагменты тела в один байтовый буфер для отправки, либо реализовать HttpContent на основе List<ArraySegment<byte>>.

Ни один из вариантов не кажется особенно привлекательным. Тело сообщения может занимать десятки мегабайт, поэтому ненужное копирование будет создавать избыточную нагрузку на подсистему памяти и снижать производительность (сервер находится на том же физическом хосте и доступен либо через устройство обратной связи, либо через виртуальные сетевые адаптеры). HttpContent и MemoryStream имеют ненужные большие и сложные API, которые кажутся трудными в реализации.

Существует ли более простой способ отправить List<ArraySegment<byte>>? Если нет, знает ли кто-то из опыта, какой путь менее болезненный – реализовать HttpContent или реализовать MemoryStream? (Последний имеет чуть более крупный API, но он кажется более простым и понятным.)

P.S.: чтобы поставить вещи в контекст: я перерабатываю инструмент, который мы используем для измерения производительности API-сервера, а также для функционального и стрессового тестирования; в настоящее время он основан на API Socket .NET, но использование обычных TCP-сокетов фактически ограничивает его HTTP 1.1. Это именно тот момент, где вступает в игру HttpClient.

Ответ или решение

Для отправки данных, представленных в виде List<ArraySegment<byte>>, через HttpClient в .NET существует несколько вариантов решения, хотя встроенной поддержки для такого типа данных нет. Давайте подробно рассмотрим этот вопрос, анализируя доступные подходы и стратегии, обеспечивающие эффективную и производительную отправку сообщений.

1. Проблема

HttpClient требует, чтобы тело HTTP-запроса было представлено в виде объекта HttpContent. Однако HttpContent и его производные, как вы правильно заметили, не поддерживают прямую передачу структур данных, таких как List<ArraySegment<byte>>. Это создаёт сложности, когда необходимо аккуратно и эффективно управлять данными большого объёма, поскольку объединение фрагментов в один массив может привести к ненужным копиям и дополнительной нагрузке на память.

2. Варианты решений

Наиболее очевидные подходы для решения данной проблемы следующие:

2.1. Объединение данных в массив байт

Этот подход предполагает создание единого массива байт, в который будут скопированы все данные из List<ArraySegment<byte>>. Это проще в реализации, однако он может привести к значительным затратам ресурсов, особенно если размеры данных велики.

Пример кода:

public byte[] CombineSegments(List<ArraySegment<byte>> segments)
{
    int totalLength = segments.Sum(segment => segment.Count);
    var result = new byte[totalLength];
    int offset = 0;

    foreach (var segment in segments)
    {
        Buffer.BlockCopy(segment.Array, segment.Offset, result, offset, segment.Count);
        offset += segment.Count;
    }

    return result;
}

Однако именно этот вариант, хоть и простой, несёт собой риски по производительности.

2.2. Создание собственного HttpContent

Этот метод может показаться более сложным на первый взгляд, но способен обеспечить реальную выгоду в производительности при работе с большими объёмами данных. Мы можем создать класс, производный от HttpContent, который будет манипулировать List<ArraySegment<byte>> без дополнительных копий.

Пример реализации:

public class ArraySegmentHttpContent : HttpContent
{
    private readonly List<ArraySegment<byte>> _segments;

    public ArraySegmentHttpContent(List<ArraySegment<byte>> segments)
    {
        _segments = segments ?? throw new ArgumentNullException(nameof(segments));
    }

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
    {
        foreach (var segment in _segments)
        {
            stream.Write(segment.Array, segment.Offset, segment.Count);
        }

        return Task.CompletedTask;
    }

    protected override bool TryComputeLength(out long length)
    {
        length = _segments.Sum(segment => segment.Count);
        return true;
    }
}

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

3. Рекомендации

Если приоритетом является производительность и управление памятью, стоит разработать собственный HttpContent. Такой подход позволит вам точно контролировать процесс сериализации и обеспечить лучшую эффективность работы с данными.

Если же время разработки критично, и нет сильной нагрузки на производительность, стоит рассмотреть вариант объединения сегментов в массив байт, особенно если объём данных относительно небольшой.

Заключение

Построение HTTP-запросов с использованием HttpClient при работе с List<ArraySegment<byte>> требует внимательного подхода и оценки компромиссов между простотой реализации и эффективностью использования ресурсов. Класс ArraySegmentHttpContent, как показано выше, предоставляет удобный способ обойти ограничения стандартного HttpContent, минимизируя накладные расходы и обеспечивая высокую производительность.

Оцените материал
Добавить комментарий

Капча загружается...