ASP.NET Core 中的要求和回應作業
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
此文章說明如何讀取要求本文及寫入回應本文。 撰寫中介軟體時,可能需要這些作業的程式碼。 在撰寫中介軟體之外,通常不需要自訂程式碼,因為作業是由 MVC 和 Razor Pages 處理。
有要求和回應主體的兩個抽象概念:Stream 和 Pipe。 針對要求讀取,HttpRequest.Body 是 Stream,而 HttpRequest.BodyReader
是 PipeReader。 針對回應寫入,HttpResponse.Body 是 Stream,而 HttpResponse.BodyWriter
是 PipeWriter。
建議透過串流使用管線。 資料流可以更容易地用於一些簡單的作業,但管線具有效能優勢,並且更容易在大部分情況下使用。 ASP.NET Core 正在啟動,以在內部使用管線,而非使用資料流。 範例包含:
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
串流不會從架構中移除。 串流會繼續在整個 .NET 中使用,且許多資料流類型沒有管道對等項目,例如 FileStreams
和 ResponseCompression
。
資料流範例
假設目標是建立一個以字串清單 (以新行區隔) 方式,讀取整個要求本文的中介軟體。 簡單的資料流實作看起來可能像下列範例這樣:
警告
下列程式碼範例:
- 用於示範不使用管道讀取要求本文的問題。
- 不適合用於生產應用程式。
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
呼叫和字串所使用的記憶體之外,系統不會配置資源給字串建立作業。
配接器
Body
、BodyReader
和 BodyWriter
屬性適用於 HttpRequest
和 HttpResponse
。 當您將 Body
設定為不同的串流時,有一組新的配接器可自動將每種類型與其他類型配接。 如果將 HttpRequest.Body
設定至新的資料流,HttpRequest.BodyReader
會自動設為包裝 HttpRequest.Body
的新 PipeReader
。
StartAsync
HttpResponse.StartAsync
用於指出不可修改標題,以及執行 OnStarting
回呼。 使用 Kestrel 作為伺服器時,在使用 PipeReader
前先呼叫 StartAsync
可保證 GetMemory
傳回的記憶體屬於 Kestrel 的內部 Pipe,而不是外部緩衝區。