Gestion des connexions HttpClient

1. Vue d'ensemble

Dans cet article, nous allons passer en revue les bases de la gestion des connexions dans HttpClient 4.

Nous couvrirons l'utilisation de BasichttpClientConnectionManager et PoolingHttpClientConnectionManager pour imposer une utilisation sûre, conforme au protocole et efficace des connexions HTTP.

2. Le BasicHttpClientConnectionManager pour une connexion à un seul filetage de bas niveau

Le BasicHttpClientConnectionManager est disponible depuis HttpClient 4.3.3 comme l'implémentation la plus simple d'un gestionnaire de connexions HTTP. Il est utilisé pour créer et gérer une seule connexion qui ne peut être utilisée que par un thread à la fois.

Exemple 2.1. Obtenir une demande de connexion pour une connexion de bas niveau ( HttpClientConnection )

BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(); HttpRoute route = new HttpRoute(new HttpHost("www.baeldung.com", 80)); ConnectionRequest connRequest = connManager.requestConnection(route, null);

La méthode requestConnection obtient du gestionnaire un pool de connexions pour une route spécifique à laquelle se connecter. Le paramètre route spécifie une route de «sauts proxy» vers l'hôte cible ou l'hôte cible lui-même.

Il est possible d'exécuter une demande à l'aide d'un HttpClientConnection directement, mais gardez à l'esprit que cette approche de bas niveau est verbeuse et difficile à gérer. Les connexions de bas niveau sont utiles pour accéder aux données de socket et de connexion telles que les délais d'expiration et les informations sur l'hôte cible, mais pour les exécutions standard, HttpClient est une API beaucoup plus facile à utiliser .

3. Utilisation de PoolingHttpClientConnectionManager pour obtenir et gérer un pool de connexions multithread

Le PoolingHttpClientConnectionManager créera et gérera un pool de connexions pour chaque route ou hôte cible que nous utilisons. La taille par défaut du pool de connexions simultanées pouvant être ouvertes par le gestionnaire est de 2 pour chaque route ou hôte cible et de 20 pour le nombre total de connexions ouvertes. Tout d'abord, voyons comment configurer ce gestionnaire de connexions sur un simple HttpClient:

Exemple 3.1. Définition de PoolingHttpClientConnectionManager sur un HttpClient

HttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom().setConnectionManager(poolingConnManager) .build(); client.execute(new HttpGet("/")); assertTrue(poolingConnManager.getTotalStats().getLeased() == 1);

Ensuite, voyons comment le même gestionnaire de connexions peut être utilisé par deux HttpClients s'exécutant dans deux threads différents:

Exemple 3.2. Utilisation de deux HttpClients pour se connecter à un hôte cible chacun

HttpGet get1 = new HttpGet("/"); HttpGet get2 = new HttpGet("//google.com"); PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client1 = HttpClients.custom().setConnectionManager(connManager).build(); CloseableHttpClient client2 = HttpClients.custom().setConnectionManager(connManager).build(); MultiHttpClientConnThread thread1 = new MultiHttpClientConnThread(client1, get1); MultiHttpClientConnThread thread2 = new MultiHttpClientConnThread(client2, get2); thread1.start(); thread2.start(); thread1.join(); thread2.join();

Notez que nous utilisons une implémentation de thread personnalisé très simple - la voici:

Exemple 3.3. Thread personnalisé exécutant une requête GET

public class MultiHttpClientConnThread extends Thread { private CloseableHttpClient client; private HttpGet get; // standard constructors public void run(){ try { HttpResponse response = client.execute(get); EntityUtils.consume(response.getEntity()); } catch (ClientProtocolException ex) { } catch (IOException ex) { } } }

Notez l' appel EntityUtils.consume (response.getEntity) - nécessaire pour consommer tout le contenu de la réponse (entité) afin que le gestionnaire puisse libérer la connexion au pool .

4. Configurer le gestionnaire de connexion

Les valeurs par défaut du gestionnaire de connexions de regroupement sont bien choisies mais - en fonction de votre cas d'utilisation - peuvent être trop petites. Alors - voyons comment nous pouvons configurer:

