Các thao tác Request và Response trong ASP.NET Core
Bởi Justin Kotalik
Bài viết này giải thích cách đọc từ request body (thân yêu cầu) và ghi vào response body (thân phản hồi). Code cho các thao tác này có thể cần thiết khi viết middleware (phần mềm trung gian). Ngoài việc viết middleware, code tùy chỉnh thường không cần thiết vì các thao tác được xử lý bởi MVC và Razor Pages.
Có hai abstraction (trừu tượng hóa) cho request body và response body: Stream và Pipe. Để đọc request, HttpRequest.Body là một Stream, và HttpRequest.BodyReader là một PipeReader. Để ghi response, HttpResponse.Body là một Stream, và HttpResponse.BodyWriter là một PipeWriter.
Pipelines được khuyến nghị hơn streams. Streams có thể dễ sử dụng hơn cho một số thao tác đơn giản, nhưng pipelines có lợi thế về hiệu suất và dễ sử dụng hơn trong hầu hết các tình huống. ASP.NET Core đang bắt đầu sử dụng pipelines thay vì streams trong nội bộ. Ví dụ bao gồm:
FormReaderTextReaderTextWriterHttpResponse.WriteAsync
Streams không bị loại bỏ khỏi framework. Streams tiếp tục được sử dụng trong .NET:
- Nhiều loại stream không có tương đương với pipe, chẳng hạn như
FileStreamsvàResponseCompression. - Việc thêm nén vào stream rất đơn giản.
Ví dụ với Stream
Giả sử mục tiêu là tạo một middleware đọc toàn bộ request body dưới dạng danh sách chuỗi, tách theo dòng mới. Một cài đặt stream đơn giản có thể trông như ví dụ sau:
Code sau: - Được dùng để minh họa các vấn đề khi không dùng pipe để đọc request body. - Không nhằm mục đích sử dụng trong ứng dụng production.
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"));
}Code này hoạt động, nhưng có một số vấn đề:
- Trước khi append vào
StringBuilder, ví dụ tạo thêm một chuỗi khác (encodedString) bị loại bỏ ngay lập tức. Quá trình này xảy ra cho tất cả bytes trong stream, vì vậy kết quả là phân bổ bộ nhớ thêm bằng kích thước của toàn bộ request body. - Ví dụ đọc toàn bộ chuỗi trước khi tách theo dòng mới. Việc kiểm tra ký tự dòng mới trong mảng byte sẽ hiệu quả hơn.
Đây là một ví dụ sửa một số vấn đề trước:
Code sau: - Được dùng để minh họa giải pháp cho một số vấn đề trong code trước trong khi không giải quyết tất cả các vấn đề. - Không nhằm mục đích sử dụng trong ứng dụng production.
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;
}Ví dụ trước:
- Không buffer toàn bộ request body trong
StringBuildertrừ khi không có ký tự dòng mới. - Không gọi
Splittrên chuỗi.
Tuy nhiên, vẫn còn một số vấn đề:
- Nếu ký tự dòng mới hiếm gặp, phần lớn request body được buffer trong chuỗi.
- Code tiếp tục tạo chuỗi (
remainingString) và thêm chúng vào buffer chuỗi, dẫn đến phân bổ thêm.
Các vấn đề này có thể sửa được, nhưng code ngày càng phức tạp hơn với ít cải thiện. Pipelines cung cấp cách giải quyết những vấn đề này với độ phức tạp code tối thiểu.
Pipelines
Ví dụ sau cho thấy cách tình huống stream trước có thể được xử lý bằng 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));
}Ví dụ này sửa nhiều vấn đề mà các cài đặt stream gặp phải:
- Không cần buffer chuỗi vì
PipeReaderxử lý các bytes chưa được sử dụng. - Chuỗi đã mã hóa được thêm trực tiếp vào danh sách chuỗi trả về.
- Ngoài lời gọi
ToArray, và bộ nhớ được sử dụng bởi chuỗi, việc tạo chuỗi không tốn phân bổ.
Khi ghi trực tiếp vào HttpResponse.BodyWriter, hãy gọi PipeWriter.FlushAsync thủ công để đảm bảo dữ liệu được flush vào response body bên dưới. Đây là lý do:
HttpResponse.BodyWriterlà mộtPipeWriterbuffer dữ liệu cho đến khi một thao tác flush được kích hoạt.- Gọi
FlushAsyncghi dữ liệu đã buffer vào response body bên dưới.
Developer tự quyết định khi nào gọi FlushAsync, cân bằng các yếu tố như kích thước buffer, overhead ghi mạng, và liệu dữ liệu có nên được gửi theo từng khối riêng biệt. Để biết thêm thông tin, xem System.IO.Pipelines in .NET.
Adapters (Bộ chuyển đổi)
Các thuộc tính Body, BodyReader, và BodyWriter có sẵn cho HttpRequest và HttpResponse. Khi bạn đặt Body thành một stream khác, một tập hợp adapters mới tự động thích ứng mỗi loại với các loại khác. Nếu bạn đặt HttpRequest.Body thành một stream mới, HttpRequest.BodyReader tự động được đặt thành một PipeReader mới bao gồm HttpRequest.Body.
StartAsync
HttpResponse.StartAsync được dùng để chỉ ra rằng headers không thể sửa đổi và chạy các callbacks OnStarting. Khi sử dụng Kestrel làm server, gọi StartAsync trước khi sử dụng PipeReader đảm bảo rằng bộ nhớ được trả về bởi GetMemory thuộc về Pipe nội bộ của Kestrel thay vì một buffer bên ngoài.