Devrions-nous créer une nouvelle instance unique de HttpClient pour toutes les demandes?

58

récemment, je suis tombé sur ce post de blog de monstres asp.net qui parle de problèmes liés à l'utilisation HttpClientde la manière suivante:

using(var client = new HttpClient())
{
}

Selon l'article du blog, si nous disposons HttpClientaprès chaque demande, les connexions TCP peuvent rester ouvertes. Cela peut potentiellement conduire à System.Net.Sockets.SocketException.

La bonne manière, comme dans le post, est de créer une instance unique HttpClientcar 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 disposé d'un HttpClientobjet après l'avoir utilisé car je sentais que c'était la meilleure façon de l'utiliser. Mais cet article de blog me fait maintenant sentir que je le faisais mal tout ce temps.

Devrions-nous créer une nouvelle instance unique HttpClientpour toutes les demandes? Y a-t-il des pièges à utiliser une instance statique?

Ankit Vijay
la source
Avez-vous rencontré des problèmes que vous avez attribués à la façon dont vous les utilisez?
Whatsisname
Peut-être vérifier cette réponse et aussi ceci .
John Wu
@whatsisname no je ne l'ai pas fait, mais en regardant le blog, j'ai eu le sentiment que je pouvais utiliser ce mauvais tout le temps. Par conséquent, je voulais comprendre de la part des autres développeurs s’ils voyaient un problème dans l’une ou l’autre approche.
Ankit Vijay
3
Je ne l'ai pas essayé moi-même (je ne donne donc pas cette réponse), mais d'après Microsoft, à partir de .NET Core 2.1, vous êtes censé utiliser HttpClientFactory comme décrit dans docs.microsoft.com/en-us/dotnet/standard/. …
Joeri Sebrechts
(Comme il est dit dans ma réponse, je voulais juste pour le rendre plus visible, donc j'écris un bref commentaire.) Instance statique gère correctement la poignée de main de fermeture de connexion tcp, une fois que vous faites Close()ou lancer un nouveau Get(). Si vous ne vous débarrassez que du client une fois que vous en avez terminé, il n'y aura plus personne pour gérer cette poignée de main de fermeture et vos ports auront tous l'état TIME_WAIT, à cause de cela.
Mladen B.

Réponses:

40

Cela semble être un article de blog convaincant. Cependant, avant de prendre une décision, je commencerais par exécuter les mêmes tests que l'auteur du blog, mais sur votre propre code. Je voudrais aussi essayer d'en savoir un peu plus sur HttpClient et son comportement.

Ce post dit:

Une instance HttpClient est un ensemble 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 lorsqu'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 pour vous de savoir avec certitude si cela est important pour votre situation est d'exécuter vos propres tests de performance.

Si vous creusez, vous trouverez plusieurs autres ressources qui traitent de ce problème (y compris un article Microsoft Best Practices), il est donc probablement préférable de l'implémenter de toute façon (avec quelques précautions).

Références

Vous utilisez mal le client et il déstabilise votre logiciel
Singleton HttpClient? Méfiez-vous de ce comportement sérieux et du moyen de le corriger.
Modèles et pratiques Microsoft - Optimisation des performances: instanciation incorrecte
Instance unique de HttpClient réutilisable lors de la révision du code
Singleton HttpClient ne respecte pas les modifications DNS (CoreFX)
Conseils généraux d'utilisation de HttpClient

Robert Harvey
la source
1
C'est une bonne liste exhaustive. C'est mon week-end à lire.
Ankit Vijay
"Si vous creusez, vous trouverez plusieurs autres ressources qui traitent ce problème ..." vous voulez dire le problème de la connexion TCP ouverte?
Ankit Vijay
Réponse courte: utilisez un HttpClient statique . Si vous devez prendre en charge les modifications DNS (de votre serveur Web ou d’autres serveurs), vous devez vous préoccuper des paramètres de délai.
Jess
3
Cela prouve à quel point HttpClient est dérouté que l'utiliser est une "lecture de fin de semaine", commentée par @AnkitVijay.
Usr
@Jess en plus des changements de DNS - envoyer tout le trafic de votre client à travers un seul socket va également gâcher l'équilibrage de la charge?
Iain
16

Je suis en retard à la fête, mais voici mon parcours d’apprentissage sur ce sujet épineux.

1. Où pouvons-nous trouver le défenseur officiel de la réutilisation de HttpClient?

Je veux dire, si la réutilisation de HttpClient est prévue et qu'il est important de le faire , un tel avocat est mieux documenté dans sa propre documentation d'API, plutôt que d'être caché dans de nombreux "Sujets avancés", "Performances (anti) modèles" ou autres blogs. . Sinon, comment un nouvel apprenant est-il censé le savoir avant qu'il ne soit trop tard?

Dès à présent (mai 2018), le premier résultat de recherche lorsque Google "c # httpclient" pointe vers cette page de référence d'API sur MSDN , ce qui ne mentionne absolument pas cette intention. Pour les débutants, la première leçon est de toujours cliquer sur le lien "Autres versions" juste après le titre de la page d’aide de MSDN. Vous y trouverez probablement des liens vers la "version actuelle". Dans ce cas HttpClient, cela vous mènera au dernier document contenant cette description d'intention .

J'imagine que de nombreux développeurs novices dans ce sujet n'ont pas trouvé la bonne page de documentation. C'est pourquoi ces connaissances ne sont pas très répandues et les gens ont été surpris d'apprendre plus tard , peut-être de manière difficile .

2. La (mis?) Conception de using IDisposable

Celui-ci est un peu hors sujet, mais mérite néanmoins d'être souligné, ce n'est pas un hasard si les personnes dans les articles de blog susmentionnés blâment comment HttpClientleur IDisposableinterface les incite à utiliser le using (var client = new HttpClient()) {...}modèle, puis à conduire au problème.

Je crois que cela revient à une conception non dite (mal?): "Un objet identifiable devrait être de courte durée" .

CEPENDANT, même si cela ressemble certainement à une chose de courte durée lorsque nous écrivons du code dans ce style:

using (var foo = new SomeDisposableObject())
{
    ...
}

la documentation officielle sur IDisposable ne mentionne jamais que les IDisposableobjets doivent être de courte durée. Par définition, IDisposable est simplement un mécanisme vous permettant de libérer des ressources non gérées. Rien de plus. En ce sens, vous êtes censé éventuellement déclencher la cession, mais cela ne vous oblige pas à le faire de manière éphémère.

Il est donc de votre devoir de bien choisir quand déclencher la mise au rebut, en vous basant sur les 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 cet article de blog , nous pouvons clairement remarquer que le "correctif" s'initialise HttpClientune fois mais ne le supprime jamais, c'est pourquoi nous pouvons voir dans sa sortie netstat que la connexion reste à l'état ESTABLISHED, ce qui signifie qu'elle PAS été correctement fermé. S'il était fermé, son état serait plutôt dans TIME_WAIT. Dans la pratique, il n’est pas difficile de laisser filtrer une seule connexion ouverte après la fin de votre programme et l’affiche du blog continue d’obtenir un gain de performances après le correctif; mais toujours, il est conceptuellement incorrect de blâmer IDisposable et de choisir de ne PAS le jeter.

3. Devons-nous mettre HttpClient dans une propriété statique, ou même le mettre comme singleton?

Sur la base de la compréhension de la section précédente, je pense que la réponse devient claire: "pas nécessairement". Cela dépend vraiment de la manière dont vous organisez votre code, tant que vous réutilisez un HttpClient ET (idéalement) le supprimez éventuellement.

De manière hilarante, même l'exemple de la section Remarques du document officiel actuel ne le fait pas parfaitement. Il définit une classe "GoodController", contenant une propriété statique HttpClient qui ne sera pas supprimée. qui désobéit à ce qu'un autre exemple de la section Exemples souligne: "il faut appeler Dispose ... pour que l'application ne fuit pas de ressources".

Enfin, singleton n’est pas sans 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 groupe de variables globales. "

- Cité de cette conférence inspirante, "Global State and Singletons"

PS: SqlConnection

Celui-ci est sans rapport avec l'actuel Q & A, 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.

La différence est causée par leur approche de mise en œuvre. Chaque instance HttpClient utilise son propre pool de connexions (cité ci- dessous ); mais SqlConnection lui-même est géré par un pool de connexion central, selon cela .

Et vous devez toujours disposer de SqlConnection, comme vous êtes supposé le faire pour HttpClient.

RayLuo
la source
14

J'ai fait quelques tests, voir des améliorations de performances avec 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 couru le code avec 10, 100, 1000 et 1000 connexions.
  • A exécuté chaque test 3 fois pour connaître la moyenne.
  • Exécuté une méthode à la fois

J'ai trouvé l'amélioration des performances entre 40% et 60% en utilisant statique HttpClientau lieu de la jeter à la HttpClientdemande. J'ai mis les détails du résultat du test de performance dans l'article du blog ici .

Ankit Vijay
la source
1

Pour fermer correctement la connexion TCP , nous devons effectuer une séquence de paquets FIN-FIN + ACK-ACK (tout comme SYN - SYN + ACK - ACK, lors de l’ ouverture d’une connexion TCP ). Si nous appelons simplement une méthode .Close () (ce qui se produit généralement lorsqu'un HttpClient est en train de se départir) et si nous n'attendons 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 TCP local, car nous avons jeté notre écouteur (HttpClient) et nous n’avons jamais eu la possibilité de réinitialiser l’état du port à un état fermé approprié, une fois que l’homologue 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é. Ce n’est qu’alors que nous pourrons envoyer notre ACK final et disposer du HttpClient.

Juste pour ajouter, il est logique de garder les connexions TCP ouvertes, si vous exécutez des requêtes HTTP, à cause de l'en-tête HTTP "Connexion: Keep-Alive". De plus, vous pouvez demander à l'homologue distant de fermer la connexion pour vous 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.

Mladen B.
la source
1

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 surcharge. 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;
}
Alper Ebicoglu
la source
-5

Il n'y a pas qu'un seul moyen d'utiliser la classe HttpClient. L'important est de concevoir votre application de manière à tenir compte de son environnement et de ses contraintes.

HTTP est un excellent protocole à utiliser lorsque vous devez exposer des API publiques. Il peut également être utilisé efficacement pour les services internes légers à faible temps de latence - bien que le modèle de file d'attente de messages RPC constitue souvent un meilleur choix pour les services internes.

Bien gérer HTTP est une opération très complexe.

Considérer ce qui suit:

  1. La création d’un socket et l’établissement d’une connexion TCP utilisent la bande passante et le temps du réseau.
  2. HTTP / 1.1 prend en charge les requêtes de pipeline sur le même socket. L'envoi de plusieurs requêtes les unes après les autres, sans avoir à attendre les réponses précédentes, est probablement à l'origine de l'amélioration de la vitesse signalée par l'article du blog.
  3. Mise en cache et équilibreur de charge - si vous avez un équilibreur de charge devant les serveurs, assurez-vous que vos demandes ont des en-têtes de cache appropriés afin de réduire la charge sur vos serveurs et d’obtenir les réponses aux clients plus rapidement.
  4. N'interrogez jamais une ressource, utilisez le découpage HTTP pour renvoyer des réponses périodiques.

Mais surtout, testez, mesurez et confirmez. S'il ne se comporte pas comme prévu, nous pouvons alors répondre à des questions spécifiques sur la manière d'atteindre les résultats escomptés.

Michael Shaw
la source
4
Cela ne répond réellement à rien demandé.
Whatsisname
Vous semblez supposer qu’il n’ya qu’UNE SEULE façon. Je ne pense pas qu'il y en a. Je sais que vous devez l'utiliser de manière appropriée, puis tester et mesurer son comportement, puis ajuster votre approche jusqu'à ce que vous soyez heureux.
Michael Shaw
Vous avez un peu écrit sur l'utilisation de HTTP ou non pour communiquer. Le PO a demandé comment utiliser au mieux un composant de bibliothèque particulier.
Whatsisname
1
@ MichaelShaw: HttpClientimplémente IDisposable. Il n’est donc pas déraisonnable de s’attendre à ce qu’il s’agisse d’un objet éphémère qui sait se nettoyer lui-même et qui se prête bien à l’enveloppement d’une usingdéclaration à chaque fois que vous en avez besoin. Malheureusement, ce n'est pas comme ça que ça fonctionne. Le billet de blog lié au PO montre clairement qu'il existe des ressources (en particulier des connexions de socket TCP) qui subsistent longtemps après que la usingdéclaration est devenue hors de portée et que l' HttpClientobjet a probablement été supprimé.
Robert Harvey
1
Je comprends ce processus de pensée. C’est juste si vous pensiez à HTTP du point de vue de l’architecture et aviez l’intention de faire beaucoup de demandes au même service - alors vous penseriez à la mise en cache et au pipeline, puis à la pensée de faire de HttpClient un objet éphémère tout simplement se sentir mal. De même, si vous faites des demandes à différents serveurs et que vous n'obtenez aucun avantage à garder le socket en ligne, il est logique de supprimer l'objet HttpClient après son utilisation.
Michael Shaw