Управление подключением HttpClient

Управление соединениями HttpClient

1. обзор

В этой статье мы рассмотрим основы управления соединениями в HttpClient 4.

Мы рассмотрим использованиеBasichttpClientConnectionManager иPoolingHttpClientConnectionManager для обеспечения безопасного, совместимого с протоколом и эффективного использования HTTP-соединений.

2. BasicHttpClientConnectionManager для низкоуровневого однониточного соединения

BasicHttpClientConnectionManager доступен с HttpClient 4.3.3 как простейшая реализация диспетчера HTTP-соединений. Он используется для создания и управления одним соединением, которое может использоваться только одним потоком за раз.

Дальнейшее чтение:

Расширенная настройка HttpClient

Конфигурации HttpClient для расширенных вариантов использования.

Read more

Как отправлять пользовательские файлы cookie с помощью Apache HttpClient 4.

Read more

HttpClient с SSL

Пример того, как настроить HttpClient с SSL.

Read more

Пример 2.1. Getting a Connection Request for a Low Level Connection (HttpClientConnection)с

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

МетодrequestConnection получает от менеджера пул соединений для определенногоroute, к которому нужно подключиться. Параметрroute указывает маршрут «прокси-переходов» к целевому узлу или к самому целевому узлу.

Можно выполнить запрос, используяHttpClientConnection напрямую, но имейте в виду, что этот низкоуровневый подход является многословным и сложным в управлении. Низкоуровневые соединения полезны для доступа к данным сокетов и соединений, таким как тайм-ауты и информация о целевом хосте, но для стандартного выполненияHttpClient - гораздо более простой API для работы.

3. ИспользованиеPoolingHttpClientConnectionManager для получения и управления пулом многопоточных подключений

PoolingHttpClientConnectionManager будет создавать и управлять пулом соединений для каждого маршрута или целевого хоста, который мы используем. По умолчанию размер пула одновременныхconnections, который может быть открыт менеджером, равен2 for each route or target host и20 for total открытых соединений. Во-первых, давайте посмотрим, как настроить этот диспетчер соединений на простом HttpClient:

Пример 3.1. Setting the PoolingHttpClientConnectionManager on a HttpClient **

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

Далее - давайте посмотрим, как один и тот же диспетчер соединений может использоваться двумя HttpClients, работающими в двух разных потоках:

Пример 3.2. Using Two HttpClients to Connect to One Target Host Eachс

