Вопрос или проблема
Мне нужно отправлять 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
, минимизируя накладные расходы и обеспечивая высокую производительность.