HttpClient连接管理
主机间建立网络连接是个非常复杂跟耗时的过程(例如TCP三次握手bla bla),在HTTP请求中,如果可以复用一个连接来执行多次请求,可以很大地提高吞吐量。
HttpClient中,连接就是一种可以复用的资源。它提供了一系列连接管理的API,帮助我们处理连接管理的各种问题。本文基于4.5.10版本,介绍这些API的使用。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.10</version>
</dependency>
HttpClient中的连接是有状态且线程不安全的,它使用专门的连接管理器来管理这些连接,作为连接的工厂,负责连接的生命周期管理,以及对连接的并发访问进行同步。连接管理器抽象为 HttpClientConnectionManager 接口,接口有两种实现,分别是 BasicHttpClientConnectionManager 和 PoolingHttpClientConnectionManager。
目录
- HttpClient连接管理
- 1 BasicHttpClientConnectionManager
- 2 PoolingHttpClientConnectionManager
- 3 配置连接管理器
- 4 keep-alive策略
- 5 连接持久化与复用
- 6 配置超时时间
- 7 连接驱逐
- 8 关闭连接
- 参考资料
1 BasicHttpClientConnectionManager
BasicHttpClientConnectionManager,http连接管理器最简单的一种实现,用于创建和管理单个连接,只用于单线程,显然也是线程安全的。
BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager();
HttpRoute route = new HttpRoute(new HttpHost("www.baidu.com", 80));
ConnectionRequest connectionRequest = connectionManager.requestConnection(route, null);
上面的方法是基于 BasicHttpClientConnectionManager 的底层API的使用,requestConnection 方法从 connectionManager 管理的连接池取出一个连接,连接到 route 对象定义的“www.baidu.com”。
HttpRoute
HttpClient可以直接与目标主机建立连接,也可以通过由一系列跳转最终达到目标主机,这个过程称为路由。 RouteInfo 接口定义了连接到目标主机的路由信息,而 HttpRoute
就是该接口的一个具体实现,这个类是不可变的。
2 PoolingHttpClientConnectionManager
PoolingHttpClientConnectionManager 可以创建并管理一个连接池,为多个路由或目标主机提供连接。简单的用法如下:
//为一个HttpClient对象配置连接池
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build();
try {
client.execute(new HttpGet("https://www.baidu.com"));
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(connectionManager.getTotalStats().getLeased());
单个连接池可以供多个线程的多个HttpClient对象使用
//可以使用一个连接池,管理面向不同目标主机的请求
HttpGet get1 = new HttpGet("https://www.zhihu.com");
HttpGet get2 = new HttpGet("https://www.baidu.com");
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client1 = HttpClients.custom().setConnectionManager(connectionManager).build();
CloseableHttpClient client2 = HttpClients.custom().setConnectionManager(connectionManager).build();
MultiHttpClientConnThread t1 = new MultiHttpClientConnThread(client1, get1);
MultiHttpClientConnThread t2 = new MultiHttpClientConnThread(client2, get2);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
其中 MultiHttpClientConnThread 是自定义的类,定义如下
@Slf4j
public class MultiHttpClientConnThread extends Thread {
private final CloseableHttpClient client;
private final HttpGet get;
private PoolingHttpClientConnectionManager connectionManager;
public MultiHttpClientConnThread(final CloseableHttpClient client, final HttpGet get) {
this.client = client;
this.get = get;
}
public MultiHttpClientConnThread(final CloseableHttpClient client, final HttpGet get, final PoolingHttpClientConnectionManager connectionManager) {
this.client = client;
this.get = get;
this.connectionManager = connectionManager;
}
@Override
public void run() {
try {
log.info("Thread Running:" + getName());
if (connectionManager != null) {
log.info("Before - Leased Connections = " + connectionManager.getTotalStats().getLeased());
log.info("Before - Available Connections = " + connectionManager.getTotalStats().getAvailable());
}
HttpResponse response = client.execute(get);
if (connectionManager != null) {
log.info("After - Leased Connections = " + connectionManager.getTotalStats().getLeased());
log.info("After - Available Connections = " + connectionManager.getTotalStats().getAvailable());
}
//消费response,为了把连接释放回连接池
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
log.error("", e);
}
}
}
注意EntityUtils.consume(response.getEntity())
,需要消费掉response的全部内容,连接管理器才会把这个连接释放回归连接池。
3 配置连接管理器
PoolingHttpClientConnectionManager 可配置的选项如下:
- 连接总数,默认值为20
- 单个普通路由的最大连接数,默认值为2
- 特定某个路由的最大连接数,默认值为2
//调整默认的连接池参数
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
//总连接数为5
connectionManager.setMaxTotal(5);
//单个路由最大连接数为4
connectionManager.setDefaultMaxPerRoute(4);
//特定路由www.baidu.com的最大连接数为5
HttpHost httpHost = new HttpHost("www.baidu.com", 80);
HttpRoute route = new HttpRoute(httpHost);
connectionManager.setMaxPerRoute(route, 5);
如果使用默认设置,在多线程请求的情况下,单个路由很容易就达到最大连接数了
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build();
MultiHttpClientConnThread t1 = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
MultiHttpClientConnThread t2 = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
MultiHttpClientConnThread t3 = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
运行以上代码可以看到以下结果:
INFO chenps3.httpclient.MultiHttpClientConnThread(36) - Thread Running:Thread-0
INFO chenps3.httpclient.MultiHttpClientConnThread(36) - Thread Running:Thread-1
INFO chenps3.httpclient.MultiHttpClientConnThread(36) - Thread Running:Thread-2
INFO chenps3.httpclient.MultiHttpClientConnThread(38) - Before - Leased Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(38) - Before - Leased Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(39) - Before - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(38) - Before - Leased Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(39) - Before - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(39) - Before - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(43) - After - Leased Connections = 2
INFO chenps3.httpclient.MultiHttpClientConnThread(43) - After - Leased Connections = 2
INFO chenps3.httpclient.MultiHttpClientConnThread(44) - After - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(44) - After - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(43) - After - Leased Connections = 1
INFO chenps3.httpclient.MultiHttpClientConnThread(44) - After - Available Connections = 1
可以看到,即使有3个线程的请求并发执行,最多只有2个连接被使用。没有拿到连接的线程则会暂时阻塞,直到有连接归还到连接池。
4 keep-alive策略
如果没有在响应头部找到keep-alive,HttpClient假定是无限大,因此通常需要自定义一个keep-alive策略。
//优先使用响应头的keep-alive值,如果没找到,设置为5秒
ConnectionKeepAliveStrategy strategy = new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) {
HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.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 5000;
}
};
//自定义策略应用到client
CloseableHttpClient client = HttpClients.custom()
.setKeepAliveStrategy(strategy)
.build();
5 连接持久化与复用
HTTP/1.1 规范中声明,如果连接没有被关闭,就可以被复用。HttpClient中,连接一旦被连接管理器释放,就会保持可复用的状态。
BasicHttpClientConnectionManager只能使用一个连接,因此使用前必须要先显式释放:
BasicHttpClientConnectionManager basic = new BasicHttpClientConnectionManager();
HttpClientContext ctx = HttpClientContext.create();
HttpGet get = new HttpGet("https://www.baidu.com");
//使用底层api实现一次请求
HttpRoute route = new HttpRoute(new HttpHost("www.baidu.com", 80));
ConnectionRequest request = basic.requestConnection(route, null);
HttpClientConnection connection = request.get(10, TimeUnit.SECONDS);
basic.connect(connection, route, 1000, ctx);
basic.routeComplete(connection, route, ctx);
HttpRequestExecutor executor = new HttpRequestExecutor();
ctx.setTargetHost(new HttpHost("www.baidu.com", 80));
executor.execute(get, connection, ctx);
//显式释放连接,允许被复用
basic.releaseConnection(connection, null, 1, TimeUnit.SECONDS);
//使用高层api实现一次请求
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(basic)
.build();
client.execute(get);
如果没有显式释放连接,执行最后一行代码会有以下异常:
Exception in thread "main" java.lang.IllegalStateException: Connection is still allocated
PoolingHttpClientConnectionManager 可以隐式释放连接。以下代码使用10个线程执行10个请求,共享5个连接:
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(5);
connectionManager.setMaxTotal(5);
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(connectionManager)
.build();
MultiHttpClientConnThread[] threads = new MultiHttpClientConnThread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
}
for (MultiHttpClientConnThread i : threads) {
i.start();
}
for (MultiHttpClientConnThread i : threads) {
i.join();
}
6 配置超时时间
虽然HttpClient支持设置多种超时时间,但通过连接管理器,只能设置socket的超时时间。
//设置socket超时时间为5秒
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setSocketConfig(
new HttpHost("www.baidu.com", 80),
SocketConfig.custom().setSoTimeout(5000).build());
7 连接驱逐
连接驱逐是指,探测空闲和过期的连接,并关闭它们。连接驱逐有两种实现方式:
- 在HttpClient执行请求前检测连接是否过期
- 使用一个监控线程来探测并关闭过期连接
//通过定义一个RequestConfig对象,令client在请求前检查连接是否过期,有性能损耗
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
RequestConfig requestConfig = RequestConfig.custom().setStaleConnectionCheckEnabled(true).build();
CloseableHttpClient client = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();
//定义一个监视器线程类,探测并关闭过期连接和超过30秒的空闲连接
public class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connectionManager;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(1000);
//关闭过期连接
connectionManager.closeExpiredConnections();
//关闭空闲超过30秒的连接
connectionManager.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException e) {
showdown();
}
}
private void showdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
8 关闭连接
正确关闭连接的步骤如下:
- 消费并关闭响应
- 关闭client对象
- 关闭connection manager对象
如果连接关闭之前就关闭掉了连接管理器,管理器所管理的所有连接和资源都会直接释放。
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(connectionManager)
.build();
HttpGet get = new HttpGet("https://www.baidu.com");
CloseableHttpResponse response = client.execute(get);
EntityUtils.consume(response.getEntity()); //消费响应
response.close(); //关闭响应
client.close(); //关闭client对象
connectionManager.close(); //关闭connection manager对象
参考资料
- https://www.baeldung.com/httpclient-connection-management
- http://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/connmgmt.html