HttpClient-Verbindungsverwaltung

1. Übersicht

In diesem Artikel werden die Grundlagen des Verbindungsmanagements in HttpClient 4 erläutert.

Wir werden die Verwendung von BasichttpClientConnectionManager und PoolingHttpClientConnectionManager behandeln , um eine sichere, protokollkonforme und effiziente Verwendung von HTTP-Verbindungen zu erzwingen.

2. Der BasicHttpClientConnectionManager für eine Low-Level-Single-Threaded-Verbindung

Der BasicHttpClientConnectionManager ist seit HttpClient 4.3.3 als einfachste Implementierung eines HTTP-Verbindungsmanagers verfügbar. Es wird verwendet, um eine einzelne Verbindung zu erstellen und zu verwalten, die jeweils nur von einem Thread verwendet werden kann.

Beispiel 2.1. Abrufen einer Verbindungsanforderung für eine Verbindung auf niedriger Ebene ( HttpClientConnection )

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

Die requestConnection- Methode ruft vom Manager einen Verbindungspool für eine bestimmte Route ab, zu der eine Verbindung hergestellt werden soll. Der Parameter route gibt eine Route von "Proxy-Hops" zum Zielhost oder zum Zielhost selbst an.

Es ist möglich, eine Anforderung direkt über eine HttpClientConnection auszuführen. Beachten Sie jedoch, dass dieser Ansatz auf niedriger Ebene ausführlich und schwierig zu verwalten ist. Low-Level-Verbindungen sind nützlich, um auf Socket- und Verbindungsdaten wie Zeitüberschreitungen und Zielhostinformationen zuzugreifen. Für Standardausführungen ist der HttpClient jedoch eine viel einfachere API, mit der man arbeiten kann.

3. Verwenden des PoolingHttpClientConnectionManager zum Abrufen und Verwalten eines Pools von Multithread-Verbindungen

Der PoolingHttpClientConnectionManager erstellt und verwaltet einen Verbindungspool für jede von uns verwendete Route oder jeden Zielhost. Die Standardgröße des Pools gleichzeitiger Verbindungen , die vom Manager geöffnet werden können, beträgt 2 für jede Route oder jeden Zielhost und 20 für insgesamt offene Verbindungen. Schauen wir uns zunächst an, wie Sie diesen Verbindungsmanager auf einem einfachen HttpClient einrichten:

Beispiel 3.1. Festlegen des PoolingHttpClientConnectionManager auf einem HttpClient

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

Weiter - sehen wir uns an, wie derselbe Verbindungsmanager von zwei HttpClients verwendet werden kann, die in zwei verschiedenen Threads ausgeführt werden:

Beispiel 3.2. Verwenden von zwei HttpClients zum Herstellen einer Verbindung mit jeweils einem Zielhost

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();

Beachten Sie, dass wir eine sehr einfache benutzerdefinierte Thread-Implementierung verwenden - hier ist es:

Beispiel 3.3. Benutzerdefinierter Thread Ausführen einer GET-Anforderung

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) { } } }

Beachten Sie den Aufruf von EntityUtils.consume (response.getEntity) , der erforderlich ist, um den gesamten Inhalt der Antwort (Entität) zu verbrauchen, damit der Manager die Verbindung zum Pool wiederherstellen kann .

4. Konfigurieren Sie den Verbindungsmanager

Die Standardeinstellungen des Pooling-Verbindungsmanagers sind gut gewählt, können jedoch - abhängig von Ihrem Anwendungsfall - zu klein sein. Schauen wir uns also an, wie wir Folgendes konfigurieren können:

  • die Gesamtzahl der Verbindungen
  • Die maximale Anzahl von Verbindungen pro (beliebiger) Route
  • Die maximale Anzahl von Verbindungen pro einzelner, spezifischer Route

Beispiel 4.1. Erhöhen der Anzahl der Verbindungen, die geöffnet und verwaltet werden können, über die Standardgrenzen hinaus

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

Lassen Sie uns die API rekapitulieren:

  • setMaxTotal (int max) : Legen Sie die maximale Anzahl offener Verbindungen fest.
  • setDefaultMaxPerRoute (int max) : Legen Sie die maximale Anzahl gleichzeitiger Verbindungen pro Route fest, die standardmäßig 2 beträgt.
  • setMaxPerRoute (int max) : Legen Sie die Gesamtzahl der gleichzeitigen Verbindungen zu einer bestimmten Route fest, standardmäßig 2.

Ohne die Standardeinstellung zu ändern, werden wir also ganz einfach die Grenzen des Verbindungsmanagers erreichen - mal sehen, wie das aussieht:

Beispiel 4.2. Verwenden von Threads zum Ausführen von Verbindungen

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();

