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

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 Streamve HttpRequest.BodyReader şeklindedir PipeReader. Yanıt yazmak HttpResponse.Body için, bir Streamve HttpResponse.BodyWriter şeklindedir PipeWriter.

İşlem hatları akışlar üzerinden önerilir. Akışlar bazı basit işlemler için daha kolay kullanılabilir, 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ılmaz. Akışlar .NET genelinde kullanılmaya devam eder ve birçok akış türünün ve ResponseCompressiongibi FileStreams kanal eşdeğerleri yoktur.

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:

  • İstek gövdesini okumak için bir kanal kullanmama 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"));
}

Kod açıklamalarının İngilizce dışındaki dillere çevirisini görmek isterseniz, bunu bu GitHub tartışma konusunda bize bildirin.

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

  • öğesine eklemeden StringBuilderö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.
  • Dizede çağrı Split yapmaz.

Ancak yine de birkaç sorun vardır:

  • Yeni satır karakterleri seyrek ise, istek gövdesinin büyük bir kısmı dizede arabelleğe alınıyor.
  • Kod dizeler () oluşturmaya devam ederremainingString ve bunları dize arabelleğine ekler ve bu da ek 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.

Pipelines

Aşağıdaki örnek, aynı senaryonun 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:

  • Kullanılmamış baytları işlediğinden dize arabelleğine PipeReader 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şturma işlemi serbesttir.

Bağdaştırıcı

Bodyve için HttpRequestHttpResponse, BodyReaderve BodyWriter özellikleri 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 ayarlarsanızHttpRequest.Body, HttpRequest.BodyReader otomatik olarak öğesini kaydıran HttpRequest.Bodyyeni PipeReader bir akışa ayarlanır.

StartAsync

HttpResponse.StartAsync üst bilgilerin değiştirilemez olduğunu belirtmek ve geri çağırmaları çalıştırmak OnStarting için kullanılır. Sunucu olarak kullanırkenKestrel, tarafından döndürülen GetMemory belleğin PipeReader bir dış arabellek yerine 'nin iç Pipe birimine Kestrelait olduğunu garanti ederek kullanmadan önce çağrısı StartAsync yapın.

Ek kaynaklar