Operace požadavků a odpovědí v ASP.NET Core

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

Toky se z architektury neodeberou. Toky se nadále používají v rozhraní .NET a mnoho 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ší prostředky