Wie bereits erwähnt, beträgt das Verbindungslimit pro Host standardmäßig 2 . In diesem Beispiel versuchen wir, dass 3 Threads 3 Anforderungen an denselben Host senden , aber nur 2 Verbindungen werden parallel zugewiesen.

Werfen wir einen Blick auf die Protokolle - es werden drei Threads ausgeführt, aber nur zwei geleaste Verbindungen:

[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. Strategie zur Aufrechterhaltung der Verbindung

Zitieren des HttpClient 4.3.3. Referenz: "Wenn der Keep-AliveHeader in der Antwort nicht vorhanden ist, geht HttpClient davon aus, dass die Verbindung auf unbestimmte Zeit am Leben gehalten werden kann." (Siehe die HttpClient-Referenz).

Um dies zu umgehen und tote Verbindungen verwalten zu können, benötigen wir eine angepasste Strategieimplementierung und bauen sie in den HttpClient ein .

Beispiel 5.1. Eine benutzerdefinierte Keep Alive-Strategie

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; } };

Diese Strategie versucht zunächst, die im Header angegebene Keep-Alive- Richtlinie des Hosts anzuwenden . Wenn diese Informationen nicht im Antwortheader vorhanden sind, bleiben die Verbindungen 5 Sekunden lang aktiv.

Jetzt erstellen wir einen Client mit dieser benutzerdefinierten Strategie :

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

6. Persistenz / Wiederverwendung der Verbindung

Die HTTP / 1.1-Spezifikation besagt, dass Verbindungen wiederverwendet werden können, wenn sie nicht geschlossen wurden - dies wird als Verbindungspersistenz bezeichnet.

Sobald eine Verbindung vom Manager freigegeben wurde, bleibt sie für die Wiederverwendung geöffnet. Bei Verwendung eines BasicHttpClientConnectionManager, der nur eine einzelne Verbindung verwalten kann, muss die Verbindung freigegeben werden, bevor sie wieder zurückgemietet wird:

Beispiel 6.1. Wiederverwendung der BasicHttpClientConnectionManager- Verbindung

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);

Werfen wir einen Blick darauf, was passiert.

Beachten Sie zunächst, dass wir zuerst eine Verbindung auf niedriger Ebene verwenden, damit wir die volle Kontrolle darüber haben, wann die Verbindung freigegeben wird, und dann eine normale Verbindung auf höherer Ebene mit einem HttpClient. Die komplexe Low-Level-Logik ist hier nicht sehr relevant - das einzige, was uns wichtig ist, ist der Aufruf von releaseConnection . Dadurch wird die einzige verfügbare Verbindung freigegeben und kann wiederverwendet werden.

Anschließend führt der Client die GET-Anforderung erneut mit Erfolg aus. Wenn wir das Freigeben der Verbindung überspringen, erhalten wir eine IllegalStateException vom 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)

Beachten Sie, dass die vorhandene Verbindung nicht geschlossen, sondern nur freigegeben und dann von der zweiten Anforderung wiederverwendet wird.

In contrast to the above example, The PoolingHttpClientConnectionManager allows connection re-use transparently without the need to release a connection implicitly:

Example 6.2.PoolingHttpClientConnectionManager: Re-Using Connections with 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); }

The example above has 10 threads, executing 10 requests but only sharing 5 connections.

Of course, this example relies on the server's Keep-Alive timeout. To make sure the connections don't die before being re-used it is recommended to configure the client with a Keep-Alive strategy (See Example 5.1.).

7. Configuring Timeouts – Socket Timeout Using the Connection Manager

The only timeout that can be set at the time when connection manager is configured is the socket timeout:

Example 7.1. Setting Socket Timeout to 5 Seconds

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

For a more in-depth discussion of timeouts in the HttpClient – see this.

8. Connection Eviction

Connection eviction is used to detect idle and expired connections and close them; there are two options to do this.

  1. Relying on the HttpClient to check if the connection is stale before executing a request. This is an expensive option that is not always reliable.
  2. Create a monitor thread to close idle and/or closed connections.

Example 8.1. Setting the HttpClient to Check for Stale Connections

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

Example 8.2. Using a Stale Connection Monitor Thread

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

In diesem Artikel wurde erläutert, wie die HTTP-Verbindungsverwaltungs-API von HttpClient verwendet wird, um den gesamten Prozess der Verwaltung von Verbindungen abzuwickeln - vom Öffnen und Zuweisen über die gleichzeitige Verwendung durch mehrere Agenten bis zum endgültigen Schließen.

Wir haben gesehen, wie der BasicHttpClientConnectionManager eine einfache Lösung für die Verarbeitung einzelner Verbindungen ist und wie er Verbindungen auf niedriger Ebene verwalten kann. Wir haben auch gesehen, wie der PoolingHttpClientConnectionManager in Kombination mit der HttpClient- API eine effiziente und protokollkonforme Verwendung von HTTP-Verbindungen ermöglicht.