Operasi permintaan dan respons di ASP.NET Core
Catatan
Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Peringatan
Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Penting
Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.
Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Oleh Justin Kotalik
Artikel ini menjelaskan cara membaca dari isi permintaan dan menulis ke isi respons. Kode untuk operasi ini mungkin diperlukan saat menulis middleware. Di luar penulisan middleware, kode kustom umumnya tidak diperlukan karena operasi ditangani oleh MVC dan Razor Pages.
Ada dua abstraksi untuk badan permintaan dan respons: Stream dan Pipe. Untuk pembacaan permintaan, HttpRequest.Body adalah Stream, dan HttpRequest.BodyReader
merupakan PipeReader. Untuk penulisan respons, HttpResponse.Body adalah Stream, dan HttpResponse.BodyWriter
merupakan PipeWriter.
Alur direkomendasikan melalui aliran. Aliran dapat lebih mudah digunakan untuk beberapa operasi sederhana, tetapi alur memiliki keunggulan performa dan lebih mudah digunakan dalam sebagian besar skenario. ASP.NET Core mulai menggunakan alur alih-alih streaming secara internal. Contohnya meliputi:
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
Aliran tidak dihapus dari kerangka kerja. Aliran terus digunakan di seluruh .NET, dan banyak jenis aliran tidak memiliki setara pipa, seperti FileStreams
dan ResponseCompression
.
Contoh aliran
Misalkan tujuannya adalah untuk membuat middleware yang membaca seluruh isi permintaan sebagai daftar string, memisahkan pada baris baru. Implementasi aliran sederhana mungkin terlihat seperti contoh berikut:
Peringatan
Kode berikut:
- Digunakan untuk menunjukkan masalah dengan tidak menggunakan pipa untuk membaca isi permintaan.
- Tidak dimaksudkan untuk digunakan dalam aplikasi produksi.
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"));
}
Jika Anda ingin melihat komentar kode yang diterjemahkan ke bahasa selain bahasa Inggris, beri tahu kami dalam masalah diskusi GitHub ini.
Kode ini berfungsi, tetapi ada beberapa masalah:
- Sebelum menambahkan ke
StringBuilder
, contoh membuat string lain (encodedString
) yang segera dibuang. Proses ini terjadi untuk semua byte dalam aliran, sehingga hasilnya adalah alokasi memori ekstra ukuran seluruh isi permintaan. - Contoh membaca seluruh string sebelum memisahkan pada baris baru. Lebih efisien untuk memeriksa baris baru dalam array byte.
Berikut adalah contoh yang memperbaiki beberapa masalah sebelumnya:
Peringatan
Kode berikut:
- Digunakan untuk menunjukkan solusi untuk beberapa masalah dalam kode sebelumnya sambil tidak menyelesaikan semua masalah.
- Tidak dimaksudkan untuk digunakan dalam aplikasi produksi.
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;
}
Contoh sebelumnya ini:
- Tidak buffer seluruh isi permintaan dalam
StringBuilder
kecuali tidak ada karakter baris baru. - Tidak memanggil
Split
string.
Namun, masih ada beberapa masalah:
- Jika karakter baris baru jarang, sebagian besar isi permintaan di-buffer dalam string.
- Kode terus membuat string (
remainingString
) dan menambahkannya ke buffer string, yang menghasilkan alokasi tambahan.
Masalah ini dapat diperbaiki, tetapi kode menjadi semakin rumit dengan sedikit peningkatan. Alur menyediakan cara untuk menyelesaikan masalah ini dengan kompleksitas kode minimal.
Pipelines
Contoh berikut menunjukkan bagaimana skenario yang sama dapat ditangani menggunakan 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));
}
Contoh ini memperbaiki banyak masalah yang dimiliki implementasi stream:
- Tidak perlu buffer string karena
PipeReader
byte handel yang belum digunakan. - String yang dikodekan langsung ditambahkan ke daftar string yang dikembalikan.
ToArray
Selain panggilan, dan memori yang digunakan oleh string, pembuatan string bebas alokasi.
Adapter
Properti Body
, BodyReader
, dan BodyWriter
tersedia untuk HttpRequest
dan HttpResponse
. Saat Anda mengatur Body
ke aliran yang berbeda, set adaptor baru secara otomatis menyesuaikan setiap jenis dengan jenis lainnya. Jika Anda mengatur HttpRequest.Body
ke aliran baru, HttpRequest.BodyReader
secara otomatis diatur ke yang baru PipeReader
yang membungkus HttpRequest.Body
.
StartAsync
HttpResponse.StartAsync
digunakan untuk menunjukkan bahwa header tidak dapat dimodifikasi dan untuk menjalankan OnStarting
panggilan balik. Saat menggunakan Kestrel sebagai server, memanggil StartAsync
sebelum menggunakan PipeReader
jaminan bahwa memori yang dikembalikan oleh GetMemory
milik Kestrel's internal Pipe daripada buffer eksternal.
Sumber Daya Tambahan:
ASP.NET Core