Megosztás a következőn keresztül:


Kérés- és válaszműveletek a ASP.NET Core-ban

Megjegyzés:

Ez nem a cikk legújabb verziója. Az aktuális kiadásról a cikk .NET 10-es verziójában olvashat.

Figyelmeztetés

A ASP.NET Core ezen verziója már nem támogatott. További információt a .NET és a .NET Core támogatási szabályzatában talál. A jelen cikk .NET 9-es verzióját lásd az aktuális kiadásért .

Készítette : Justin Kotalik

Ez a cikk bemutatja, hogyan olvashat a kérelem törzséből, és hogyan írhat a válasz törzsébe. Ezekhez a műveletekhez kód szükséges lehet köztes szoftver írásakor. A köztes szoftver írásán kívül az egyéni kód általában nem szükséges, mert a műveleteket az MVC és Razor a Pages kezeli.

Két absztrakció van a kérelem- és választestek számára: Stream és Pipe. A kérések olvasása során HttpRequest.Body egy Stream, és HttpRequest.BodyReader egy PipeReader. A válaszíráshoz HttpResponse.Body egy Stream, és HttpResponse.BodyWriter egy PipeWriter.

A folyamatok streameken keresztül ajánlottak. Az adatfolyamok néhány egyszerű művelet esetén könnyebben használhatók, de a pipeline-ok teljesítményelőnyben vannak, és a legtöbb esetben könnyebben alkalmazhatók. ASP.NET Core a belső streamek helyett folyamatokat kezd használni. Ide sorolhatóak például a következők:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

A streamek nem törlődnek a keretrendszerből. A streamek továbbra is használhatók a .NET-ben:

  • Számos adatfolyamtípus nem rendelkezik csőegyenértékekkel, például FileStreams és ResponseCompression.
  • Egyszerűen adhat hozzá tömörítést egy streamhez.

Példák streamelése

Tegyük fel, hogy a cél egy köztes szoftver létrehozása, amely a teljes kérelemtörzset sztringek listájaként olvassa fel, új sorokra felosztva. Egy egyszerű stream implementáció a következő példához hasonlóan nézhet ki:

Figyelmeztetés

A következő kód:

  • Arra szolgál, hogy bemutassa azokat a problémákat, amelyek a kérés törzsének olvasásához használt pipe (csővezeték) hiányából származnak.
  • Éles alkalmazásokban nem használható.
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"));
}

Ha szeretné, hogy a kódkommentárok angolon kívül más nyelvekre is le legyenek fordítva, jelezze nekünk a GitHub vitafórumkérdésénél.

Ez a kód működik, de vannak problémák:

  • Mielőtt hozzáfűzné a StringBuilder-hez, a példa létrehoz egy másik karakterláncot (encodedString), amelyet azonnal eldob. Ez a folyamat a streamben lévő összes bájtra vonatkozóan történik, ezért az eredmény a teljes kérelemtörzs méretének extra memóriafoglalása.
  • A példa beolvassa a teljes karakterláncot, mielőtt új sorok mentén felosztaná. Hatékonyabb az új sorok keresése a bájttömbben.

Íme egy példa az előző problémák megoldására:

Figyelmeztetés

A következő kód:

  • Az előző kód egyes problémáinak megoldását szemlélteti, miközben nem oldja meg az összes problémát.
  • Éles alkalmazásokban nem használható.
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;
}

Ez a fenti példa:

  • Csak akkor nem puffereli a teljes kérelemtörzset, StringBuilder, ha nincsenek újsoros karakterek.
  • Nem hívja meg a Split a karakterláncot.

Azonban még mindig van néhány probléma:

  • Ha az új sor karakterek ritkán jelennek meg, a kérelem törzsének nagy része pufferelve lesz egy sztringben.
  • A kód továbbra is karakterláncokat (remainingString) hoz létre, és hozzáadja őket a karakterlánc pufferhez, ami plusz memóriaterület foglalást jelenthet.

Ezek a problémák javíthatók, de a kód egyre bonyolultabbá válik, kevés fejlesztéssel. A folyamatok minimális kódbonyolulással oldják meg ezeket a problémákat.

Csővezetékek

Az alábbi példa bemutatja, hogyan kezelhető az előző streamforgatókönyv egy PipeReaderrel:

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));
}

Ez a példa számos olyan problémát old meg, amelyek a streamek implementációinak voltak:

  • Nincs szükség karakterlánc-pufferre, mert a PipeReader kezeli a fel nem használt bájtokat.
  • A kódolt sztringek közvetlenül hozzáadódnak a visszaadott sztringek listájához.
  • A ToArray híváson és a sztring által használt memórián kívül a sztring létrehozása ingyenes.

Amikor közvetlenül ír a HttpResponse.BodyWriter, manuálisan hívja meg a PipeWriter.FlushAsync függvényt, hogy az adatok kiürüljenek az alapul szolgáló választestbe. A következőkért:

  • HttpResponse.BodyWriter olyan, PipeWriter amely puffereli az adatokat, amíg egy kiürítési művelet nem aktiválódik.
  • A hívás FlushAsync a pufferelt adatokat az alapjául szolgáló választörzsbe írja.

A fejlesztőn múlik, hogy mikor kell meghívni FlushAsync, kiegyensúlyozási tényezőket, például pufferméretet, hálózati írási többletterhelést, és hogy az adatokat különálló adattömbökben kell-e elküldeni. További információ: System.IO.Pipelines in .NET.

Adapterek

A Body, BodyReader és BodyWriter tulajdonságok elérhetők a HttpRequest és a HttpResponse számára. Ha másik streamre állít be Body , az új adapterek automatikusan egymáshoz igazítják az egyes típusokat. Ha HttpRequest.Body új folyammá van állítva, akkor HttpRequest.BodyReader automatikusan beállításra kerül egy új PipeReader-ra, amely körbefogja HttpRequest.Body.

StartAsync

A HttpResponse.StartAsync mechanizmus azt jelzi, hogy a fejlécek nem módosíthatók, és a OnStarting visszahívások futtatására szolgál. Amikor a Kestrel kiszolgálóként van használva, a StartAsync hívása garantálja, hogy a PipeReader által visszaadott memória a GetMemory belső Kestrel része, nem pedig külső puffer.

További erőforrások