  • le nombre total de connexions
  • le nombre maximum de connexions par (n'importe quel) itinéraire
  • le nombre maximum de connexions pour un seul itinéraire spécifique

Exemple 4.1. Augmentation du nombre de connexions pouvant être ouvertes et gérées au-delà des limites par défaut

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(5); connManager.setDefaultMaxPerRoute(4); HttpHost host = new HttpHost("www.baeldung.com", 80); connManager.setMaxPerRoute(new HttpRoute(host), 5);

Récapitulons l'API:

  • setMaxTotal (int max) : Définit le nombre maximum de connexions ouvertes totales.
  • setDefaultMaxPerRoute (int max) : définit le nombre maximal de connexions simultanées par route, qui est de 2 par défaut.
  • setMaxPerRoute (int max) : définit le nombre total de connexions simultanées vers une route spécifique, qui est de 2 par défaut.

Donc, sans changer la valeur par défaut, nous allons atteindre les limites du gestionnaire de connexions assez facilement - voyons à quoi cela ressemble:

Exemple 4.2. Utilisation de threads pour exécuter des connexions

HttpGet get = new HttpGet("//www.baeldung.com"); PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom(). setConnectionManager(connManager).build(); MultiHttpClientConnThread thread1 = new MultiHttpClientConnThread(client, get); MultiHttpClientConnThread thread2 = new MultiHttpClientConnThread(client, get); MultiHttpClientConnThread thread3 = new MultiHttpClientConnThread(client, get); thread1.start(); thread2.start(); thread3.start(); thread1.join(); thread2.join(); thread3.join();

Comme nous l'avons déjà mentionné, la limite de connexion par hôte est de 2 par défaut. Donc, dans cet exemple, nous essayons que 3 threads effectuent 3 requêtes vers le même hôte , mais seules 2 connexions seront allouées en parallèle.

Jetons un coup d'œil aux journaux - nous avons trois threads en cours d'exécution mais seulement 2 connexions louées:

[Thread-0] INFO o.b.h.c.MultiHttpClientConnThread - Before - Leased Connections = 0 [Thread-1] INFO o.b.h.c.MultiHttpClientConnThread - Before - Leased Connections = 0 [Thread-2] INFO o.b.h.c.MultiHttpClientConnThread - Before - Leased Connections = 0 [Thread-2] INFO o.b.h.c.MultiHttpClientConnThread - After - Leased Connections = 2 [Thread-0] INFO o.b.h.c.MultiHttpClientConnThread - After - Leased Connections = 2

5. Stratégie de connexion Keep-Alive

Citant le HttpClient 4.3.3. référence: "Si l'en- Keep-Alivetête n'est pas présent dans la réponse, HttpClient suppose que la connexion peut être maintenue indéfiniment active ." (Voir la référence HttpClient).

Pour contourner cela et être en mesure de gérer les connexions mortes, nous avons besoin d'une implémentation de stratégie personnalisée et de l' intégrer dans HttpClient .

Exemple 5.1. Une stratégie personnalisée Keep Alive

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { HeaderElementIterator it = new BasicHeaderElementIterator (response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase ("timeout")) { return Long.parseLong(value) * 1000; } } return 5 * 1000; } };

Cette stratégie essaiera d'abord d'appliquer la politique Keep-Alive de l'hôte indiquée dans l'en-tête. Si cette information n'est pas présente dans l'en-tête de la réponse, les connexions resteront actives pendant 5 secondes.

Maintenant, créons un client avec cette stratégie personnalisée :

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom() .setKeepAliveStrategy(myStrategy) .setConnectionManager(connManager) .build();

6. Persistance / réutilisation de la connexion

La spécification HTTP / 1.1 stipule que les connexions peuvent être réutilisées si elles n'ont pas été fermées - c'est ce qu'on appelle la persistance des connexions.

Une fois qu'une connexion est libérée par le gestionnaire, elle reste ouverte pour une réutilisation. Lors de l'utilisation d'un BasicHttpClientConnectionManager, qui ne peut gérer qu'une seule connexion, la connexion doit être libérée avant d'être louée à nouveau:

Exemple 6.1. Réutilisation de la connexion BasicHttpClientConnectionManager

BasicHttpClientConnectionManager basicConnManager = new BasicHttpClientConnectionManager(); HttpClientContext context = HttpClientContext.create(); // low level HttpRoute route = new HttpRoute(new HttpHost("www.baeldung.com", 80)); ConnectionRequest connRequest = basicConnManager.requestConnection(route, null); HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS); basicConnManager.connect(conn, route, 1000, context); basicConnManager.routeComplete(conn, route, context); HttpRequestExecutor exeRequest = new HttpRequestExecutor(); context.setTargetHost((new HttpHost("www.baeldung.com", 80))); HttpGet get = new HttpGet("//www.baeldung.com"); exeRequest.execute(get, conn, context); basicConnManager.releaseConnection(conn, null, 1, TimeUnit.SECONDS); // high level CloseableHttpClient client = HttpClients.custom() .setConnectionManager(basicConnManager) .build(); client.execute(get);

Jetons un coup d'œil à ce qui se passe.

Tout d'abord, notez que nous utilisons d'abord une connexion de bas niveau, juste pour avoir un contrôle total sur le moment où la connexion est libérée, puis une connexion de niveau supérieur normale avec un HttpClient. La logique complexe de bas niveau n'est pas très pertinente ici - la seule chose qui nous importe est l' appel releaseConnection . Cela libère la seule connexion disponible et lui permet d'être réutilisée.

Ensuite, le client exécute à nouveau la demande GET avec succès. Si nous ignorons la libération de la connexion, nous obtiendrons une exception IllegalStateException du HttpClient:

java.lang.IllegalStateException: Connection is still allocated at o.a.h.u.Asserts.check(Asserts.java:34) at o.a.h.i.c.BasicHttpClientConnectionManager.getConnection (BasicHttpClientConnectionManager.java:248)

