序
HttpClient可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
使用HttpClient发送请求和接收响应的步骤:
- 创建CloseableHttpClient对象;
- 创建请求方法实例,并指定请求URL。例:如果要发送Get请求,创建HttpGet对象;如果要发送POST请求,创建HttpPost对象;
- 如果需要发送参数,则调用setEntity(HttpEntity entity)方法来设置参数;
- 调用HttpGet/HttpPost对象的setHeader(String name,String value)方法设置header信息,或者调用setHeader(Header[] headers)设置一组header参数;
- 调用CloseableHttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个CloseableHttpResponse;
- 调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容;调用CloseableHttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;
- 释放连接。无论执行方法是否成功,都必须释放连接
1. 引入Maven依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
1. HttpClient连接池分析
PoolingHttpClientConnectionManager是一个HttpClientConnection的连接池,可以为多线程提供并发请求服务。主要是分配连接,回收连接。同一个远程请求,会优先使用连接池提供的空闲的长连接。
源码位置:org.apache.http.impl.conn.PoolingHttpClientConnectionManager
默认构造方法:
/**
* @since 4.4
*/
public PoolingHttpClientConnectionManager(
final HttpClientConnectionOperator httpClientConnectionOperator,
final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
final long timeToLive, final TimeUnit timeUnit) {
super();
this.configData = new ConfigData();
//连接池的默认配置defaultMaxPerRoute默认为2,maxTotal默认为20
this.pool = new CPool(new InternalConnectionFactory(
this.configData, connFactory), 2, 20, timeToLive, timeUnit);
//官方推荐使用这个来检查永久链接的可用性,而不推荐每次请求的时候才去检查
this.pool.setValidateAfterInactivity(2000);
this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
this.isShutDown = new AtomicBoolean(false);
}
- maxTotal:连接池的最大连接数。
- defaultMaxPreRount:每个Rount(远程)请求最大的连接数。
- setValidateAfterInactivity:连接空闲多长时间(单位:毫秒)进行检查。
显示的调整连接池参数:
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
2. SpringBoot集成HttpClient
2.1 超时时间设置
httpClient内部有三个超时时间设置:获取连接的超时时间、建立连接的超时时间、读取数据超时时间。
//设置网络配置器
@Bean
public RequestConfig requestConfig(){
return RequestConfig.custom().setConnectionRequestTimeout(2000) //从链接池获取连接的超时时间
.setConnectTimeout(2000) //与服务器连接超时时间,创建socket连接的超时时间
.setSocketTimeout(2000) //socket读取数据的超时时间,从服务器获取数据的超时时间
.build();
}
1. 从连接池中获取可用连接超时
HttpClient中的要用连接时尝试从连接池中获取,若是在等待了一定的时间后还没有获取到可用连接(比如连接池中没有空闲连接了)则会抛出获取连接超时异常。
2. 连接目标超时connectionTimeout
指的是连接目标url的连接超时时间,即客服端发送请求到与目标url建立起连接的最大时间。如果在该时间范围内还没有建立起连接,则就抛出connectionTimeOut异常。
如测试的时候,将url改为一个不存在的url:“http://test.com” , 超时时间3000ms过后,系统报出异常: org.apache.commons.httpclient.ConnectTimeoutException:The host did not accept the connection within timeout of 3000 ms
3. 等待响应超时(读取数据超时)socketTimeout
连接上一个url后,获取response的返回等待时间 ,即在与目标url建立连接后,等待放回response的最大时间,在规定时间内没有返回响应的话就抛出SocketTimeout。
测试的时候的连接url为我本地开启的一个url,http://localhost:8080/firstTest.htm?method=test,在我这个测试url里,当访问到这个链接时,线程sleep一段时间,来模拟返回response超时。
2.2 KeepAliveStrategy策略
keep-alive详解 —— 通过使用Keep-alive机制,可以减少tcp连接建立的次数,也以为这可以减少TIME_WAIT状态连接,以此提高性能和提高HTTP服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()调用)。但是长时间的tcp连接容易导致系统资源无效占用,配置不当的Keep-alive有事比重复利用连接带来的损失还更大。所以正确地设置Keep-alive timeout时间非常重要。
Keep-alive:timeout=5,max=100
的含义。
意思是说:过期时间5秒,max是最多100次请求,强制断掉连接,也就是在timeout时间内每来一个新的请求,max会自动减1,直到为0,强制断掉连接。
需要注意的是:使用keep-alive要根据业务情况来定,若是少数固定客户端,长时间高频次的访问服务器,启用keep-client非常合适!
在HttpClient中默认的keepClient策略:
org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy
默认的话,是读取response中的keep-alive中的timeout参数,若是没有读到,那么设置为-1,这个代表无穷,但是这样设置便存在问题。因为现实中的HTTP服务器配置了在特定不活动周期之后丢掉连接来保存系统资源,往往是不通知客户端的。
默认的keep-alive策略
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy();
@Override
public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
Args.notNull(response, "HTTP response");
final HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
final HeaderElement he = it.nextElement();
final String param = he.getName();
final String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch(final NumberFormatException ignore) {
}
}
}
return -1;
}
}
解决方案:可以自定义keep-alive策略,如果没有读到,则设置保存连接为60s。
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//设置连接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//设置超时时间
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定义连接管理器将由多个客户端实例共享。如果连接管理器是共享的,则其生命周期应由调用者管理,如果客户端关闭则不会关闭。
httpClientBuilder.setConnectionManagerShared(true);
//设置KeepAlive
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
// Honor 'keep-alive' header
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")) {
try {
return Long.parseLong(value) * 1000;
} catch(NumberFormatException ignore) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(
HttpClientContext.HTTP_TARGET_HOST);
if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// Keep alive for 5 seconds only
return 5 * 1000;
} else {
// otherwise keep alive for 30 seconds
return 30 * 1000;
}
}
};
httpClientBuilder.setKeepAliveStrategy(myStrategy);
return httpClientBuilder;
}
2.3 Connection eviction policy(连接逐出策略)
当一个连接被释放到连接池时,它可以保持活动状态而不能监控socket的状态和任何I/O事件。如果连接在服务器端被关闭,那么客户端连接也不能侦测连接状态中的变化和关闭本端的套接字去做出适当响应。
HttpClient尝试通过测试连接是否有效来解决该问题,但是它在服务器端关闭,失效的连接检查不是100%可靠。唯一的解决方案:创建监控线程来回收因为长时间不活动而被认为过期的连接。
public class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
监控线程可以周期地调用ClientConnectionManager#closeExpiredConnections()
方法来关闭所有过期的连接,从连接池中收回关闭的连接。它也可以选择性调用ClientConnectionManager#closeIdleConnections()
方法来关闭所有已经空闲超过给定时间周期的连接。httpclient参数配置
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//设置连接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//设置超时时间
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定义连接管理器将由多个客户端实例共享。如果连接管理器是共享的,则其生命周期应由调用者管理,如果客户端关闭则不会关闭。
httpClientBuilder.setConnectionManagerShared(true);
//启动线程,5秒钟清空一次失效连接
new IdleConnectionMonitorThread(poolingHttpClientConnectionManager).start();
return httpClientBuilder;
}
2.4 HttpClient的重试机制
HttpClient使用连接池PoolingHttpClientConnectionManager
设置重试策略:org.apache.http.impl.client.DefaultHttpRequestRetryHandler
重试机制的源码:org.apache.http.impl.execchain.RetryExec#execute
在默认情况下,httpClient会使用默认的重试策略
DefaultHttpRequestRetryHandler
(不管你设置不设置)。
默认策略的构造方法:
public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
this(retryCount, requestSentRetryEnabled, Arrays.asList(
InterruptedIOException.class,
UnknownHostException.class,
ConnectException.class,
SSLException.class));
}
- retryCount:重试次数;
- requestSentRetryEnabled:如果一个请求重试成功,是否还会被再次重试;
- InterruptedIOException、UnknownHostException、ConnectException、SSLException,发生这4中异常(以及子类异常)不重试;
默认重试策略的校验方法:org.apache.http.impl.client.DefaultHttpRequestRetryHandler # retryRequest
@Override
public boolean retryRequest(
final IOException exception,
final int executionCount,
final HttpContext context) {
Args.notNull(exception, "Exception parameter");
Args.notNull(context, "HTTP context");
if (executionCount > this.retryCount) {
// Do not retry if over max retry count
return false;
}
if (this.nonRetriableClasses.contains(exception.getClass())) {
return false;
}
for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
if (rejectException.isInstance(exception)) {
return false;
}
}
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final HttpRequest request = clientContext.getRequest();
//同一个请求在异步任务重已经被终止,则不进行重试
if(requestIsAborted(request)){
return false;
}
//如果请求是幂等的,get请求,便可以重试
if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}
//如果请求未发送成功,或者允许发送成功依旧可以发送,便可以重试
if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
// Retry if the request has not been sent fully or
// if it's OK to retry methods that have been sent
return true;
}
// otherwise do not retry
return false;
}
关于默认的重试策略:
- 如果重试超过3次,则不进行重试;
- 如果重试是特使异常及其子类,则不重试;
- 同一个请求在异步任务被终止,则不请求;
- 幂等的方法可以进行重试,比如Get;
- 如果请求未被发送成功,可以被重试;
如何判断请求是否发送成功?
源码:org.apache.http.protocol.HttpCoreContext # isRequestSent
根据http.request_sent
参数来判断是否发送成功。
RetryExec底层通信使用的是MainClientExec,而MainClientExec底层便调用的是HttpRequestExecutor.doSendRequest()。
故http.request_sent
参数的设置,是通过HttpRequestExecutor.doSendRequest()方法设置的。
不重试的异常
- InterruptedIOException,线程中断异常
- UnknownHostException,找不到对应host
- ConnectException,找到了host但是建立连接失败。
- SSLException,https认证异常
另外,我们还经常会提到两种超时,连接超时与读超时:
- java.net.SocketTimeoutException: Read timed out
- java.net.SocketTimeoutException: connect timed out
这两种超时都是SocketTimeoutException,继承自InterruptedIOException,属于上面的第1种线程中断异常,不会进行重试。
如何禁止重试
在HttpClinetBuilder中,其Build()方法中选择了RetryExec执行器时,是默认开启重试策略。
故我们可以在构建httpClient实例的时候手动禁止掉即可。
httpClientBuilder.disableAutomaticRetries();
如何自定义重试策略
只需要实现org.apache.http.client.HttpRequestRetryHandler
接口,重新里面的方法即可。
而重试策略的源码是在org.apache.http.impl.execchain.RetryExec#execute
实现的。
httpClientBuilder.setRetryHandler(new MyHttpRequestRetryHandler());
2.5 设置个性化的请求参数
因为我们在配置文件中,配置了默认的socketTimeout(建立连接的最大时间,即响应超时时间),但是实际业务中,不同的请求有着不同的响应超时时间。如何为不同的业务设置不同的超时时间呢?
我们知道,实际上我们注入的CloseableHttpClient
是一个抽象类,实际上,他将org.apache.http.impl.client.InternalHttpClient
类型注入进来,那么在我们使用org.apache.http.client.methods.HttpRequestBase
(注:httpPost/httpGet的共同父类)发送请求时,可以单独的设置RequestConfig
参数。
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());获取RequestConfig.Builder对象,以便设置个性化参数。
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//设置超时时间
if (socketTimeout > 0) {
//获取原有配置
//实际注入类型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//设置个性化配置
RequestConfig config = custom.setSocketTimeout(socketTimeout).build();
request.setConfig(config);
}
ResponseHandler<String> handler = new BasicResponseHandler();
String response = httpClient.execute(request, handler);
return response;
}
}
2.6 HttpClient响应数据处理
EntityUtils.consume将释放httpEntity持有的所有资源,这实际上意味着释放任何基础流并将连接对象放回到池中(在连接池时多线程的情况下),或者释放连接管理器以便处理下一个请求。
源码:org.apache.http.impl.client.CloseableHttpClient # execute
若是获取自定义响应实体,则实现org.apache.http.client.ResponseHandler
接口。
处理响应的方法:
@Test
public void test1() throws IOException, InterruptedException {
HttpPost httpPost = new HttpPost("http://www.baidu.com");
httpPost.setConfig(requestConfig);
Map<String, String> innerReq = new HashMap<>();
innerReq.put("XX", "data1");
innerReq.put("YY", "data2");
String innerReqJson = JSONObject.toJSONString(innerReq);
StringEntity entity = new StringEntity(innerReqJson, "UTF-8");
httpPost.addHeader("content-type", "application/json;charset=UTF-8");
httpPost.setEntity(entity);
//执行请求
CloseableHttpResponse execute = closeableHttpClient.execute(httpPost);
//设置返回数据
String res = EntityUtils.toString(execute.getEntity(), "UTF-8");
//关闭资源
EntityUtils.consume(execute.getEntity());
log.info(res);
}
关闭资源
为什么笔者使用EntityUtils.consume(httpEntity);?(Why did the author use EntityUtils.consume(httpEntity);?)
EntityUtils.consume(execute.getEntity());
(新)使用ResponseHandler处理响应数据
无论请求执行成功还是导致异常,HttpClient都会自动确保将连接释放回连接管理器。
@Test
public void test() throws IOException, InterruptedException {
HttpPost httpPost = new HttpPost("http://www.baidu.com");
httpPost.setConfig(requestConfig);
Map<String, String> innerReq = new HashMap<>();
innerReq.put("XX", "data1");
innerReq.put("YY", "data2");
String innerReqJson = JSONObject.toJSONString(innerReq);
StringEntity entity = new StringEntity(innerReqJson, "UTF-8");
httpPost.addHeader("content-type", "application/json;charset=UTF-8");
httpPost.setEntity(entity);
//自定义ResponseHandler
ResponseHandler<ResponseVo> handler = new ResponseHandler<ResponseVo>() {
@Override
public ResponseVo handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
final StatusLine statusLine = response.getStatusLine();
final HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
EntityUtils.consume(entity);
throw new HttpResponseException(statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
if (entity == null) {
throw new ClientProtocolException("异常!");
}
String res = EntityUtils.toString(entity);
ResponseVo responseVo = JSON.parseObject(res, ResponseVo.class);
return responseVo;
}
};
//无论请求执行成功还是导致异常,HttpClient都会自动确保将连接释放回连接管理器。
ResponseHandler<String> responseHandler = new BasicResponseHandler();
// String execute1 = closeableHttpClient.execute(httpPost, responseHandler);
ResponseVo execute = closeableHttpClient.execute(httpPost, handler);
log.info(JSON.toJSONString(execute));
}
附录:
httpClient配置:
@Configuration
public class HttpClientConfig {
@Autowired
private HttpClientProperties httpClientProperties;
/**
* 显示修改httpClient连接池参数,注:若未显示设置,应该有默认配置!
*
* @return
*/
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
//创建出来的对象,已经设置了:协议Http和Https对应的处理Socket链接工厂对象。
PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
httpClientConnectionManager.setDefaultMaxPerRoute(httpClientProperties.getDefaultMaxPerRoute());
httpClientConnectionManager.setMaxTotal(httpClientProperties.getMaxTotal());
httpClientConnectionManager.setValidateAfterInactivity(httpClientProperties.getValidateAfterInactivity());
return httpClientConnectionManager;
}
//设置网络配置器
@Bean
public RequestConfig requestConfig(){
return RequestConfig.custom().setConnectionRequestTimeout(httpClientProperties.getConnectionRequestTimeout()) //从链接池获取连接的超时时间
.setConnectTimeout(httpClientProperties.getConnectTimeout()) //与服务器连接超时时间,创建socket连接的超时时间
.setSocketTimeout(httpClientProperties.getSocketTimeout()) //socket读取数据的超时时间,从服务器获取数据的超时时间
// .setSocketTimeout(1) //socket读取数据的超时时间,从服务器获取数据的超时时间
// .setExpectContinueEnabled(true) //设置是否开启 客户端在发送Request Message之前,先判断服务器是否愿意接受客户端发送的消息主体
.build();
}
/**
* 实例化连接池,设置连接池管理器
*
* @param poolingHttpClientConnectionManager
* @return
*/
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//设置连接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//设置超时时间
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定义连接管理器将由多个客户端实例共享。如果连接管理器是共享的,则其生命周期应由调用者管理,如果客户端关闭则不会关闭。
httpClientBuilder.setConnectionManagerShared(true);
//设置Keep-Alive
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
// Honor 'keep-alive' header
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")) {
try {
return Long.parseLong(value) * 1000;
} catch(NumberFormatException ignore) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(
HttpClientContext.HTTP_TARGET_HOST);
if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// Keep alive for 5 seconds only
return 5 * 1000;
} else {
// otherwise keep alive for 30 seconds
return 30 * 1000;
}
}
};
httpClientBuilder.setKeepAliveStrategy(myStrategy);
// httpClientBuilder.setRetryHandler(new MyHttpRequestRetryHandler());
// httpClientBuilder.disableAutomaticRetries();
new IdleConnectionMonitorThread(poolingHttpClientConnectionManager).start();//启动线程,5秒钟清空一次失效连接
return httpClientBuilder;
}
@Bean
public CloseableHttpClient getCloseableHttpClient(HttpClientBuilder httpClientBuilder) {
return httpClientBuilder.build();
}
}
定时清除线程
@Slf4j
public class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
log.info("【定时清除过期连接开始...】");
// 关闭超时的连接
connMgr.closeExpiredConnections();
// 关闭空闲时间大于30s的连接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
文章参考
1. 官方文档
类PoolingHttpClientConnectionManager 官网API文档
httpclient源码分析之 PoolingHttpClientConnectionManager 获取连接
2. 相关博客
使用PoolingHttpClientConnectionManager解决友…
HttpClient.DefaultRequestHeaders.ExpectContinue。 ExpectContinue的用途是什么,在什么条件下它被设置为true或false。
理解HTTP协议中的 Expect: 100-continue
java.lang.IllegalStateException: Connection pool shut down 的解决方案
网友评论