ASP.NET Core 中的要求和回應作業

作者:Justin Kotalik

此文章說明如何讀取要求本文及寫入回應本文。 撰寫中介軟體時,可能需要這些作業的程式碼。 在撰寫中介軟體之外,通常不需要自訂程式碼,因為作業是由 MVC 和 Razor Pages 處理。

有要求和回應主體的兩個抽象概念:StreamPipe。 針對要求讀取,HttpRequest.BodyStream,而 HttpRequest.BodyReaderPipeReader。 針對回應寫入,HttpResponse.BodyStream,而 HttpResponse.BodyWriterPipeWriter

建議透過串流使用管線。 資料流可以更容易地用於一些簡單的作業,但管線具有效能優勢,並且更容易在大部分情況下使用。 ASP.NET Core 正在啟動,以在內部使用管線,而非使用資料流。 範例包含:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

串流不會從架構中移除。 串流會繼續在整個 .NET 中使用,且許多資料流類型沒有管道對等項目,例如 FileStreamsResponseCompression

資料流範例

假設目標是建立一個以字串清單 (以新行區隔) 方式,讀取整個要求本文的中介軟體。 簡單的資料流實作看起來可能像下列範例這樣:

警告

下列程式碼範例:

  • 用於示範不使用管道讀取要求本文的問題。
  • 不適合用於生產應用程式。
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"));
}

如果您想要查看翻譯為英文以外語言的程式碼註解,請在此 GitHub 討論問題中告訴我們。

此程式碼確實有用,但有一些問題:

  • 附加到 StringBuilder 之前,此範例會建立另一個會立即棄置的字串 (encodedString)。 系統會對資料流中的所有位元組執行此處理序,因此整個要求本文會額外耗用記憶體。
  • 此範例會在於新行上進行分割之前讀取整個字串。 檢查位元組陣列中的新行比較有效率。

以下是修正上述一些問題的範例:

警告

下列程式碼範例:

  • 用於示範上述程式碼中某些問題的解決方案,然而無法解決所有問題。
  • 不適合用於生產應用程式。
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;
}

上述範例:

  • 不會針對 StringBuilder 中的整個要求本文進行緩衝處理,除非沒有任何新行字元。
  • 不會在字串上呼叫 Split

不過,仍有一些問題:

  • 如果新行字元是疏鬆的,大部分要求本文都會在字串中進行緩衝處理。
  • 程式碼會繼續建立字串 (remainingString) 並將其新增至字串緩衝區,這會導致額外的配置。

這些問題是可以修正的,但程式碼變得越來越複雜,而且改善幅度很有限。 管線提供解決這些問題的方法,且具有最低的程式碼複雜度。

管線

下列範例示範如何使用 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));
}

此範例可修正資料流實作的許多問題:

  • 這並不需要字串緩衝區,因為 PipeReader 會處理未使用的位元組。
  • 編碼的字串會直接新增至所傳回字串的清單。
  • 除了 ToArray 呼叫和字串所使用的記憶體之外,系統不會配置資源給字串建立作業。

配接器

BodyBodyReaderBodyWriter 屬性適用於 HttpRequestHttpResponse。 當您將 Body 設定為不同的串流時,有一組新的配接器可自動將每種類型與其他類型配接。 如果將 HttpRequest.Body 設定至新的資料流,HttpRequest.BodyReader 會自動設為包裝 HttpRequest.Body 的新 PipeReader

StartAsync

HttpResponse.StartAsync 用於指出不可修改標題,以及執行 OnStarting 回呼。 使用 Kestrel 作為伺服器時,在使用 PipeReader 前先呼叫 StartAsync 可保證 GetMemory 傳回的記憶體屬於 Kestrel 的內部 Pipe,而不是外部緩衝區。

其他資源