Notez que la connexion existante n'est pas fermée, juste libérée puis réutilisée par la deuxième requête.

Contrairement à l'exemple ci-dessus, le PoolingHttpClientConnectionManager permet la réutilisation de la connexion de manière transparente sans qu'il soit nécessaire de libérer une connexion implicitement:

Exemple 6.2. PoolingHttpClientConnectionManager : réutilisation des connexions avec des threads

HttpGet get = new HttpGet("//echo.200please.com"); PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setDefaultMaxPerRoute(5); connManager.setMaxTotal(5); CloseableHttpClient client = HttpClients.custom() .setConnectionManager(connManager) .build(); MultiHttpClientConnThread[] threads = new MultiHttpClientConnThread[10]; for(int i = 0; i < threads.length; i++){ threads[i] = new MultiHttpClientConnThread(client, get, connManager); } for (MultiHttpClientConnThread thread: threads) { thread.start(); } for (MultiHttpClientConnThread thread: threads) { thread.join(1000); }

L'exemple ci-dessus a 10 threads, exécutant 10 requêtes mais ne partageant que 5 connexions.

Bien entendu, cet exemple repose sur le délai d'expiration Keep-Alive du serveur . Pour s'assurer que les connexions ne meurent pas avant d'être réutilisées, il est recommandé de configurer le client avec une stratégie Keep-Alive (voir l'exemple 5.1.).

7. Configuration des délais d'expiration - Délai d'expiration du socket à l'aide du gestionnaire de connexion

Le seul délai d'expiration qui peut être défini au moment de la configuration du gestionnaire de connexions est le délai d'expiration du socket:

Exemple 7.1. Définition du délai d'expiration du socket sur 5 secondes

HttpRoute route = new HttpRoute(new HttpHost("www.baeldung.com", 80)); PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setSocketConfig(route.getTargetHost(),SocketConfig.custom(). setSoTimeout(5000).build());

Pour une discussion plus approfondie des délais d'expiration dans HttpClient - voir ceci.

8. Expulsion de connexion

L'expulsion de connexion est utilisée pour détecter les connexions inactives et expirées et les fermer ; il existe deux options pour ce faire.

  1. S'appuyer sur HttpClien t pour vérifier si la connexion est périmée avant d'exécuter une requête. C'est une option coûteuse qui n'est pas toujours fiable.
  2. Créez un thread de surveillance pour fermer les connexions inactives et / ou fermées.

Exemple 8.1. Configuration de HttpClient pour vérifier les connexions périmées

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig( RequestConfig.custom().setStaleConnectionCheckEnabled(true).build() ).setConnectionManager(connManager).build();

Exemple 8.2. Utilisation d'un thread de surveillance de connexion périmée

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom() .setConnectionManager(connManager).build(); IdleConnectionMonitorThread staleMonitor = new IdleConnectionMonitorThread(connManager); staleMonitor.start(); staleMonitor.join(1000);

The IdleConnectionMonitorThreadclass is listed below:

public class IdleConnectionMonitorThread extends Thread { private final HttpClientConnectionManager connMgr; private volatile boolean shutdown; public IdleConnectionMonitorThread( PoolingHttpClientConnectionManager connMgr) { super(); this.connMgr = connMgr; } @Override public void run() { try { while (!shutdown) { synchronized (this) { wait(1000); connMgr.closeExpiredConnections(); connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } } } catch (InterruptedException ex) { shutdown(); } } public void shutdown() { shutdown = true; synchronized (this) { notifyAll(); } } }

9. Connection Closing

A connection can be closed gracefully (an attempt to flush the output buffer prior to closing is made), or forcefully, by calling the shutdown method (the output buffer is not flushed).

To properly close connections we need to do all of the following:

  • consume and close the response (if closeable)
  • close the client
  • close and shut down the connection manager

Example 8.1. Closing Connection and Releasing Resources

connManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom() .setConnectionManager(connManager).build(); HttpGet get = new HttpGet("//google.com"); CloseableHttpResponse response = client.execute(get); EntityUtils.consume(response.getEntity()); response.close(); client.close(); connManager.close(); 

If the manager is shut down without connections being closed already – all connections will be closed and all resources released.

It's important to keep in mind that this will not flush any data that may have been ongoing for the existing connections.

10. Conclusion

Dans cet article, nous avons expliqué comment utiliser l'API de gestion des connexions HTTP de HttpClient pour gérer l'ensemble du processus de gestion des connexions - de leur ouverture et de leur allocation, en passant par la gestion de leur utilisation simultanée par plusieurs agents, jusqu'à leur fermeture finale.

Nous avons vu comment le BasicHttpClientConnectionManager est une solution simple pour gérer des connexions uniques et comment il peut gérer les connexions de bas niveau. Nous avons également vu comment le PoolingHttpClientConnectionManager combiné à l' API HttpClient permet une utilisation efficace et conforme au protocole des connexions HTTP.