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

, 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.

Další materiály