Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Примечание.
Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.
В этой статье объясняется, как читать текст запроса и писать текст ответа. Возможно, вам потребуется написать код для этих операций при создании ПО промежуточного слоя. Кроме написания промежуточного программного обеспечения, писать такой код обычно не нужно, так как эти операции обрабатываются MVC и Razor страниц.
Существует две абстракции для текста запросов и ответов: Stream и Pipe. При чтении запроса HttpRequest.Body — это Stream, а HttpRequest.BodyReader — это PipeReader. При записи ответа HttpResponse.Body — это Stream, а HttpResponse.BodyWriter — это PipeWriter.
Конвейеры рекомендуется использовать через потоки. Потоки удобнее использовать для некоторых простых операций, но производительность конвейеров выше и с ними проще работать в большинстве сценариев. Начиная с ASP.NET Core преимущество отдается внутреннему использованию конвейеров вместо потоков. Вот некоторые примеры.
FormReaderTextReaderTextWriterHttpResponse.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"));
}
Этот код работает, но есть определенные проблемы:
- Перед добавлением
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) по-прежнему создаются в коде и добавляются в буфер строки, что приводит к выделению дополнительной памяти.
Эти проблемы можно решить, но ценой усложнения кода при незначительных его улучшениях. Конвейеры позволяют решить эти проблемы при минимальной сложности кода.
Трубопроводы
В следующем примере показано, как можно обрабатывать предыдущий сценарий потока с помощью 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и памяти, используемой строкой, создание строк не связано с выделением памяти.
При прямой записи в HttpResponse.BodyWriter вызовите PipeWriter.FlushAsync вручную, чтобы убедиться, что данные сбрасываются в основное тело ответа. Для этого есть следующие причины.
-
HttpResponse.BodyWriterPipeWriter— это буферизация данных до запуска операции очистки. - Вызов
FlushAsyncзаписывает буферированные данные в основной текст отклика.
Разработчик может решить, когда следует вызывать FlushAsync, балансировать факторы, такие как размер буфера, расходы на запись сети, а также следует ли отправлять данные в дискретных блоках. Дополнительные сведения см. в разделе System.IO.Pipelines в .NET.
Адаптеры
Для Body и BodyReader доступны свойства BodyWriter, HttpRequest и HttpResponse. Если назначить Body другому потоку, новый набор адаптеров автоматически адаптирует каждый тип к другому. Если назначить HttpRequest.Body новому потоку, HttpRequest.BodyReader автоматически назначается новому PipeReader, который создает оболочку для HttpRequest.Body.
StartAsync
Метод HttpResponse.StartAsync используется для указания того, что заголовки являются неизменяемыми, а также для запуска обратных вызовов OnStarting. При использовании Kestrel в качестве сервера вызов StartAsync перед применением PipeReader гарантирует, что память, возвращенная GetMemory, относится к внутреннему KestrelPipe, а не к внешнему буферу.
Дополнительные ресурсы
ASP.NET Core