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

, Kotin Kotalik tarafından

Bu makalede, istek gövdesinden okuma ve yanıt gövdesine yazma işlemleri açıklanmaktadır. Bu işlemler için kod, ara yazılım yazılırken gerekli olabilir. İşlemler MVC ve sayfalar tarafından işlendiği için, yazma ara yazılımı dışında, özel kod genellikle gerekli değildir Razor .

İstek ve yanıt gövdelerinin iki soyutlamaları vardır: Stream ve Pipe . İstek okuma için HttpRequest.Body bir Stream , ve HttpRequest.BodyReader olur PipeReader . Yanıt yazma için HttpResponse.Body bir Stream , ve HttpResponse.BodyWriter olur PipeWriter .

Pipelines akışlar üzerinde önerilir. Akışlar bazı basit işlemler için daha kolay olabilir, ancak işlem hatları performans avantajına sahiptir ve çoğu senaryoda daha kolay kullanılır. ASP.NET Core dahili akışlar yerine işlem hatlarını kullanmaya başlıyor. Örnekler arasında şunlar yer almaktadır:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

Akışlar çerçeveden kaldırılmıyor. Akışlar .net genelinde kullanmaya devam edin ve birçok akış türü, ve gibi kanal eşdeğerlerine sahip değildir FileStreams ResponseCompression .

Stream örnekleri

Hedefin, yeni satırları bölerek tüm istek gövdesini bir dizeler listesi olarak okuyan bir ara yazılım oluşturduğunu varsayalım. Basit bir akış uygulamasının aşağıdaki örnekteki gibi görünebilir:

Uyarı

Aşağıdaki kod:

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

Ingilizce dışındaki dillere çevrilmiş kod açıklamalarını görmek isterseniz, Bu GitHub tartışma sorununubize tanıyın.

Bu kod işe yarar ancak bazı sorunlar vardır:

  • ' A eklemeden önce StringBuilder , örnek hemen oluşturulan başka bir dize ( encodedString ) oluşturur. Bu işlem akıştaki tüm baytlar için gerçekleşir, bu nedenle sonuç, tüm istek gövdesinin boyutunun fazladan bellek ayırmasından oluşur.
  • Örnek, yeni satırlara bölmeden önce tüm dizeyi okur. Bayt dizisindeki yeni satırları denetlemek daha etkilidir.

Yukarıdaki sorunlardan bazılarını düzelten bir örnek aşağıda verilmiştir:

Uyarı

Aşağıdaki kod:

  • , Tüm sorunları çözmediğinden önceki koddaki bazı sorunlara yönelik çözümleri 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:

  • StringBuilderHerhangi bir yeni satır karakteri olmadıkça tüm istek gövdesini arabelleğe almaz.
  • Dizeye çağrı yapmaz Split .

Ancak, yine de bazı sorunlar vardır:

  • Yeni satır karakterleri seyrek ise, dize içinde istek gövdesinin büyük bir bölümü arabelleğe alınır.
  • Kod dizeler () oluşturmaya devam eder remainingString ve bunları dize arabelleğine ekler ve bu da ek bir ayırmaya neden olur.

Bu sorunlar düzeltilebilir, ancak kod çok daha karmaşık bir geliştirme sayesinde daha karmaşık hale geliyor. Pipelines, bu sorunları en az kod karmaşıklığı ile çözmenin bir yolunu sağlar.

Pipelines

Aşağıdaki örnek, bir Pipereaderkullanılarak aynı senaryonun nasıl işlenebileceğini göstermektedir:

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ışlar uygulamalarında bulunan birçok sorunu düzeltir:

  • Kullanılmayan baytları işlediği için bir dize arabelleği gerekmez PipeReader .
  • Kodlanmış dizeler doğrudan döndürülen dizeler listesine eklenir.
  • ToArrayÇağrı dışında ve dize tarafından kullanılan bellek, dize oluşturma ücretsizdir.

Örünü

Body, Ve BodyReader BodyWriter özellikleri ve için kullanılabilir HttpRequest HttpResponse . BodyFarklı bir akışa ayarlandığında, yeni bir bağdaştırıcı kümesi, her türü otomatik olarak birbirlerine uyarlar. HttpRequest.BodyYeni bir akışa ayarlarsanız, HttpRequest.BodyReader otomatik olarak sarmalanmış yeni olarak ayarlanır PipeReader HttpRequest.Body .

StartAsync

HttpResponse.StartAsync üstbilgilerin değiştirilemeyen ve geri çağırmaların çalıştırıldığı belirtmek için kullanılır OnStarting . KestrelSunucu olarak kullanırken, StartAsync PipeReader tarafından döndürülen belleğin, GetMemory Kestrel Pipe dış bir arabellek yerine iç öğesine ait olduğunu garanti eder.

Ek kaynaklar