Перейти к основному содержимому

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

· 9 мин. чтения

1. Обзор

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

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

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

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

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

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

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

Можно выполнить запрос, используя 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. Использование двух HttpClient для подключения к одному целевому хосту каждый

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.foreach.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.foreach.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. Индивидуальная стратегия 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;
}
};

Эта стратегия сначала попытается применить политику 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.foreach.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.foreach.com", 80)));
HttpGet get = new HttpGet("http://www.foreach.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.foreach.com", 80));
PoolingHttpClientConnectionManager connManager
= new PoolingHttpClientConnectionManager();
connManager.setSocketConfig(route.getTargetHost(),SocketConfig.custom().
setSoTimeout(5000).build());

Более подробное обсуждение тайм-аутов в HttpClient — см. здесь .

8. Исключение соединения

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

  1. Использование HttpClient для проверки устаревания соединения перед выполнением запроса. Это дорогой вариант, который не всегда надежен.
  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 (буфер вывода не сбрасывается).

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

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

Пример 9.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.

Код, использованный в этой статье, можно найти на нашем Github .