HttpGet get1 = new HttpGet("/");
HttpGet get2 = new HttpGet("http://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();

Обратите внимание, что мы используемa very simple custom thread implementation - вот он:

Пример 3.3. Custom Thread Executing a GET Requestс

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

Обратите внимание на вызовEntityUtils.consume(response.getEntity) - он необходим для использования всего содержимого ответа (объекта), чтобы менеджер могrelease the connection back to the pool.

4. Настроить диспетчер подключений

Значения по умолчанию диспетчера соединений пула выбраны правильно, но - в зависимости от вашего варианта использования - могут быть слишком малы. Итак, давайте посмотрим, как мы можем настроить:

  • общее количество соединений

  • максимальное количество соединений на (любой) маршрут

  • максимальное количество соединений на один конкретный маршрут

Пример 4.1. Increasing the Number of Connections that Can be Open and Managed Beyond the default Limitsс

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

Подведем итоги API:

  • setMaxTotal(int max): Установите максимальное количество открытых соединений.

  • setDefaultMaxPerRoute(int max): установите максимальное количество одновременных подключений на каждом маршруте, которое по умолчанию равно 2.

  • setMaxPerRoute(int max): установите общее количество одновременных подключений к определенному маршруту, которое по умолчанию равно 2.

Итак, без изменения значения по умолчаниюwe’re going to reach the limits of the connection manager довольно легко - давайте посмотрим, как это выглядит:

Пример 4.2. Using Threads to Execute Connectionsс

HttpGet get = new HttpGet("http://www.example.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();

Как мы уже обсуждали, по умолчаниюthe per host connection limit is 2. Итак, в этом примере мы пытаемся сделать3 requests to the same host тремя потоками, но только 2 соединения будут выделены параллельно.

Давайте посмотрим на журналы - у нас работает три потока, но только 2 арендованных соединения:

[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. Стратегия поддержания активности соединения

Цитирование HttpClient 4.3.3. ссылка: «Если заголовокKeep-Alive отсутствует в ответе,HttpClient предполагает, что соединение может поддерживаться в рабочем состоянии бесконечно». (See the HttpClient Reference).

Чтобы обойти это и иметь возможность управлять неработающими соединениями, нам нужна индивидуальная реализация стратегии и встроить ее вHttpClient.

Пример 5.1. A Custom Keep Alive Strategyс

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

Эта стратегия сначала попытается применить политику хостаKeep-Alive, указанную в заголовке. Если эта информация отсутствует в заголовке ответа, она будет поддерживать соединения в течение 5 секунд.

Теперь - давайте создадимa client with this custom strategy:

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

6. Сохранение соединения / повторное использование

Спецификация HTTP / 1.1 гласит, что соединения могут быть повторно использованы, если они не были закрыты - это называется постоянством соединения.

Как только соединение освобождается менеджером, оно остается открытым для повторного использования. При использованииBasicHttpClientConnectionManager,, который может управлять только одним соединением, соединение должно быть освобождено, прежде чем оно будет снова арендовано:

Пример 6.1. BasicHttpClientConnectionManagerConnection Reuse

BasicHttpClientConnectionManager basicConnManager =
    new BasicHttpClientConnectionManager();
HttpClientContext context = HttpClientContext.create();

// low level
HttpRoute route = new HttpRoute(new HttpHost("www.example.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.example.com", 80)));
HttpGet get = new HttpGet("http://www.example.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);

Давайте посмотрим, что происходит.

Во-первых, обратите внимание, что сначала мы используем низкоуровневое соединение, чтобы иметь полный контроль над тем, когда соединение будет разорвано, а затем обычное соединение более высокого уровня с HttpClient. Сложная низкоуровневая логика здесь не очень актуальна - единственное, что нас волнует, это вызовreleaseConnection. Это освобождает единственное доступное соединение и позволяет использовать его повторно.

Затем клиент снова выполняет запрос GET с успехом. Если мы пропустим освобождение соединения, мы получим исключение IllegalStateException от 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)

Обратите внимание, что существующее соединение не закрывается, а просто освобождается, а затем повторно используется вторым запросом.

В отличие от приведенного выше примера,PoolingHttpClientConnectionManager позволяет прозрачно повторно использовать соединение без необходимости неявно освобождать соединение:

Пример 6.2. *PoolingHttpClientConnectionManager: Повторное использование соединений с потоками *

HttpGet get = new HttpGet("http://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);
}

В приведенном выше примере 10 потоков, выполняющих 10 запросов, но имеющих только 5 подключений.

Конечно, в этом примере используется тайм-аут сервераKeep-Alive. Чтобы убедиться, что соединения не умирают перед повторным использованием, рекомендуется настроитьclient со стратегиейKeep-Alive (см. Пример 5.1.).

7. Настройка тайм-аутов - тайм-аут сокета с помощью диспетчера подключений

Единственный тайм-аут, который может быть установлен во время настройки диспетчера соединений, - это тайм-аут сокета:

Пример 7.1. Setting Socket Timeout to 5 Secondsс

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

Для более подробного обсуждения тайм-аутов в HttpClient -see this.

8. Удаление соединения

Исключение соединения используется дляdetect idle and expired connections and close them; для этого есть два варианта.

  1. ИспользованиеHttpClient для проверки того, устарело ли соединение, перед выполнением запроса. Это дорогой вариант, который не всегда надежен.

  2. Создайте поток монитора для закрытия незанятых и / или закрытых соединений.

Пример 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();

Пример 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);

КлассIdleConnectionMonitorThread приведен ниже:

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. Закрытие соединения

Соединение может быть закрыто изящно (делается попытка очистить выходной буфер перед закрытием) или принудительно, путем вызова методаshutdown (выходной буфер не очищается).

Чтобы правильно закрыть соединения, нам нужно сделать все следующее:

  • потреблять и закрывать ответ (если можно закрыть)

  • закрыть клиент

  • закрыть и закрыть диспетчер соединений

Пример 8.1. Closing Connection and Releasing Resourcesс

connManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom()
  .setConnectionManager(connManager).build();
HttpGet get = new HttpGet("http://google.com");
CloseableHttpResponse response = client.execute(get);

EntityUtils.consume(response.getEntity());
response.close();
client.close();
connManager.close();

Если менеджер выключен без уже закрытых соединений - все соединения будут закрыты и все ресурсы будут освобождены.

Важно помнить, что при этом не будут сброшены данные, которые могли сохраняться для существующих подключений.

10. Заключение

В этой статье мы обсудили, как использовать API управления HTTP-соединениями HttpClient для обработкиthe entire process of managing connections - от их открытия и выделения, через управление их одновременным использованием несколькими агентами, до их окончательного закрытия.

Мы увидели, чтоBasicHttpClientConnectionManager является простым решением для обработки одиночных подключений и как он может управлять низкоуровневыми подключениями. Мы также увидели, какPoolingHttpClientConnectionManager в сочетании с APIHttpClient обеспечивает эффективное и совместимое с протоколом использование HTTP-соединений.