首先我们需要知道,HttpClient是不建议每次使用都创建的,因为它本身就带一个连接池。如果我们使用频繁的话,频繁创建HttpClient对象也不是明智的。
我在使用HttpClient的时候,在一个调用链中,只是依次使用了HttpClient调用了几次http接口,却发现在第5个的时候,报错了
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
这是我当时使用的配置:
private static HttpClient client;
static {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(3000)
.setSocketTimeout(5000).build();
client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.build();
}
查了下才知道这个错,是因为连接池没有空闲连接,并且等待获取连接的时间达到了setConnectionRequestTimeout
,我很疑惑为啥才一个单线程使用了几下就报错了呢?于是我开始了测试。
这里先说结论
HttpClient默认连接池,最大连接是20个,最大同路由的是2个,也就是maxConnTotal是20,maxConnPerRoute是2。
maxConnTotal是同时间正在使用的最多的连接数
maxConnPerRoute是针对一个域名同时间正在使用的最多的连接数
可以看org.apache.http.impl.conn.PoolingHttpClientConnectionManager
类,
public PoolingHttpClientConnectionManager(HttpClientConnectionOperator httpClientConnectionOperator, HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, long timeToLive, TimeUnit timeUnit) {
this.log = LogFactory.getLog(this.getClass());
this.configData = new PoolingHttpClientConnectionManager.ConfigData();
this.pool = new CPool(new PoolingHttpClientConnectionManager.InternalConnectionFactory(this.configData, connFactory), 2, 20, timeToLive, timeUnit);
this.pool.setValidateAfterInactivity(2000);
this.connectionOperator = (HttpClientConnectionOperator)Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
this.isShutDown = new AtomicBoolean(false);
}
先前5个调用百度,后5次调用hao123
package com.zgd.demo.web.test;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import java.io.IOException;
/**
* @Author: zgd
* @Date: 2019/8/15 00:04
* @Description: 测试httpclient
*/
@Slf4j
public class MyHttpClientTest {
private static HttpClient client;
static {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(3000)
.setSocketTimeout(5000).build();
client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.build();
}
@Test
public void fun01() {
for (int i = 0; i < 10; i++) {
String url = "";
if (i < 5){
url = "http://www.baidu.com";
}else {
url = "http://www.hao123.com";
}
HttpGet httpGet = new HttpGet(url);
log.info("------url:{}-----",url);
HttpResponse res = null;
try {
res = client.execute(httpGet);
} catch (IOException e) {
log.error("异常: ",e);
return;
}
int code = res.getStatusLine().getStatusCode();
log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
}
}
}
运行结果
在第3个,i=2的时候就跑这个异常了,在debug级别日志打印出total kept alive: 0; route allocated: 2 of 2; total allocated: 2 of 20
,也就是这个route下一共2个连接,已经使用2个。整个pool有20个连接,使用了2个
依次调用百度和hao123
@Test
public void fun01() {
for (int i = 0; i < 10; i++) {
String url = "";
if (i % 2 == 0){
url = "http://www.baidu.com";
}else {
url = "http://www.hao123.com";
}
HttpGet httpGet = new HttpGet(url);
log.info("------url:{}-----",url);
HttpResponse res = null;
try {
res = client.execute(httpGet);
} catch (IOException e) {
log.error("异常: ",e);
return;
}
int code = res.getStatusLine().getStatusCode();
log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
}
}
我们修改代码如下
@Test
public void fun01() {
for (int i = 0; i < 10; i++) {
String url = "";
if (i % 2 == 0){
url = "http://www.baidu.com";
}else {
url = "http://www.hao123.com";
}
HttpGet httpGet = new HttpGet(url);
log.info("------url:{}-----",url);
HttpResponse res = null;
try {
res = client.execute(httpGet);
int code = res.getStatusLine().getStatusCode();
log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
} catch (IOException e) {
log.error("异常: ",e);
return;
}finally {
if (res != null){
EntityUtils.consumeQuietly(res.getEntity());
}
}
}
}
不管是先百度后好123,还是依次调用,都完全ok了
这里使用了HttpClient提供的工具类EntityUtils
来处理,EntityUtils的consume
方法都对连接资源进行了释放。
public static void consumeQuietly(HttpEntity entity) {
try {
consume(entity);
} catch (IOException var2) {
}
}
...
public static void consume(HttpEntity entity) throws IOException {
if (entity != null) {
if (entity.isStreaming()) {
InputStream inStream = entity.getContent();
if (inStream != null) {
inStream.close();
}
}
}
}
可以看出,出现Timeout waiting for connection from pool异常的原因主要有二:
可以看到第1条,是老生常谈的资源关闭问题。博主在开发的时候,正是因为不需要知道调用的返回结果,所以没有对Response进行处理,所以资源并没有释放,导致后面再使用HttpClient,就拿不到连接了
第2条,需要知道maxConnTotal和maxConnPerRoute的具体含义,如果是需要频繁调用同一个域名,那么即使maxConnTotal设置再大,还是受限制与maxConnPerRoute
根据业务实际情况合理配置连接池
private static HttpClient client;
static {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(1000)
.setSocketTimeout(5000).build();
client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.setMaxConnTotal(200)
.setMaxConnPerRoute(50)
.build();
}