Operace požadavků a odpovědí v ASP.NET Core
Poznámka:
Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Upozorňující
Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Důležité
Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.
Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Autor: Justin Kotalik
Tento článek vysvětluje, jak číst z textu požadavku a zapisovat do textu odpovědi. Při psaní middlewaru se může vyžadovat kód pro tyto operace. Mimo psaní middlewaru se vlastní kód obecně nevyžaduje, protože operace zpracovává MVC a Razor Pages.
Existují dvě abstrakce pro tělo požadavku a odpovědi: Stream a Pipe. Pro žádosti o čtení, HttpRequest.Body je , Streama HttpRequest.BodyReader
je PipeReader. Pro psaní HttpResponse.Body odpovědí je , Streama HttpResponse.BodyWriter
je PipeWriter.
Kanály se doporučují přes streamy. Streamy se dají snadněji používat pro některé jednoduché operace, ale kanály mají výhodu výkonu a ve většině scénářů se snadněji používají. ASP.NET Core začíná používat kanály místo datových proudů interně. Příkladem může být:
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
Streamy se z architektury neodeberou. Streamy se nadále používají v rámci .NET a řada typů datových proudů nemá ekvivalenty kanálu, například FileStreams
a ResponseCompression
.
Příklady streamu
Předpokládejme, že cílem je vytvořit middleware, který přečte celý text požadavku jako seznam řetězců a rozdělí se na nové řádky. Jednoduchá implementace streamu může vypadat jako v následujícím příkladu:
Upozorňující
Následující kód:
- Slouží k předvedení problémů s použitím kanálu ke čtení textu požadavku.
- Není určen k použití v produkčních aplikacích.
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"));
}
Pokud chcete zobrazit komentáře ke kódu přeložené do jiných jazyků, než je angličtina, dejte nám vědět v této diskuzi na GitHubu.
Tento kód funguje, ale existují některé problémy:
- Před připojením k sadě
StringBuilder
, příklad vytvoří další řetězec (encodedString
), který je okamžitě vyvolán. K tomuto procesu dochází u všech bajtů v datovém proudu, takže výsledkem je přidělení paměti navíc velikost celého textu požadavku. - Příklad před rozdělením na nové řádky přečte celý řetězec. Je efektivnější zkontrolovat nové řádky v bajtovém poli.
Tady je příklad, který řeší některé z předchozích problémů:
Upozorňující
Následující kód:
- Slouží k předvedení řešení některých problémů v předchozím kódu, zatímco neřeší všechny problémy.
- Není určen k použití v produkčních aplikacích.
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;
}
Tento předchozí příklad:
- Neuvolní celý text požadavku do
StringBuilder
vyrovnávací paměti, pokud neexistují žádné znaky nového řádku. - Nezavolá
Split
řetězec.
Stále ale dochází k několika problémům:
- Pokud jsou znaky nového řádku zhuštěné, velká část textu požadavku je v řetězci uložena do vyrovnávací paměti.
- Kód nadále vytváří řetězce (
remainingString
) a přidává je do vyrovnávací paměti řetězce, což vede k dodatečnému přidělení.
Tyto problémy jsou opravitelné, ale kód se stává postupně složitější s malým vylepšením. Kanály poskytují způsob, jak tyto problémy vyřešit s minimální složitostí kódu.
Pipelines
Následující příklad ukazuje, jak stejný scénář lze zpracovat pomocí 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));
}
Tento příklad řeší řadu problémů, které měly implementace datových proudů:
- Vyrovnávací paměť řetězců není nutná, protože
PipeReader
popisovače bajtů, které nebyly použity. - Kódované řetězce se přímo přidají do seznamu vrácených řetězců.
ToArray
Kromě volání a paměti používané řetězcem je vytvoření řetězce přidělené zdarma.
Adaptéry
, Body
BodyReader
a BodyWriter
vlastnosti jsou k dispozici pro HttpRequest
a HttpResponse
. Když nastavíte Body
jiný datový proud, nová sada adaptérů automaticky přizpůsobí každý typ druhému. Pokud nastavíte HttpRequest.Body
nový datový proud, HttpRequest.BodyReader
nastaví se automaticky na nový PipeReader
, který se zabalí HttpRequest.Body
.
StartAsync
HttpResponse.StartAsync
slouží k označení, že hlavičky nejsou možné upravit a spouštět OnStarting
zpětná volání. Při použití Kestrel jako serveru volání StartAsync
před použitím PipeReader
záruky, že paměť vrácená GetMemory
patří do Kestrelinterní Pipe než externí vyrovnávací paměti.