ASP.NET Core'da istek ve yanıt işlemleri

Not

Bu, bu makalenin en son sürümü değildir. Geçerli sürüm için bu makalenin .NET 10 sürümüne bakın.

Uyarı

ASP.NET Core'un bu sürümü artık desteklenmiyor. Daha fazla bilgi için bkz . .NET ve .NET Core Destek İlkesi. Geçerli sürüm için bu makalenin .NET 10 sürümüne bakın.

Tarafından Justin Kotalik

Bu makalede, istek gövdesinden okuma ve yanıt gövdesine yazma işlemi açıklanır. Ara yazılım yazarken bu işlemlerin kodu gerekebilir. İşlemler MVC ve Razor Sayfalar tarafından işlenmediğinden, ara yazılım yazmanın dışında özel kod genellikle gerekli değildir.

İstek ve yanıt gövdeleri için iki soyutlama vardır: Stream ve Pipe. okuma isteği için, HttpRequest.Body bir Stream, ve HttpRequest.BodyReader bir PipeReader şeklindedir. Yanıt yazımı için, HttpResponse.Body bir Stream ve HttpResponse.BodyWriter bir PipeWriter biçimindedir.

Pipeline'lar, akışlar yerine önerilir. Bazı basit işlemler için akışların kullanımı daha kolay olabilir, ancak işlem hatlarının bir performans avantajı vardır ve çoğu senaryoda kullanımı daha kolaydır. ASP.NET Core, dahili akışlar yerine işlem hatlarını kullanmaya başlıyor. Örnekler şunları içerir:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

Akışlar çerçeveden kaldırılmıyor. Akışlar .NET genelinde kullanılmaya devam eder:

  • Birçok akış türünün, FileStreams ve ResponseCompression gibi kanal eşdeğerleri yoktur.
  • Akışa sıkıştırma eklemek kolaydır.

Akış örnekleri

Amacın, istek gövdesinin tamamını dize listesi olarak okuyan ve yeni satırlara bölen bir ara yazılım oluşturmak olduğunu varsayalım. Basit bir akış uygulaması aşağıdaki örneğe benzer olabilir:

Uyarı

Aşağıdaki kod:

  • Bir boru (pipe) kullanılmadığında istek gövdesini okuma sorunlarını göstermek için kullanılır.
  • Üretim uygulamalarında kullanılmak üzere tasarlanmamıştır.
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"));
}

Bu kod çalışır, ancak bazı sorunlar vardır:

  • StringBuilder öğesine eklemeden önce, örnek, hemen atılan başka bir dize (encodedString) oluşturur. Bu işlem akıştaki tüm baytlar için gerçekleşir, bu nedenle sonuç istek gövdesinin tamamının boyutuna ek bellek ayırma işlemidir.
  • Örnek, yeni satırlara bölmeden önce dizenin tamamını okur. Bayt dizisindeki yeni satırları denetlemek daha verimlidir.

Aşağıda, önceki sorunlardan bazılarını düzelten bir örnek verilmişti:

Uyarı

Aşağıdaki kod:

  • Tüm sorunları çözmezken önceki koddaki bazı sorunların çözümlerini göstermek için kullanılır.
  • Üretim uygulamalarında kullanılmak üzere tasarlanmamıştır.
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;
}

Bu önceki örnek:

  • Yeni satır karakteri olmadığı sürece istek gövdesinin tamamını bir StringBuilder içinde arabelleğe almaz.
  • Dize üzerinde Split çağrısı yapmaz.

Ancak yine de birkaç sorun vardır:

  • Yeni satır karakterleri seyrekse, istek gövdesinin büyük bir kısmı dizide arabelleklenir.
  • Kod dizeler oluşturmayı sürdürüremainingStringr ve bunları dize arabelleğine ekler, bu da ekstra bellek ayırmaya neden olur.

Bu sorunlar düzeltilebilir, ancak kod çok az geliştirmeyle giderek daha karmaşık hale geliyor. İşlem hatları, bu sorunları en düşük kod karmaşıklığıyla çözmek için bir yol sağlar.

Boru Hatları

Aşağıdaki örnek, önceki akış senaryosunun PipeReader kullanılarak nasıl işleneceğini gösterir:

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

Bu örnek, akış uygulamalarının karşılaştığı birçok sorunu düzeltir:

  • Çünkü PipeReader kullanılmamış baytları işlediğinden dize arabelleğine gerek yoktur.
  • Kodlanmış dizeler, döndürülen dizeler listesine doğrudan eklenir.
  • çağrı ve dize tarafından kullanılan bellek dışında, ToArray dize oluşturulması için herhangi bir tahsis gerekmez.

doğrudan adresine HttpResponse.BodyWriteryazarken, verilerin temel alınan yanıt yanıtı gövdesine boşaltıldığından emin olmak için el ile çağrısı PipeWriter.FlushAsync yapın. Bunun nedeni şu şekildedir:

  • HttpResponse.BodyWriter , PipeWriter temizleme işlemi tetiklenene kadar verileri arabelleğe alır.
  • Çağrıldığında FlushAsync, arabelleğe alınan verileri temel yanıt gövdesine yazar.

Çağrıyı ne zaman yapacağına karar vermek geliştiriciye bağlıdır; bu, arabelleğin boyutu, ağ üzerinden yazma ek yükü ve verilerin ayrık parçalar halinde gönderilip gönderilmeyeceği gibi faktörleri dengelemeyi içerir. Daha fazla bilgi için bkz . .NET'te System.IO.Pipelines.

Bağdaştırıcılar

Body, BodyReader ve BodyWriter özellikleri HttpRequest ve HttpResponse için kullanılabilir. Farklı bir akışa ayarladığınızda Body , yeni bir bağdaştırıcı kümesi her türü otomatik olarak diğerine uyarlar. Yeni bir akışa HttpRequest.Body ayarlarsanız, HttpRequest.Body öğesini kapsayan yeni bir PipeReader olarak HttpRequest.BodyReader otomatik olarak ayarlanır.

StartAsync

HttpResponse.StartAsync, üst bilgilerin değiştirilemez olduğunu belirtmek ve OnStarting geri çağırmalarını çalıştırmak için kullanılır. Kestrel sunucu olarak kullanıldığında, PipeReader'yi kullanmadan önce StartAsync çağrısı yapmak, GetMemory tarafından döndürülen belleğin bir dış arabellek yerine Kestrel'nin iç Pipe birimine ait olmasını garanti eder.

Ek kaynaklar