共用方式為


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

注意

這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前的版本,請參閱 本文的 .NET 9 版本

賈斯汀·科塔利克

此文章說明如何讀取要求本文及寫入回應本文。 撰寫中介軟體時,可能需要這些作業的程式碼。 在撰寫中介軟體之外,通常不需要自訂程式碼,因為作業是由 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 呼叫和字串所使用的記憶體之外,系統不會配置資源給字串建立作業。

直接將資料寫入HttpResponse.BodyWriter 時,請手動呼叫 PipeWriter.FlushAsync ,以確保資料已排除至基礎回應的主體。 原因如下:

  • HttpResponse.BodyWriterPipeWriter,用來緩衝數據,直到觸發排清作業為止。
  • 呼叫 FlushAsync 會將緩衝的數據寫入基礎響應主體。

開發人員決定何時呼叫 FlushAsync、平衡緩衝區大小、網路寫入額外負荷等因素,以及數據是否應該以離散區塊傳送。 如需詳細資訊,請參閱 .NET 中的 System.IO.Pipelines

配接器

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

StartAsync

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

其他資源