ASP.NET Core의 요청 및 응답 작업
참고 항목
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
Important
이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.
현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
작성자 Justin Kotalik
이 문서에서는 요청 본문을 읽고 응답 본문을 쓰는 방법을 설명합니다. 미들웨어를 작성할 때 이러한 작업에 대한 코드가 필요할 수 있습니다. 미들웨어 작성 외에, MVC 및 Razor Pages에서 작업이 처리되기 때문에 사용자 지정 코드는 일반적으로 필요하지 않습니다.
요청 및 응답 본문에 대한 두 가지 추상(Stream 및 Pipe)이 있습니다. 요청 읽기의 경우 HttpRequest.Body은 Stream이고 HttpRequest.BodyReader
는 PipeReader입니다. 응답 작성의 경우 HttpResponse.Body은 Stream이고 HttpResponse.BodyWriter
는 PipeWriter입니다.
스트림보다 Pipelines를 사용하는 것이 좋습니다. 일부 간단한 작업에서는 스트림이 더 편리할 수도 있지만, 파이프라인은 성능상의 장점이 있고 대부분의 시나리오에서 더 편리합니다. ASP.NET Core는 내부적으로 스트림 대신 파이프라인을 사용하기 시작했습니다. 예를 들면 다음과 같습니다.
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
스트림이 프레임워크에서 제거되지 않습니다. 스트림은 .NET 전체에서 계속 사용되며, FileStreams
및 ResponseCompression
과 같이 상응하는 파이프 항목이 없는 스트림 형식도 많습니다.
스트림 예제
새 줄에서 분할하여 전체 요청 본문을 문자열 목록으로 읽는 미들웨어를 만들려 한다고 가정해 보겠습니다. 단순 스트림 구현은 다음 예제와 같이 표시될 수 있습니다.
Warning
코드는 다음과 같습니다.
- 파이프를 사용하지 않고 요청 본문을 읽을 때 발생하는 문제를 보여 주는 데 사용됩니다.
- 프로덕션 앱에서 사용하기 위한 것이 아닙니다.
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
)이 생성됩니다. 이 프로세스는 스트림의 모든 바이트에 대해 발생하므로 결과는 전체 요청 본문의 크기만큼 추가 메모리가 할당됩니다. - 예제에서는 새 줄에서 분할하기 전에 전체 문자열을 읽습니다. 바이트 배열에서 새 줄을 확인하는 것이 더 효율적입니다.
앞의 문제 중 일부를 해결한 예제는 다음과 같습니다.
Warning
코드는 다음과 같습니다.
- 위 코드의 모든 문제를 해결하는 것이 아니라 일부 문제에 대한 해결을 보여 주는 데 사용됩니다.
- 프로덕션 앱에서 사용하기 위한 것이 아닙니다.
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
)을 계속 만들어 문자열 버퍼에 추가하므로 추가 할당이 발생합니다.
이러한 문제는 해결할 수 있지만, 코드가 별다른 개선 사항 없이 점점 더 복잡해집니다. 파이프라인은 코드 복잡성을 최소화하여 이러한 문제를 해결하는 방법을 제공합니다.
Pipelines
다음 예제에서는 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에 속하게 됩니다.
추가 리소스
ASP.NET Core