Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 10 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la stratégie de support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 10 de cet article.
Par Justin Kotalik
Cet article explique comment lire à partir du corps de la requête et écrire dans le corps de la réponse. Le code pour ces opérations peut être requis lors de l’écriture d’intergiciels. En dehors de l’écriture d’intergiciels, le code personnalisé n’est généralement pas requis, car les opérations sont gérées par MVC et Razor Pages.
Il existe deux abstractions pour les corps de la requête et de la réponse : Stream et Pipe. Pour la lecture des requêtes, HttpRequest.Body est un Streamet HttpRequest.BodyReader est un PipeReader. Pour l’écriture de réponses, HttpResponse.Body est un Streamet HttpResponse.BodyWriter est un PipeWriter.
Les pipelines sont recommandés par rapport aux flux. Les flux peuvent être plus faciles à utiliser pour des opérations simples, mais les pipelines présentent un avantage de performances et sont plus faciles à utiliser dans la plupart des scénarios. ASP.NET Core commence à utiliser des pipelines au lieu de flux en interne. Voici quelques exemples :
FormReaderTextReaderTextWriterHttpResponse.WriteAsync
Les flux ne sont pas supprimés du cadre. Les flux continuent d’être utilisés dans .NET :
- De nombreux types de flux n’ont pas d’équivalents de canal, tels que
FileStreamsetResponseCompression. - Il est simple d’ajouter la compression à un flux.
Exemples de flux
Supposons que votre objectif est de créer un intergiciel qui lit le corps de la requête dans sa totalité comme une liste de chaînes, avec un fractionnement sur les nouvelles lignes. Une implémentation simple de flux peut se présenter comme dans l’exemple suivant :
Avertissement
Le code suivant :
- Est utilisé pour illustrer les problèmes causés par le fait de ne pas utiliser un canal pour lire le corps de la requête.
- N’est pas destiné à être utilisé dans les applications de 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"));
}
Ce code fonctionne, mais il existe certains problèmes :
- Avant d’ajouter à
StringBuilder, l’exemple crée une autre chaîne (encodedString) qui est immédiatement rejetée. Ce processus se produit pour tous les octets dans le flux, il en résulte une allocation de mémoire supplémentaire de la taille de la totalité du corps de la demande. - L’exemple lit la chaîne entière avant de la découper aux nouvelles lignes. Il est plus efficace de vérifier les nouvelles lignes dans le tableau d’octets.
Voici un exemple qui résout certains des problèmes précédents :
Avertissement
Le code suivant :
- Est utilisé pour illustrer les solutions à certains problèmes dans le code précédent sans résoudre tous les problèmes.
- N’est pas destiné à être utilisé dans les applications de 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;
}
Cet exemple précédent :
- Ne met pas le corps entier de la requête en mémoire tampon dans un
StringBuilder, sauf s'il n'y a aucun caractère de nouvelle ligne. - N’appelle pas
Splitsur la chaîne de caractères.
Toutefois, il existe toujours quelques problèmes :
- Si les caractères nouvelle-ligne sont épars, une grande partie du corps de la requête est mise en mémoire tampon dans une chaîne.
- Le code continue de créer des chaînes (
remainingString) et les ajoute à la mémoire tampon de chaîne, ce qui entraîne une allocation supplémentaire.
Ces problèmes sont réparables, mais le code est de plus en plus compliqué avec peu d’amélioration. Les pipelines permettent de résoudre ces problèmes avec un code peu compliqué.
Conduites
L’exemple suivant montre comment gérer le scénario de flux précédent à l’aide d’un 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));
}
Cet exemple résout de nombreux problèmes trouvés dans les implémentations de flux :
- Une mémoire tampon de chaîne est inutile, car le
PipeReadergère des octets qui n’ont pas été utilisés. - Les chaînes codées sont ajoutées directement à la liste des chaînes retournées.
- À l’exception de l’appel
ToArray, et de la mémoire utilisée par la chaîne, la création de chaînes ne nécessite pas d'allocation.
Lorsque vous écrivez directement à HttpResponse.BodyWriter, appelez PipeWriter.FlushAsync manuellement pour vous assurer que les données sont vidées dans le corps de la réponse. Voici pourquoi :
-
HttpResponse.BodyWriterest unPipeWritercomposant qui met en mémoire tampon les données jusqu’à ce qu’une opération de purge soit déclenchée. - L’appel
FlushAsyncécrit les données mises en mémoire tampon dans le corps de la réponse sous-jacent.
Il appartient au développeur de décider quand appeler FlushAsync, d’équilibrer les facteurs tels que la taille de la mémoire tampon, la surcharge d’écriture réseau et si les données doivent être envoyées en blocs discrets. Pour plus d’informations, consultez System.IO.Pipelines dans .NET.
Adaptateurs
Les propriétés Body, BodyReaderet BodyWriter sont disponibles pour HttpRequest et HttpResponse. Lorsque vous définissez Body sur un autre flux, un nouvel ensemble d’adaptateurs adapte automatiquement chaque type à l’autre. Si vous définissez HttpRequest.Body sur un nouveau flux, HttpRequest.BodyReader est automatiquement défini sur un nouveau PipeReader qui enveloppe HttpRequest.Body.
StartAsync
HttpResponse.StartAsync est utilisé pour indiquer que les en-têtes sont non modifiables et pour exécuter des rappels OnStarting. Lors de l’utilisation de Kestrel en tant que serveur, l’appel de StartAsync avant d’utiliser le PipeReader garantit que la mémoire retournée par GetMemory appartiendra au Kestrel interne de Pipe au lieu d’une mémoire tampon externe.