HttpClient说明及连接池管理

作者:老胡

编辑:阿兵

我们的系统中有太多关于Http请求的调用场景,比如调用第三方支付,第三方征信服务,对接各大银行前置机以及SpringCloud各服务调用等等,常常会使用一个重要的开源框架HttpClient,然而版本的不同,各种使用五花八门,在生产中经常出现问题,又不知道如何解,所谓知其然不知其所以然。

本次我们就邀请了融资平台-清结算组技术专家-老胡同学给大家分享下关于HttpClient使用中各种不规范到规范写法,这次也是整理老胡同学在“周四见”关于HttpClient使用分享,并对HttpClient连接池管理进行说明。

HttpClient是基于标准、纯净的java语言,实现了Http1.0和Http1.1,HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。它实现了HTTP标准中Client端的所有功能,使用它能够很容易地进行HTTP信息的传输。HttpClient目前最新版本是4.5.5,但是很多项目依然在使用4.3之前的版本,HttpClient前期几个版本更新,每个版本对API都有改动,所以使用起来不太友好。

一、HttpClient历史版本

HttpClient前期的基本版本变化较大,不同HttpClient版本其请求发送的方式也不一样,下面来做个归纳:

HttpClient3.x

HttpClient4.x到HttpClient4.3以下

HttpClient4.3以上

3.X.X版本,创建Client和设置超时时间

HttpClient client = new HttpClient();client.setConnectionTimeout(30000);  client.setTimeout(30000);

4.X~4.3版本,创建Client和设置超时时间

HttpClient httpClient=new DefaultHttpClient();

//连接时间httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,2000);

//数据传输时间httpClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT,2000);

4.3以后版本,创建Client和设置超时时间

CloseableHttpClient httpClient = HttpClients.createDefault();HttpGet httpGet=new HttpGet("");//HTTP Get请求(POST雷同)RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(2000)

.setConnectTimeout(2000).build();//设置请求和传输超时时间

httpGet.setConfig(requestConfig);httpClient.execute(httpGet);//执行请求

注,以下所有代码示例,及说明全部是基于4.5.5最新版本说明

二、HttpClient使用步骤

使用HttpClient发送请求、接收响应,一般需要如下几个步骤。

1. 创建HttpClient对象。

2. 创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象。

3. 如果需要发送请求参数,HttpGet请求,使用 NameValuePair 来拼接URI。HttpPost使用setEntity(HttpEntity var1)方法来添加请求参数;

4. 调用HttpClient对象的execute(HttpUriRequest var1)发送请求,该方法返回一个HttpResponse。

5. 调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。

6. 释放连接。无论执行方法是否成功,都必须释放连接

三、HttpClient概览

1、核心接口HttpClient,有三个实现类,其中AutoRetryHttpClient 和 DecompressingHttpClient 都被标记为 Deprecated,只有CloseableHttpClient被推荐使用。

核心实现类CloseableHttpClient也有三种实现,其中 MinimalHttpClient 是一个极简版的实现,是核心设计的最直接简单的提现。而 InternalHttpClient 则是一个完全可配置版本,提供最丰富完善的功能。HttpClient类继承关系如下:

2、目前最新版的HttpClient的实现类为CloseableHttpClient。创建CloseableHttpClient实例有两种方式:

(1)使用CloseableHttpClient的工厂类HttpClients的方法来创建实例,HttpClients提供了根据各种默认配置来创建CloseableHttpClient实例的方法,最简单的实例化方式是调用HttpClients.createDefault()。

(2)使用CloseableHttpClient的builder类HttpClientBuilder,对一些属性进行配置(采用装饰者模式,不断的.setxx().setxx()就行了),再调用build方法来创建实例。另外,createDefault()最终调用的也是HttpClientBuilder.create().build()。

build()方法最终是根据各种配置来new一个InternalHttpClient实例。IternalHttpClient的定义如下:

其中最重要的三个:HttpClientConnectionManager、HttpRoutePlanner、RequestConfig,接下来会主要介绍下。

(1)HttpClient不仅支持简单的直连,也支持复杂的路由及代理。HttpRoutePlanner是基于http上下文情况下,客户端到服务器的路由计算策略,一般没有代理的话,就不用配置。这里有一个很关键的概念—Route,一个Route指当前机器->目标host的一条线路,也就是如果目标URL的host是同一个,那么它们的route也是一样的。

