récemment, je suis tombé sur cet article de blog de monstres asp.net qui parle de problèmes avec l'utilisation de HttpClient
de la manière suivante:
using(var client = new HttpClient())
{
}
Selon l'article de blog, si nous supprimons le HttpClient
après chaque demande, il peut garder les connexions TCP ouvertes. Cela peut potentiellement conduire à System.Net.Sockets.SocketException
.
La bonne façon selon la publication est de créer une seule instance de HttpClient
car cela aide à réduire le gaspillage de sockets.
De la poste:
Si nous partageons une seule instance de HttpClient, nous pouvons réduire le gaspillage de sockets en les réutilisant:
namespace ConsoleApplication { public class Program { private static HttpClient Client = new HttpClient(); public static void Main(string[] args) { Console.WriteLine("Starting connections"); for(int i = 0; i<10; i++) { var result = Client.GetAsync("http://aspnetmonsters.com").Result; Console.WriteLine(result.StatusCode); } Console.WriteLine("Connections done"); Console.ReadLine(); } } }
J'ai toujours supprimé HttpClient
objet après l'avoir utilisé car je pensais que c'était la meilleure façon de l'utiliser. Mais ce billet de blog me fait maintenant sentir que je me trompais depuis si longtemps.
Devrions-nous créer une nouvelle instance unique de HttpClient
pour toutes les demandes? Y a-t-il des pièges à utiliser une instance statique?
Cela ressemble à un article de blog convaincant. Cependant, avant de prendre une décision, je ferais d'abord les mêmes tests que le rédacteur du blog, mais sur votre propre code. J'essaierais également d'en savoir un peu plus sur HttpClient et son comportement.
Ce message déclare:
Une instance HttpClient est une collection de paramètres appliqués à toutes les demandes exécutées par cette instance. En outre, chaque instance HttpClient utilise son propre pool de connexions, isolant ses demandes des demandes exécutées par d'autres instances HttpClient.
Donc, ce qui se passe probablement quand un HttpClient est partagé, c'est que les connexions sont réutilisées, ce qui est bien si vous n'avez pas besoin de connexions persistantes. La seule façon de savoir avec certitude si cela est important pour votre situation est d'exécuter vos propres tests de performances.
Si vous creusez, vous trouverez plusieurs autres ressources qui traitent de ce problème (y compris un article Microsoft Best Practices), donc c'est probablement une bonne idée de l'implémenter de toute façon (avec quelques précautions).
Vous n'utilisez pas Httpclient et cela déstabilise votre logiciel
Singleton HttpClient? Méfiez-vous de ce comportement grave et comment y remédier
Modèles et pratiques Microsoft - Optimisation des performances: instanciation incorrecte
Instance unique de HttpClient réutilisable sur la révision du code
Singleton HttpClient ne respecte pas les modifications DNS (CoreFX)
Conseils généraux pour l'utilisation de HttpClient
Je suis en retard à la fête, mais voici mon parcours d'apprentissage sur ce sujet délicat.
Je veux dire, si la réutilisation de HttpClient est prévue et il est important de le faire , un tel défenseur est mieux documenté dans sa propre documentation API, plutôt que d'être caché dans de nombreux "sujets avancés" , "Performance (anti) pattern" ou d'autres articles de blog. Sinon, comment un nouvel apprenant est-il censé le savoir avant qu'il ne soit trop tard?
À partir de maintenant (mai 2018), le premier résultat de recherche lors de la recherche sur "c # httpclient" pointe vers cette page de référence de l'API sur MSDN , qui ne mentionne pas du tout cette intention. Eh bien, la leçon 1 ici pour les débutants est, cliquez toujours sur le lien "Autres versions" juste après le titre de la page d'aide MSDN, vous y trouverez probablement des liens vers la "version actuelle". Dans ce cas HttpClient, il vous amènera au dernier document contenant ici cette description d'intention .
Je soupçonne que de nombreux développeurs qui ne connaissent pas ce sujet n'ont pas non plus trouvé la bonne page de documentation, c'est pourquoi cette connaissance n'est pas largement diffusée, et les gens ont été surpris quand ils l'ont découvert plus tard , peut-être durement .
using
IDisposable
Celui-ci est légèrement hors sujet mais mérite d'être souligné que ce n'est pas une coïncidence de voir des gens dans ces articles de blog susmentionnés blâmer la façon dont l'interface HttpClient
de IDisposable
les fait avoir tendance à utiliser le using (var client = new HttpClient()) {...}
modèle et ensuite conduire au problème.
Je crois que cela se résume à une conception (inexacte?) Tacite: "un objet IDisposable devrait être de courte durée" .
CEPENDANT, alors que cela ressemble certainement à une chose de courte durée lorsque nous écrivons du code dans ce style:
using (var foo = new SomeDisposableObject())
{
...
}
les documentation officielle sur IDisposable ne mentionne jamais IDisposable
les objets doivent être de courte durée. Par définition, IDisposable est simplement un mécanisme qui vous permet de libérer des ressources non gérées. Rien de plus. En ce sens, vous êtes censé déclencher éventuellement la cession, mais cela ne vous oblige pas à le faire de manière éphémère.
Il vous appartient donc de bien choisir le moment du déclenchement de la mise au rebut, en fonction des exigences du cycle de vie de votre objet réel. Rien ne vous empêche d'utiliser un IDisposable de manière durable:
using System;
namespace HelloWorld
{
class Hello
{
static void Main()
{
Console.WriteLine("Hello World!");
using (var client = new HttpClient())
{
for (...) { ... } // A really long loop
// Or you may even somehow start a daemon here
}
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
Avec cette nouvelle compréhension, maintenant nous revisitons ce billet de blog , nous pouvons clairement remarquer que le "correctif" initialise HttpClient
une fois mais ne le supprime jamais, c'est pourquoi nous pouvons voir à partir de son netstat sortie que, la connexion reste à l'état ESTABLISHED ce qui signifie qu'elle n'a PAS été correctement fermée. S'il était fermé, son état serait à la place dans TIME_WAIT. Dans la pratique, ce n'est pas grave de laisser une seule connexion ouverte après la fin de votre programme et l'affiche du blog continue de voir un gain de performances après la correction; mais quand même, il est conceptuellement incorrect de blâmer IDisposable et de choisir de NE PAS le jeter.
Sur la base de la compréhension de la section précédente, je pense que la réponse ici devient claire: "pas nécessairement". Cela dépend vraiment de la façon dont vous organisez votre code, tant que vous réutilisez un HttpClient ET (idéalement) le supprimez éventuellement.
De manière hilarante, même pas l'exemple de la section Remarques du document officiel actuel ne le fait strictement. Il définit une classe "GoodController", contenant une propriété HttpClient statique qui ne sera pas supprimée; qui désobéit à ce qui n autre exemple dans la section Exemples souligne: "besoin d'appeler disposer ... pour que l'application ne fuit pas les ressources".
Et enfin, singleton n'est pas sans ses propres défis.
"Combien de personnes pensent que la variable globale est une bonne idée? Personne.
Combien de personnes pensent que singleton est une bonne idée? Quelques.
Ce qui donne? Les singletons ne sont qu'un tas de variables globales. "
- Cité de cette conférence inspirante, "Global State and Singletons"
Celui-ci n'est pas pertinent pour le Q & A actuel, mais c'est probablement un bon à savoir. Le modèle d'utilisation de SqlConnection est différent. Vous n'avez PAS besoin de réutiliser SqlConnection , car il gérera mieux son pool de connexions de cette façon.
La différence est due à leur approche de mise en œuvre. Chaque instance de HttpClient utilise son propre pool de connexions (cité de ici ); mais SqlConnection lui-même est géré par un pool de connexion central, selon this .
Et vous devez toujours disposer de SqlConnection, comme vous êtes censé le faire pour HttpClient.
J'ai fait quelques tests pour voir des améliorations de performances avec le statique HttpClient
. J'ai utilisé le code ci-dessous pour mes tests:
namespace HttpClientTest
{
using System;
using System.Net.Http;
class Program
{
private static readonly int _connections = 10;
private static readonly HttpClient _httpClient = new HttpClient();
private static void Main()
{
TestHttpClientWithStaticInstance();
TestHttpClientWithUsing();
}
private static void TestHttpClientWithUsing()
{
try
{
for (var i = 0; i < _connections; i++)
{
using (var httpClient = new HttpClient())
{
var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
}
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
private static void TestHttpClientWithStaticInstance()
{
try
{
for (var i = 0; i < _connections; i++)
{
var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
}
}
Pour tester:
J'ai trouvé l'amélioration des performances entre 40% et 60% uon en utilisant HttpClient
statique au lieu de la supprimer pour la demande HttpClient
. J'ai mis les détails du résultat du test de performance dans le blog ici .
Voici un client API de base qui utilise efficacement HttpClient et HttpClientHandler. Lorsque vous créez un nouveau HttpClient pour faire une demande, il y a beaucoup de frais généraux. Ne recréez PAS HttpClient pour chaque demande. Réutilisez HttpClient autant que possible ...
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class MyApiClient : IDisposable
{
private readonly TimeSpan _timeout;
private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler;
private readonly string _baseUrl;
private const string ClientUserAgent = "my-api-client-v1";
private const string MediaTypeJson = "application/json";
public MyApiClient(string baseUrl, TimeSpan? timeout = null)
{
_baseUrl = NormalizeBaseUrl(baseUrl);
_timeout = timeout ?? TimeSpan.FromSeconds(90);
}
public async Task<string> PostAsync(string url, object input)
{
EnsureHttpClientCreated();
using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
{
using (var response = await _httpClient.PostAsync(url, requestContent))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
}
public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
{
var strResponse = await PostAsync(url, input);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
{
var strResponse = await GetAsync(url);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<string> GetAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.GetAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> PutAsync(string url, object input)
{
return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
}
public async Task<string> PutAsync(string url, HttpContent content)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.PutAsync(url, content))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> DeleteAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.DeleteAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public void Dispose()
{
_httpClientHandler?.Dispose();
_httpClient?.Dispose();
}
private void CreateHttpClient()
{
_httpClientHandler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
};
_httpClient = new HttpClient(_httpClientHandler, false)
{
Timeout = _timeout
};
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);
if (!string.IsNullOrWhiteSpace(_baseUrl))
{
_httpClient.BaseAddress = new Uri(_baseUrl);
}
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
}
private void EnsureHttpClientCreated()
{
if (_httpClient == null)
{
CreateHttpClient();
}
}
private static string ConvertToJsonString(object obj)
{
if (obj == null)
{
return string.Empty;
}
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
private static string NormalizeBaseUrl(string url)
{
return url.EndsWith("/") ? url : url + "/";
}
}
Usage:
using (var client = new MyApiClient("http://localhost:8080"))
{
var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
Pour correctement fermer la connexion TCP , nous devons terminer une séquence de paquets FIN - FIN + ACK - ACK (tout comme SYN - SYN + ACK - ACK, lorsque - ouverture d'une connexion TCP ). Si nous appelons simplement une méthode .Close () (cela se produit généralement lorsqu'un HttpClient est en train de supprimer), et nous n'attendez pas que le côté distant confirme notre demande de fermeture (avec FIN + ACK), nous nous retrouvons avec l'état TIME_WAIT sur le port local TCP, car nous avons supprimé notre écouteur (HttpClient) et nous n'avons jamais eu la chance de réinitialiser l'état du port à un état fermé correct, une fois que le pair distant nous a envoyé le paquet FIN + ACK.
La bonne façon de fermer la connexion TCP serait d'appeler la méthode .Close () et d'attendre que l'événement de fermeture de l'autre côté (FIN + ACK) arrive de notre côté. Seulement alors nous pouvons envoyer notre ACK final et disposer du HttpClient.
Juste pour ajouter, il est logique de garder les connexions TCP ouvertes, si vous effectuez des requêtes HTTP, en raison de l'en-tête HTTP "Connection: Keep-Alive". De plus, vous pouvez demander à la télécommande homologue pour fermer la connexion à votre place, en définissant l'en-tête HTTP "Connection: Close". De cette façon, vos ports locaux seront toujours correctement fermés, au lieu d'être dans un état TIME_WAIT.