Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Poznámka:
Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 10 tohoto článku.
Upozorňující
Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v zásadách podpory .NET a .NET Core. Aktuální verzi najdete v tomto článku ve verzi .NET 9.
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:
FormReaderTextReaderTextWriterHttpResponse.WriteAsync
Streamy se z architektury neodeberou. Streamy se budou dál používat v rámci .NET:
- Mnoho typů datových proudů nemá ekvivalenty potrubí, například
FileStreamsaResponseCompression. - Přidání komprese do datového proudu je jednoduché.
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
StringBuildervyrovná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 lze předchozí scénář streamu 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
PipeReaderpopisovače bajtů, které nebyly použity. - Kódované řetězce se přímo přidají do seznamu vrácených řetězců.
-
ToArrayKromě volání a paměti používané řetězcem je vytvoření řetězce přidělené zdarma.
Při zapisování přímo do HttpResponse.BodyWriter volejte PipeWriter.FlushAsync ručně, aby se zajistilo, že se data odesílají do základního těla odpovědi. Tady je důvod:
-
HttpResponse.BodyWriterjePipeWriterkterý uchovává data, dokud se neaktivuje operace vyprázdnění. - Volání
FlushAsynczapisuje data uložená do vyrovnávací paměti do základního textu odpovědi.
Je na vývojáři, aby se rozhodl, kdy volat FlushAsync, s přihlédnutím k faktorům, jako je velikost vyrovnávací paměti, režie při zápisu do sítě a zda se mají data odesílat v diskrétních blocích. Další informace naleznete v tématu System.IO.Pipelines v .NET.
Adaptéry
, BodyBodyReadera 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.