(2)HttpClientConnectionManager是一个HTTP连接管理器。它负责管理HTTP连接的创建,以及连接的生命周期,保证一个HTTP连接在某一时刻只被一个线程使用。HttpClientConnectionManager有两种具体实现如下:

a、BasicHttpClientConnectionManager

BasicHttpClientConnectionManager每次只管理一个connection,所以只能被一个线程使用。当新的请求进来,并且route与上一个相同的时候,连接会被重用,否则,它会关闭现有的连接并创建新的连接来响应新的请求。

b、PoolingHttpClientConnectionManager

使用HttpClients.createDefault()来创建client实例,将使用PoolingHttpClientConnectionManager来管理连接,PoolingHttpClientConnectionManager的配置中有两个最大连接数量,分别控制着总的最大连接数量和每个route的最大连接数量,最大路由连接数MaxPerRoute默认为2,总连接数量MaxTotal默认为20,也就是连接池中总的connection数量不超过20。每次新来一个请求,如果连接池中已经存在相同route并且可用的connection,连接池就会直接复用这个connection,当不存在route相同的connection,就会新建一个connection为之服务;如果连接池已满,则请求会等待直到被服务或者超时。两个连接数最好根据使用场景的不用设置合理的值,如果所有的请求目标host是同一个的话,MaxTotal和MaxPerRoute的数量应该设置同一个,增加最大并发请求数量。

(3)RequestConfig是对request的一些配置,里面比较重要的有三个超时时间,  三个超时时间也是相当重要的,默认的情况下这三个超时时间都为0(如果不设置request的Config,会在execute的过程中使用HttpClientParamConfig的getRequestConfig中用默认参数进行设置),这也就意味着无限等待,很容易导致所有的请求阻塞在这个地方无限期等待。三个超时时间分别为:

a、connectionRequestTimeout-获取连接的超时时间

指的是从连接池中取出连接的超时时间,如果连接池中没有可用的连接,则request会被阻塞,最长等待connectionRequestTimeout的时间,如果还没有被服务,则抛出ConnectionPoolTimeoutException异常,不继续等待。若connectionRequestTimeout默认为0,则无限期等待。

b、connectTimeout-连接超时时间

指的是与服务器建立连接的超时时间,也就是取得连接之后到接通目标URL的等待时间。建立连接超过connectTimeout时间,则会抛出ConnectionTimeoutException异常。若connectTimeout默认为0,则无限期等待。

c、socketTimeout-请求超时时间

指的是socket读数据的超时时间,也就是连接到服务器之后,从服务器获取响应数据需要等待的时间,或者说连接上一个URL之后到获取response的返回等待时间。发生超时,会抛出SocketTimeoutException异常。。若socketTimeout默认为0,则无限期等待。

四、释放连接

在请求结束之后,想要复用connection连接,无论请求结果是否成功,都必须在执行完请求之后,把连接正确的释放到连接池中。释放连接的方式有两种,如下

1、instream.close();

在主动操作输入流,或者调用EntityUtils.toString(toString(HttpEntity entity, String str))时会调用instream.read()、instream.close()等方法,如果流已经读完,且连接可重用,就会尝试释放连接,否则关闭连接。

在每次通过instream.read()读取数据流后,都会调用checkEOF方法判断流是否读取结束,并在流读取完成后调用ConnectionHolder.releaseConnection()来释放连接,若连接可重用,则将连接释放到连接池中;否则将关闭连接。(具体调用过程有兴趣的同学可以跟下)

instream.close()方法最终将调用ConnectionHolder.releaseConnection()方法,部分源码如下:

2、response.close();

调用response.close()方法,将最终调用ConnectionHolder.releaseConnection()方法,方法源码如上图所示。若在方法之前,若正常释放了连接,则released=true,调用response.close()不做任何操作;若没有操作过输入流,也没有调用instream.close()来关闭输入流,那么连接就没有被释放,released=false,若连接可重用,则释放连接到连接池中,否则将直接关闭连接(此处与4.3版本有所不同,4.3版本中,若连接没有被释放,直接调用response.close方法,将直接关闭连接,并不会释放连接到连接池中)。

注:httpClient.close()最终调用的是InternalHttpClient#close(),会关闭整个连接管理器,并关闭连接池中所有连接,所以,请谨慎使用httpClient.close()方法。

五、代码示例

//连接池管理,默认最大连接数20,每个路由最大连接数2;

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();

//连接池最大连接数

connManager.setMaxTotal(100);

//默认的每个路由的最大连接数

