本文介绍如何读取请求正文和写入响应正文。 写入中间件时,可能需要这些操作的代码。 除写入中间件外,通常不需要自定义代码,因为操作由 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")); }
此代码有效,但存在一些问题:
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) { 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'); if (index == -1) { break; } var encodedString = Encoding.UTF8.GetString(buffer, prevIndex, index); 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, index, bytesRemaining - index); 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(ref results, in readOnlySequence); // Skip the line + the \n character (basically position) buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); } } while (position != null); // At this point, buffer will be updated to point one byte after the last // \n character. reader.AdvanceTo(buffer.Start, buffer.End); if (readResult.IsCompleted) { break; } } return results; } private static void AddStringToList(ref 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
。
HttpResponse.StartAsync
用于指示标头不可修改并运行 OnStarting
回调。 使用 Kestrel 作为服务器时,在使用 PipeReader
之前调用 StartAsync
可确保 GetMemory
返回的内存属于 Kestrel 的内部 Pipe 而不是外部缓冲区。