Операции запросов и ответов в ASP.NET Core
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 8 этой статьи.
Автор Джастин Коталик (Justin Kotalik)
В этой статье объясняется, как читать текст запроса и писать текст ответа. Возможно, вам потребуется написать код для этих операций при создании ПО промежуточного слоя. В других случаях писать такой код обычно не нужно, так как эти операции обрабатываются MVC и Razor Pages.
Существует две абстракции для текста запросов и ответов: Stream и Pipe. При чтении запроса HttpRequest.Body — это Stream, а HttpRequest.BodyReader
— это PipeReader. При записи ответа HttpResponse.Body — это Stream, а HttpResponse.BodyWriter
— это PipeWriter.
Рекомендуется использовать конвейеры, а не потоки. Потоки удобнее использовать для некоторых простых операций, но производительность конвейеров выше и с ними проще работать в большинстве сценариев. Начиная с ASP.NET Core преимущество отдается внутреннему использованию конвейеров вместо потоков. Вот некоторые примеры.
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
Потоки не удаляются из платформы. С ними продолжают работать в .NET, и многие типы потоков не имеют эквивалентного конвейера, например FileStreams
и ResponseCompression
.
Примеры потоков
Предположим, необходимо создать ПО промежуточного слоя, которое считывает весь текст запроса как список строк с разделением на новые строки. Реализация простого потока может выглядеть следующим образом:
Предупреждение
Следующий код:
- используется для демонстрации проблем без использования канала для чтения текста запроса;
- не предназначен для использования в рабочих приложениях.
private async Task<List<string>> GetListOfStringsFromStream(Stream requestBody)
{
// Build up the request body in a string builder.
StringBuilder builder = new StringBuilder();
// Rent a shared buffer to write the request body into.
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
while (true)
{
var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);
if (bytesRemaining == 0)
{
break;
}
// Append the encoded string into the string builder.
var encodedString = Encoding.UTF8.GetString(buffer, 0, bytesRemaining);
builder.Append(encodedString);
}
ArrayPool<byte>.Shared.Return(buffer);
var entireRequestBody = builder.ToString();
// Split on \n in the string.
return new List<string>(entireRequestBody.Split("\n"));
}
Если вы хотите увидеть комментарии к коду, переведенные на языки, отличные от английского, сообщите нам на странице обсуждения этой проблемы на сайте GitHub.
Этот код работает, но есть определенные проблемы:
- Перед добавлением
StringBuilder
в коде создается другая строка (encodedString
), которая сразу отбрасывается. Этот процесс выполняется для всех байтов в потоке, и в результате выделяется дополнительный объем памяти для всего текста запроса. - Код считывает всю строку перед разделением на новые строки. Более эффективным вариантом является поиск новых строк в массиве байтов.
Ниже приведен пример, в котором устранены некоторые предыдущие проблемы.
Предупреждение
Следующий код:
- используется для демонстрации решений некоторых проблем в приведенном выше коде без решения всех проблем;
- не предназначен для использования в рабочих приложениях.
private async Task<List<string>> GetListOfStringsFromStreamMoreEfficient(Stream requestBody)
{
StringBuilder builder = new StringBuilder();
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
List<string> results = new List<string>();
while (true)
{
var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);
if (bytesRemaining == 0)
{
results.Add(builder.ToString());
break;
}
// Instead of adding the entire buffer into the StringBuilder
// only add the remainder after the last \n in the array.
var prevIndex = 0;
int index;
while (true)
{
index = Array.IndexOf(buffer, (byte)'\n', prevIndex);
if (index == -1)
{
break;
}
var encodedString = Encoding.UTF8.GetString(buffer, prevIndex, index - prevIndex);
if (builder.Length > 0)
{
// If there was a remainder in the string buffer, include it in the next string.
results.Add(builder.Append(encodedString).ToString());
builder.Clear();
}
else
{
results.Add(encodedString);
}
// Skip past last \n
prevIndex = index + 1;
}
var remainingString = Encoding.UTF8.GetString(buffer, prevIndex, bytesRemaining - prevIndex);
builder.Append(remainingString);
}
ArrayPool<byte>.Shared.Return(buffer);
return results;
}
В предшествующем примере:
- Не создается буфер для всего текста запроса в
StringBuilder
, если нет символов новой строки. - В строке не вызывается
Split
.
Но некоторые проблемы при этом остаются:
- Если символы новой строки разрежены, большая часть текста запроса буферизуется в строке.
- Строки (
remainingString
) по-прежнему создаются в коде и добавляются в буфер строки, что приводит к выделению дополнительной памяти.
Эти проблемы можно решить, но ценой усложнения кода при незначительных его улучшениях. Конвейеры позволяют решить эти проблемы при минимальной сложности кода.
Pipelines
В следующем примере показано, как тот же сценарий можно обработать с помощью PipeReader.
private async Task<List<string>> GetListOfStringFromPipe(PipeReader reader)
{
List<string> results = new List<string>();
while (true)
{
ReadResult readResult = await reader.ReadAsync();
var buffer = readResult.Buffer;
SequencePosition? position = null;
do
{
// Look for a EOL in the buffer
position = buffer.PositionOf((byte)'\n');
if (position != null)
{
var readOnlySequence = buffer.Slice(0, position.Value);
AddStringToList(results, in readOnlySequence);
// Skip the line + the \n character (basically position)
buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
}
}
while (position != null);
if (readResult.IsCompleted && buffer.Length > 0)
{
AddStringToList(results, in buffer);
}
reader.AdvanceTo(buffer.Start, buffer.End);
// At this point, buffer will be updated to point one byte after the last
// \n character.
if (readResult.IsCompleted)
{
break;
}
}
return results;
}
private static void AddStringToList(List<string> results, in ReadOnlySequence<byte> readOnlySequence)
{
// Separate method because Span/ReadOnlySpan cannot be used in async methods
ReadOnlySpan<byte> span = readOnlySequence.IsSingleSegment ? readOnlySequence.First.Span : readOnlySequence.ToArray().AsSpan();
results.Add(Encoding.UTF8.GetString(span));
}
В этом примере устранены многие проблемы, характерные для реализации посредством потоков.
- В буфере строки теперь нет необходимости, так как
PipeReader
обрабатывает байты, которые не использовались. - Кодированные строки напрямую добавляются в список возвращенных строк.
- Создание строк не связано с выделением памяти, кроме памяти, используемой строкой (за исключением вызова
ToArray
).
Адаптеры
Для HttpRequest
и HttpResponse
доступны свойства Body
, BodyReader
и BodyWriter
. Если назначить Body
другому потоку, новый набор адаптеров автоматически адаптирует каждый тип к другому. Если назначить HttpRequest.Body
новому потоку, HttpRequest.BodyReader
автоматически назначается новому PipeReader
, который создает оболочку для HttpRequest.Body
.
StartAsync
Метод HttpResponse.StartAsync
используется для указания того, что заголовки являются неизменяемыми, а также для запуска обратных вызовов OnStarting
. При использования Kestrel в качестве сервера вызов StartAsync
перед применением PipeReader
гарантирует, что память, возвращенная GetMemory
, относится к внутреннему методу Kestrel Pipe, а не к внешнему буферу.
Дополнительные ресурсы
ASP.NET Core