connManager.setDefaultMaxPerRoute(40);

//设置到单个路由的最大连接数,会覆盖defaultMaxPerRoute

connManager.setMaxPerRoute(new HttpRoute(new HttpHost("hostname", 80)), 10);

//socket配置

SocketConfig socketConfig = SocketConfig.custom()

       .setTcpNoDelay(true)     //是否立即发送数据,设置为true会关闭Socket缓冲,默认为false

       .setSoReuseAddress(true) //是否可以在一个进程关闭Socket后,它还没有释放端口时,其它进程还可以立即重用端口

       .setSoTimeout(500)       //接收数据的等待超时时间,单位ms

       .setSoLinger(60)         //关闭Socket时,要么发送完所有数据,要么等待60s后,就关闭连接,此时socket.close()是阻塞的

       .setSoKeepAlive(true)    //开启监视TCP连接是否有效

       .build();

connManager.setDefaultSocketConfig(socketConfig);

//单个host配置,覆盖默认配置

//connManager.setSocketConfig(new HttpHost("hostname", 80), socketConfig);

//消息约束

MessageConstraints messageConstraints = MessageConstraints.custom()

       .setMaxHeaderCount(200)

       .setMaxLineLength(2000)

       .build();

//Http connection相关配置

ConnectionConfig connectionConfig = ConnectionConfig.custom()

       .setMalformedInputAction(CodingErrorAction.IGNORE)

       .setUnmappableInputAction(CodingErrorAction.IGNORE)

       .setCharset(Consts.UTF_8)

       .setMessageConstraints(messageConstraints)

       .build();

//一般不修改HTTP connection相关配置,故不设置

connManager.setDefaultConnectionConfig(connectionConfig);

//connManager.setConnectionConfig(new HttpHost("hostname", 80), ConnectionConfig.DEFAULT);

//超时相关配置

RequestConfig defaultRequestConfig = RequestConfig.custom()

       .setConnectTimeout(2 * 1000)         //连接超时时间

       .setSocketTimeout(2 * 1000)          //读超时时间(等待数据超时时间)

       .setConnectionRequestTimeout(500)    //从池中获取连接超时时间

       .build();

//自定义重试策略new HttpRequestRetryHandler();重写重试策略方法

//默认配置,默认使用DefaultHttpRequestRetryHandler,默认重试3次;

//其中InterruptedIOException、UnknownHostException、ConnectException、SSLException,发生这4中异常不重试

//禁用重试(参数:retryCount=0、requestSentRetryEnabled=false)

HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(0,false);

//创建HttpClient

CloseableHttpClient httpclient = HttpClients.custom()

       .setConnectionManager(connManager)             //连接管理器

       .setProxy(new HttpHost("hostname", 8080))       //设置代理

       .setDefaultRequestConfig(defaultRequestConfig) //默认请求配置

       .setRetryHandler(retryHandler)               //重试策略

       .build();

//创建一个Get请求,并重新设置请求参数,覆盖默认

HttpGet httpget = new HttpGet("/");

RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig)

       .setSocketTimeout(5000)   //连接超时时间

       .setConnectTimeout(5000)  //读超时时间(等待数据超时时间)

       .setConnectionRequestTimeout(5000)  //从池中获取连接超时时间

       .setProxy(new HttpHost("hostname", 8080))

       .build();

httpget.setConfig(requestConfig);

CloseableHttpResponse response = null;

try {

   //执行请求

   response = httpclient.execute(httpget);

   HttpEntity entity = response.getEntity();

   if (entity != null) {

       InputStream inputStream = entity.getContent();

       try {

           inputStream.read();

           //处理.....

       } catch (IOException ex) {

           throw ex;

       } finally {

           // 关闭流,释放连接回到连接池

           inputStream.close();

       }

   }

} catch (Exception e) {

   e.printStackTrace();

} finally {

   try {

       if (response != null) {

           //response.close(),如果连接之前已经被释放,则什么也不做;

           //若连接还没有被释放,则释放连接到连接池;

           response.close();

       }

       if (httpclient != null) {

           //关闭连接管理器,并会关闭其管理的所有连接,请谨慎使用

           httpclient.close();

       }

   } catch (IOException e) {

       e.printStackTrace();

   }

}

如果你想进入消费金融行业

请关注我们!

消费金融架构

做消费金融行业的精品,提供我们实践中的业务知识、架构规划与实施、技术攻关和最佳实践。

快速关注,请猛扫下面二维码!