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

Od Justin Kotalik

Tento článek vysvětluje, jak číst z textu požadavku a zapisovat do textu odpovědi. Kód pro tyto operace může být vyžadován při psaní middlewaru. Mimo psaní middlewaru se vlastní kód obecně nevyžaduje, protože operace zpracovává MVC a Razor Pages.

Pro těla požadavku a odpovědi existují dvě abstrakce: a Stream Pipe . Pro čtení požadavků HttpRequest.Body je , a je Stream HttpRequest.BodyReader PipeReader . Pro psaní odpovědí HttpResponse.Body je , a je Stream HttpResponse.BodyWriter PipeWriter .

Pipelines se doporučuje používat přes datové proudy. Toky jednoduché operace jednodušší, ale kanály mají výhodu v oblasti výkonu a ve většině scénářů se snadněji používají. ASP.NET Core začít používat kanály místo datových proudů interně. Mezi příklady patří:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

Toky se z architektury odebraly. Toky i nadále používat v celé .NET a mnoho typů datových proudů nemá ekvivalenty kanálu, například FileStreams a ResponseCompression .

Příklady streamů

Předpokládejme, že cílem je vytvořit middleware, který čte celý text požadavku jako seznam řetězců a rozděluje je na nové řádky. Jednoduchá implementace streamu může vypadat jako v následujícím příkladu:

Upozornění

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ž angličtiny, dejte nám vědět v tomto problému diskuze na GitHubu.

Tento kód funguje, ale existuje několik problémů:

  • Před připojením k objektu vytvoří příklad další StringBuilder řetězec ( ), který se okamžitě encodedString vyvolá. K tomuto procesu dochází u všech bajtů v datovém proudu, takže výsledkem je dodatečné přidělení paměti pro celý text požadavku.
  • Příklad před rozdělením na nové řádky přečte celý řetězec. Je efektivnější kontrolovat nové řádky v poli bajtů.

Tady je příklad, který opravuje některé z předchozích problémů:

Upozornění

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:

  • Neuloží celý text požadavku do vyrovnávací paměti, pokud neexistují žádné StringBuilder znaky nového řádku.
  • Nevolá Split v řetězci .

Stále však existuje několik problémů:

  • Pokud jsou znaky nového řádku zhuštěné, velká část textu požadavku je v řetězci ve vyrovnávací paměti.
  • Kód dál vytváří řetězce ( ) a přidává je do vyrovnávací paměti řetězců, což má za výsledek remainingString dodatečné přidělení.

Tyto problémy je možné opravit, ale s malým zlepšením se kód postupně komplikuje. Pipelines 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 stejný scénář zpracovat pomocí třídy 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ší mnoho problémů, které implementace datových proudů měly:

  • Není potřeba vyrovnávací paměť řetězce, protože popisovače PipeReader bajtů, které nebyly použity.
  • Kódované řetězce se přidávají přímo do seznamu vrácených řetězců.
  • Kromě volání a paměti používané řetězcem je vytvoření řetězce ToArray bez přidělení.

Adaptéry

Vlastnosti Body BodyReader , BodyWriter a jsou k dispozici pro a HttpRequest HttpResponse . Když nastavíte jiný datový proud, nová sada adaptérů automaticky přizpůsobí Body každý typ druhému. Pokud nastavíte HttpRequest.Body na nový datový proud, automaticky se nastaví HttpRequest.BodyReader na nový, který PipeReader zabalí HttpRequest.Body .

StartAsync (Asynchronní spuštění)

HttpResponse.StartAsync slouží k označení, že hlavičky jsou neupravitelné, a ke spouštění OnStarting zpětných volání. Při použití jako serveru volání před použitím zaručuje, že paměť vrácená službou patří do interní, nikoli Kestrel StartAsync externí vyrovnávací PipeReader GetMemory Kestrel Pipe paměti.

Další zdroje informací