Рубрики
Без рубрики

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

Как открывать, управлять и закрывать соединения с Apache HttpClient 4.

Автор оригинала: Eugen Paraschiv.

1. Обзор

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

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

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

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

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

Расширенная конфигурация HttpClient

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

HttpClient 4 – Отправить пользовательский файл cookie

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

HttpClient с SSL

Пример настройки HttpClient с SSL.
Подробнее

Пример 2.1. Получение запроса на соединение для низкоуровневого соединения ( HttpClientConnection )

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

Метод request Connection получает от менеджера пул соединений для определенного маршрута для подключения. Параметр route указывает маршрут “прокси-переходов” к целевому хосту или самому целевому хосту.

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

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

PoolingHttpClientConnectionManager создаст и будет управлять пулом подключений для каждого маршрута или целевого хоста, который мы используем. По умолчанию размер пула параллельных соединений , который может быть открыт менеджером, равен 2 для каждого маршрута или целевого хоста и 20 для общего числа открытых соединений. Во – первых, давайте посмотрим, как настроить этот менеджер соединений на простом HttpClient:

Пример 3.1. Установка PoolingHttpClientConnectionManager на HttpClient

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

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

Пример 3.2. Использование двух Http-клиентов для подключения к одному целевому хосту Каждый

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

Обратите внимание, что мы используем очень простую реализацию пользовательского потока – вот она:

Пример 3.3. Пользовательский поток Выполнение запроса 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) {
        }
    }
}

Обратите внимание на |/EntityUtils.consume(response.getEntity) вызов – необходимо использовать все содержимое ответа (сущности), чтобы менеджер мог освободить соединение обратно в пул .

4. Настройте диспетчер соединений

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

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

Пример 4.1. Увеличение количества соединений, которые могут быть открыты и управляться За пределами ограничений по умолчанию

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

Давайте вспомним API:

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

Итак, не меняя значения по умолчанию, мы довольно легко достигнем пределов диспетчера соединений – давайте посмотрим, как это выглядит:

Пример 4.2. Использование потоков для выполнения соединений

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

Как мы уже обсуждали, ограничение на подключение к хосту по умолчанию равно 2 . Итак, в этом примере мы пытаемся, чтобы 3 потока делали 3 запроса к одному и тому же хосту , но параллельно будут выделяться только 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 предполагает, что соединение может сохраняться бесконечно” ( См. Ссылку HttpClient ).

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

Пример 5.1. Пользовательская стратегия Сохранения жизни

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 секунд.

Теперь – давайте создадим клиент с этой пользовательской стратегией :

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

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

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

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

Пример 6.1. 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("http://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);

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

Во – первых, обратите внимание, что сначала мы используем низкоуровневое соединение, просто чтобы иметь полный контроль над тем, когда соединение будет освобождено, а затем обычное соединение более высокого уровня с 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 . Чтобы убедиться, что соединения не умрут перед повторным использованием, рекомендуется настроить клиент со стратегией Keep-Alive (см. Пример 5.1.).

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

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

Пример 7.1. Установка тайм-аута сокета на 5 секунд

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

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

8. Выселение связи

Удаление соединений используется для обнаружения незанятых и просроченных соединений и их закрытия ; для этого есть два варианта.

  1. Полагаясь на Httpclient t, чтобы проверить, не устарело ли соединение перед выполнением запроса. Это дорогой вариант, который не всегда надежен.
  2. Создайте поток монитора для закрытия бездействующих и/или закрытых соединений.

Пример 8.1. Настройка HttpClient для проверки устаревших соединений

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

Пример 8.2. Использование устаревшего потока монитора подключений

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

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 для обработки всего процесса управления соединениями – от открытия и выделения их, через управление их одновременным использованием несколькими агентами, до окончательного закрытия их.

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