HttpClient
在代码检查中,我写了一篇关于代码检查的文章http连接功能导致一个问题:是否释放httpGet.releaseConnection()
,是否重复连接以及如何重复使用。由于我以前没有做过相关的理解,只能使用基本的使用,所以我进行了深入的研究,整理了一些经验分享。欢迎积极评论和纠正不足。
:
通常使用服务和服务之间的调用和交互Http请求处理,HttpClient主要实现以下功能的常用框架:
(1)实现一切HTTP方法(GET、POST、PUT、DELETE等)
(2)支持自动转向
(3)支持HTTPS协议
(4)支持代理服务器
在进行更深入的学习和分析之前,先简单介绍一下httpclient
,jdk
内部提供HttpURLConnection
,对http
许多公司和组织使用请求等。Http
包装再开发,提供更方便的工具,如org.apache.httpcomponents
的httpclient
包,com.squareup.okhttp3
的okhttps
等等,本文介绍的是apache
的httpClient
。
一、请求类型
Http要求的基类是HttpRequestBase
继承了AbstractExecutionAwareRequest
并实现了类HttpUriRequest
和Configurable
可配置接口。
/**get*/ HttpGet, /**post*/ HttpPost, /**put*/ HttpPut, /**patch*/ HttpPatch, /**delete*/ HttpDelete, /**其他*/ HttpHead, HttpOptions, HttpRequestBase, HttpRequestWrapper, HttpTrace, RequestWrapper HttpEntityEnclosingRequestBase, EntityEnclosingRequestWrapper,
二、使用依赖
pom依赖:如果单独使用,可以引入apache
里面有对的包Http
封装后使用一些类别
<!--httpClient--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.2</version> </dependency>
三、参考文件
参考文档:
- HttpClient详细梳理 - 简书 (jianshu.com)
- CloseableHttpClient使用和优化_superiorpengFight的专栏-CSDN博客
- HttpClient Connection Management | Baeldung
- :httpclient结构原理介绍 & 连接池详解_u013332124的专栏-CSDN博客_httpclient连接池
四、使用
4.1 获取httpClient
使用依赖包HttpClients
来实例化CloseableHttpClient
,其中的HttpClientBuilder
是CloseableHttpClient
建造者(参考设计模式-建造者模式)。
@Immutable public class HttpClients {
private HttpClients() {
super(); } public static HttpClientBuilder custom() {
return HttpClientBuilder.create(); } public static CloseableHttpClient createDefault() {
return HttpClientBuilder.create().build(); } public static CloseableHttpClient createSystem() {
return HttpClientBuilder.create().useSystemProperties().build(); } public static CloseableHttpClient createMinimal() {
return new MinimalHttpClient(new PoolingHttpClientConnectionManager()); } public static CloseableHttpClient createMinimal(final HttpClientConnectionManager connManager) {
return new MinimalHttpClient(connManager); } }
使用
// 使用工厂类 HttpClients 进行创建 // 1、默认配置创建 CloseableHttpClient httpClient = HttpClients.createDefault(); // 2、使用 builder来创建,可以添加
自定义配置 // 自定义 connectionManager 连接管理器 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build();
4.2 相关配置
无论是采用的那种工厂方法实例化的CloseableHttpClient
,其中都会有很多的配置,主要的有两个HttpClientConnectionManager
和RequestConfig
4.2.1 HttpClientConnectionManager
HTTP连接管理器。它负责新HTTP连接的创建、管理连接的生命周期还有保证一个HTTP连接在某一时刻只被一个线程使用。
-
实现
BasicHttpClientConnectionManager
:每次只管理一个connection
。不过,虽然它是thread-safe的,但由于它只管理一个连接,所以只能被一个线程使用。它在管理连接的时候如果发现有相同route的请求,会复用之前已经创建的连接,如果新来的请求不能复用之前的连接,它会关闭现有的连接并重新打开它来响应新的请求。PoolingHttpClientConnectionManager
:它管理着一个连接池。它可以同时为多个线程服务。每次新来一个请求,如果在连接池中已经存在route相同并且可用的connection
,连接池就会直接复用这个connection
;当不存在route
相同的connection
,就新建一个connection
为之服务;如果连接池已满,则请求会等待直到被服务或者超时。
-
HttpClients.createDefault()
:默认创建的是PoolingHttpClientConnectionManager
-
默认配置
public PoolingHttpClientConnectionManager( final HttpClientConnectionOperator httpClientConnectionOperator, final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, final long timeToLive, final TimeUnit tunit) { super(); this.configData = new ConfigData(); this.pool = new CPool(new InternalConnectionFactory( this.configData, connFactory), 2, 20, timeToLive, tunit); this.pool.setValidateAfterInactivity(2000); this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator"); this.isShutDown = new AtomicBoolean(false); }
4.2.2 RequestConfig
-
HttpClient.defaultConfig
:默认配置参数Builder() { super(); // 确定是否要使用陈旧的连接检查。 陈旧的连接检查可能会导致每个请求最多 30 毫秒的开销,并且应该仅在适当的时候使用。 this.staleConnectionCheckEnabled = false; // 确定是否应自动处理重定向 this.redirectsEnabled = true; // 返回要遵循的最大重定向数。 重定向次数限制旨在防止无限循环 this.maxRedirects = 50; // 确定是否应拒绝相对重定向。 HTTP 规范要求位置值是绝对 URI this.relativeRedirectsAllowed = true; // 确定是否应自动处理身份验证 this.authenticationEnabled = true; // 返回从连接管理器请求连接时使用的超时时间(以毫秒为单位)。 默认值: -1,为无限超时。 this.connectionRequestTimeout = -1; // 确定建立连接之前的超时时间(以毫秒为单位)。 默认值: -1,为无限超时。 this.connectTimeout = -1; // 以毫秒为单位定义套接字超时,它是等待数据的超时,或者换句话说,两个连续数据包之间的最长不活动时间。默认值: -1,为无限超时。 this.socketTimeout = -1; // 确定是否请求目标服务器压缩内容 this.contentCompressionEnabled = true; }
-
:默认配置中有几个超时时间都是
-1
,这是无限超时的意思,为了更好的使用和管理,在使用的过程中需要对这几个参数进行设置,如果没有设置的话,请求会持续存在,也不会抛出异常,十分。-
connectionRequestTimeout
:返回从连接管理器请求连接时使用的超时时间 -
connectTimeout
:连接超时 -
socketTimeout
:读取数据超时
-
-
配置给
HttpClient
:所有该httpClient
执行的请求,如果没有指定配置,则都会采用该defaultRequestConfig
-
配置给
HttpRequest-methods
:配置了requestConfig
,在请求时使用该配置// 创建http 请求配置 RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5 * 1000) .setConnectionRequestTimeout(5 * 1000) .setSocketTimeout(5 * 1000) .build(); // 1.配置给CloseableHttpClient CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .build(); // 2.配置http GET请求 HttpGet httpGet = new HttpGet(url); httpGet.setConfig(requestConfig);
4.3 使用示例:GET
在需要进行请求时,创建httpClient
,然后创建HttpGet
请求,配置路由、请求头、请求参数等,接收execute
请求,获取结果并处理。
public static void test(String url) {
// 创建http client客户端
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建http 请求配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5 * 1000)
.setConnectionRequestTimeout(5 * 1000)
.setSocketTimeout(5 * 1000)
.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);
// 设置请求头部编码
httpGet.setHeader(new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"));
// 设置返回编码
httpGet.setHeader(new BasicHeader("Accept", "text/plain;charset=utf-8"));
// 返回响应
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
// 判断响应码
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
// 使用工具类EntityUtils 从响应中读取内容
String result = EntityUtils.toString(entity, "utf-8");
System.out.println(result);
}
} catch (Exception e) {
System.out.print("http GET 请求异常" + e);
} finally {
// 释放资源
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
System.out.print("关闭流异常" + e);
}
// 关闭客户端
try {
httpClient.close();
} catch (IOException e) {
System.out.print("关闭HttpClient异常" + e);
}
}
}
五、问题探讨
示例中有多个Close
,分别是关闭了什么呢,是否可以省略,又在什么时候调用呢?在使用过程中,有时也会涉及到releaseConnection()
,这又是什么?有什么作用?是否是必要的呢?
5.1 关闭
-
response.close()
:官网的解释是,最底层的HTTP connection
是由响应对象response
持有的,如果没有完全的消费response content
或者正确地关闭,对应的connection
是不能被安全重用的,会被connection manager
给关闭和丢弃。 -
httpClient.close()
:关闭客户端,会先关闭客户端中的所有连接,然后销毁客户端。 -
method.releaseConnection()
:释放连接到连接池。
5.2 不关闭
所有的资源都是有限的,如果持续消费资源而不释放资源,很快就会出现因为资源获取不到而导致进程阻塞,参考一个常见的问题就是**死
锁问题
并发量**、
在HttpClient
使用过程中也会出现这样的问题,下面我们来探讨一下,如果不关闭资源,会出现什么样的问题,不同的方式来关闭又会出现什么样的问题。
5.3 response
问题:消费不彻底
多次请求,对于response
消费不彻底,没有进行关闭
// 相同的URL,多次请求
public static void testNoCloseResponse(String url, int num) {
// 创建http client客户端
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
// 设置请求头部编码
httpGet.setHeader(new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"));
// 设置返回编码
httpGet.setHeader(new BasicHeader("Accept", "text/plain;charset=utf-8"));
for (int i = 0; i < num; i++) {
// 返回响应
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
// if (response.getStatusLine().getStatusCode() == 200) {
// HttpEntity entity = response.getEntity();
// // 使用工具类EntityUtils 从响应中读取内容
// String result = EntityUtils.toString(entity, "utf-8");
// System.out.println(result);
// }
} catch (Exception e) {
System.out.print("http GET 请求异常" + e);
}finally{
// // 释放资源
// try {
// if (response != null) {
// response.close();
// }
// } catch (IOException e) {
// e.printStackTrace();
// }
}
}
}
第一次连接:连接无法被复用,kept alive 0,同时占用了一个route
。
16:37:15.048 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection request: [route: {}->http://172.23.22.58:8081][total kept alive: 0; route allocated: 0 of 2; total allocated: 0 of 20]
16:37:15.119 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection leased: [id: 0][route: {}->http://172.23.22.58:8081][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20]
// 连接完成后
httpClient.connManager.pool = [leased: [[id:0][route:{}->http://172.23.22.58:8081][state:null]]][available: []][pending: []]
第二次连接:连接无法被复用,kept alive 0,相同的IP
和请求路由,又占用了一个route
。
16:41:57.223 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection request: [route: {}->http://172.23.22.58:8081][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20]
16:41:57.224 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection leased: [id: 1][route: {}->http://172.23.22.58:8081][total kept alive: 0; route allocated: 2 of 2; total allocated: 2 of 20]
// 连接完成后
httpClient.connManager.pool = [leased: [[id:1][route:{}->http://172.23.22.58:8081][state:null], [id:0][route:{}->http://172.23.22.58:8081][state:null]]][available: []][pending: []]
第三次连接:相同的IP
和请求路由,由于默认配置中:httpClient.connManager.pool.defaultMaxPerRoute = 2
(相同的请求路径最多可以同时存在2个),没有可用的route
此时就会一直等待原连接的释放,获取到route
之后才可以进行连接。
问题:消费彻底
使用工具类消费能够更加彻底地消费response
,可以达到释放资源,复用的效果,但是如果关闭response
,仍然无法复用
// 返回响应
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
// if (response.getStatusLine().getStatusCode() == 200) {
// HttpEntity entity = response.getEntity();
// // 使用工具类EntityUtils 从响应中读取内容
// String result = EntityUtils.toString(entity, "utf-8");
// System.out.println(result);
// }
} catch (Exception e) {
System.out.print("http GET 请求异常" + e);
}
第一次连接:total kept alive: 1,连接可以被复用
16:56:23.188 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection [id: 0][route: {
}->http://172.23.22.58:8081] can be kept alive indefinitely
16:56:23.188 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection released: [id: 0][route: {
}->http://172.23.22.58:8081][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 20]
第二次连接:虽然有一个连接可以复用,但是在尝试复用的时候,发现该通道对应的流并没有关闭,无法使用,所以在关闭了该连接后,重新生成了一个
16:57:32.922 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection request: [route: {
}->http://172.23.22.58:8081][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 20]
16:57:32.922 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "end of stream"
16:57:32.922 [main] DEBUG org.apache.http.impl.conn.DefaultManagedHttpClientConnection - http-outgoing-0: Close connection
16:57:32.922 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection leased: [id: 1][route: {
}->http://172.23.22.58:8081][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20]
16:57:32.922 [main] DEBUG org.apache.http.impl.execchain.MainClientExec - Opening connection {
}->http://172.23.22.58:8081
第三次连接:与第二次相同
17:05:07.966 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection request: [route: {
}->http://172.23.22.58:8081][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 20]
17:05:07.966 [main] DEBUG org.apache.http.wire - http-outgoing-1 << "end of stream"
17:05:07.966 [main] DEBUG org.apache.http.impl.conn.DefaultManagedHttpClientConnection - http-outgoing-1: Close connection
17:05:07.966 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection leased: [id: 2][route: {
}->http://172.23.22.58:8081][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20<