HttpClient 详解

做者:小白豆豆5
连接:https://www.jianshu.com/p/14c005e9287c
来源:简书
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
java

1.HTTP 请求建立流程

使用 HttpClient 执行一个 Http 请求的步骤为:缓存

(1)建立一个 HttpClient 对象服务器

(2)建立一个 HttpRequest 对象cookie

(3)使用 HttpClient 来执行 HttpRequest请求,获得对方的 HttpResponse网络

(4)处理 HttpResponse并发

(5)关闭这次请求链接负载均衡

2.建立一个 HttpClient 对象

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

  1. 使用 CloseableHttpClient 的工厂类 HttpClients 的方法来建立实例。HttpClients 提供了根据各类默认配置来建立 CloseableHttpClient 实例的快捷方法。最简单的实例化方式是调用HttpClients.createDefault()。
  2. 使用 CloseableHttpClient 的 builder 类 HttpClientBuilder,先对一些属性进行配置(采用装饰者模式,不断的.setxxxxx().setxxxxxxxx()就好了),再调用 build() 方法来建立实例。上面的HttpClients.createDefault() 实际上调用的也就是HttpClientBuilder.create().build()。

build() 方法最终是根据各类配置来 new 一个 InternalHttpClient 实例(CloseableHttpClient 实现类)。tcp

IternalHttpClient 类的实现以下:(忽略方法部分)ide

class InternalHttpClient extends CloseableHttpClient implements Configurable { private final Log log = LogFactory.getLog(this.getClass()); private final ClientExecChain execChain; private final HttpClientConnectionManager connManager; private final HttpRoutePlanner routePlanner; private final Lookup<CookieSpecProvider> cookieSpecRegistry; private final Lookup<AuthSchemeProvider> authSchemeRegistry; private final CookieStore cookieStore; private final CredentialsProvider credentialsProvider; private final RequestConfig defaultConfig; private final List<Closeable> closeables; public InternalHttpClient(ClientExecChain execChain, HttpClientConnectionManager connManager, HttpRoutePlanner routePlanner, Lookup<CookieSpecProvider> cookieSpecRegistry, Lookup<AuthSchemeProvider> authSchemeRegistry, CookieStore cookieStore, CredentialsProvider credentialsProvider, RequestConfig defaultConfig, List<Closeable> closeables) { Args.notNull(execChain, "HTTP client exec chain"); Args.notNull(connManager, "HTTP connection manager"); Args.notNull(routePlanner, "HTTP route planner"); this.execChain = execChain; this.connManager = connManager; this.routePlanner = routePlanner; this.cookieSpecRegistry = cookieSpecRegistry; this.authSchemeRegistry = authSchemeRegistry; this.cookieStore = cookieStore; this.credentialsProvider = credentialsProvider; this.defaultConfig = defaultConfig; this.closeables = closeables; } ... }

其中须要注意的配置字段包括: HttpClientConnectionManager、HttpRoutePlanner 和 RequestConfig:

    1)HttpClientConnectionManager

            HttpClientConnectionManager 是一个 HTTP 链接管理器。它负责新 HTTP 链接的建立、管理链接的生命周期还有保证一个 HTTP 链接在某一时刻只被一个线程使用。在内部实现的时候,manager 使用一个 ManagedHttpClientConnection 的实例来做为一个实际 connection 的代理,负责管理 connection 的状态以及执行实际的 I/O 操做。若是一个被监管的 connection 被释放或者被明确关闭,尽管此时 manager 仍持有该链接的代理,可是这个 connection 的状态不会被改变也不能再执行任何的 I/O 操做。

HttpClientConnectionManager 有两种具体实现:

  • BasicHttpClientConnectionManager

  BasicHttpClientConnectionManager 每次只管理一个 connection。不过,虽然它是 thread-safe 的,但因为它只管理一个链接,因此只能被一个线程使用。它在管理链接的时候若是发现有相同route 的请求,会复用以前已经建立的链接,若是新来的请求不能复用以前的链接,它会关闭现有的链接并从新打开它来响应新的请求。

  • PoolingHttpClientConnectionManager

  PoolingHttpClientConnectionManager 与 BasicHttpClientConnectionManager 不一样,它管理着一个链接池(链接池管理部分在第7部分有详细介绍)。它能够同时为多个线程服务。每次新来一个请求,若是在链接池中已经存在 route 相同而且可用的 connection,链接池就会直接复用这个 connection;当不存在 route 相同的 connection,就新建一个 connection 为之服务;若是链接池已满,则请求会等待直到被服务或者超时(Timeout waiting for connection from pool)。

  默认不对 HttpClientBuilder 进行配置的话,new 出来的 CloeableHttpClient 实例使用的是 PoolingHttpClientConnectionManager,这种状况下 HttpClientBuilder 建立出的 HttpClient 实例就能够被多个链接和多个线程共用,在应用容器起来的时候实例化一次,在整个应用结束的时候再调用 httpClient.close() 就好了。在 PoolingHttpClientConnectionManager 的配置中有两个最大链接数量,分别控制着总的最大链接数量(MaxTotal)和每一个 route 的最大链接数量(DefaultMaxPerRoute)。若是没有显式设置,默认每一个 route 只容许最多2个connection,总的 connection 数量不超过 20。这个值对于不少并发度高的应用来讲是不够的,必须根据实际的状况设置合适的值,思路和线程池的大小设置方式是相似的,若是全部的链接请求都是到同一个url,那能够把 MaxPerRoute 的值设置成和 MaxTotal 一致,这样就能更高效地复用链接。HttpClient 4.3.5的设置方法以下:

private final static PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(MAX_CONNECTION);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(MAX_CONNECTION);
CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(poolingHttpClientConnectionManager).build();

    2)HttpRoutePlanner

        HttpClient 不只支持简单的直连、复杂的路由策略以及代理。HttpRoutePlanner 是基于 http 上下文状况下,客户端到服务器的路由计算策略,通常没有代理的话,就不用设置这个东西。这里有一个很关键的概念—route:在 HttpClient 中,一个 route 指运行环境机器->目标机器 host 的一条线路,也就是若是目标 url 的 host 是同一个,那么它们的 route 也是同样的。

    3)RequestConfig

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

    (1)connectionRequestTimeout——从链接池中取链接的超时时间

      这个时间定义的是从 ConnectionManager 管理的链接池中取出链接的超时时间, 若是链接池中没有可用的链接,则 request 会被阻塞,最长等待 connectionRequestTimeout 的时间,若是尚未被服务,则抛出 ConnectionPoolTimeoutException 异常,不继续等待。

    (2)connectTimeout——链接超时时间

      这个时间定义了经过网络与服务器创建链接的超时时间,也就是取得了链接池中的某个链接以后到接通目标 url 的链接等待时间。发生超时,会抛出ConnectionTimeoutException异常。

    (3)socketTimeout——请求超时时间

      这个时间定义了 socket 读数据的超时时间,也就是链接到服务器以后到从服务器获取响应数据须要等待的时间,或者说是链接上一个 url 以后到获取 response 的返回等待时间。发生超时会抛出SocketTimeoutException异常。

注意,4.3.5版本超时设置方法和以前的版本不一样,下面是一个设置各个超时时间的例子。注意,这样设置的是该 HttpClient 处理的全部 request 的默认配置,若是在构造 request 实例的时候不特别设置,则会使用默认配置。

RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(CON_RST_TIME_OUT).setConnectTimeout(CON_TIME_OUT).setSocketTimeout(SOCKET_TIME_OUT).build();
HttpEntityEnclosingRequestBase httpRequest = new HttpEntityEnclosingRequestBase() {
@Override
public String getMethod() {
return method;
}
};
httpRequest.setConfig(requestConfig);

3.建立一个 Request 对象

HttpClient 支持全部的 HTTP1.1 中的全部定义的请求类型:GET、HEAD、POST、PUT、DELETE、TRACE 和 OPTIONS。对使用的类为 HttpGet、HttpHead、HttpPost、HttpPut、HttpDelete、HttpTrace 和 HttpOptions。Request的对象创建很简单,通常用目标url来构造就行了。下面是一个HttpPost的建立代码:

HttpPost httpPost = new HttpPost(someGwUrl);

一个 Request 还能够 addHeader、setEntity、setConfig 等,通常这三个用的比较多。

固然,你也能够经过建立一个 HttpEntityEnclosingRequestBase 对象做为 Request 对象,配置代码以下:

HttpEntityEnclosingRequestBase httpRequest = new HttpEntityEnclosingRequestBase() { @Override public String getMethod() { return method;   // 对应的GET,POST,DELETE等 } };
httpRequest.setURI();
httpRequest.setEntity();

4.执行 Request 请求

执行 Request 请求就是调用 HttpClient 的execute方法。最简单的使用方法是调用 execute(final HttpUriRequest request)。

HttpClient 容许 http 链接在特定的 Http 上下文中执行,HttpContext 是跟一个链接相关联的,因此它也只能属于一个线程,若是没有特别设定,在 execute 的过程当中,HttpClient 会自动为每个connection new 一个 HttpClientHttpContext。

HttpClientContext localcontext = HttpClientContext.adapt(context != null ? context : newBasicHttpContext());

整个 execute 执行的常规流程为:

  1. new一个 http context
  2. 取出 Request 和URL
  3. 根据 HttpRoute 的配置看是否须要重写URL
  4. 根据 URL 的host、port和scheme设置target
  5. 在发送前用 http 协议拦截器处理 request 的各个部分
  6. 取得验证状态、user token来验证身份
  7. 从链接池中取一个可用的链接
  8. 根据request的各类配置参数以及取得的connection构造一个connManaged
  9. 打开managed的connection(包括建立route、dns解析、绑定socket、socket链接等)
  10. 请求数据(包括发送请求和接收response两个阶段)
  11. 查看keepAlive策略,判断链接是否要复用,并设置相应标识
  12. 返回response
  13. 用http协议拦截器处理response的各个部分

5.处理 response

HttpReaponse 是将服务端发回的 Http 响应解析后的对象。CloseableHttpClient 的 execute 方法返回的 response 都是 CloseableHttpResponse 类型。能够 getFirstHeader(String)、getLastHeader(String)、headerIterator(String)取得某个Header name对应的迭代器、getAllHeaders()、getEntity、getStatus等,通常这几个方法比较经常使用。在这个部分中,对于 entity 的处理须要特别注意一下。通常来讲一个 response 中的 entity 只能被使用一次,它是一个流,这个流被处理完就再也不存在了。先 response.getEntity() 再使用 HttpEntity.getContent()来获得一个java.io.InputStream,而后再对内容进行相应的处理。

有一点很是重要,想要复用一个 connection 就必需要让它占有的系统资源获得正确释放。释放资源有两种方法:

  1)关闭和 entity 相关的 content stream

  若是是使用 outputStream 就要保证整个 entity 都被 write out,若是是 inputStream,则在最后要记得调用 inputStream.close()。或者使用 EntityUtils.consume(entity) 或EntityUtils.consumeQuietly(entity) 来让 entity 被彻底耗尽(后者不抛异常)来作这一工做。EntityUtils 中有个 toString 方法也很方便的(调用这个方法最后也会自动把 inputStream close掉的),不过只有在能够肯定收到的 entity 不是特别大的状况下才能使用。

作过实验,若是没有让整个 entity 被 fully consumed,则该链接是不能被复用的,很快就会由于在链接池中取不到可用的链接超时或者阻塞在这里(由于该链接的状态将会一直是 leased 的,即正在被使用的状态)。因此若是想要复用 connection,必定必定要记得把 entity fully consume 掉,只要检测到 stream 的 eof,才会自动调用 ConnectionHolder 的 releaseConnection 方法进行处理(注意,ConnectionHolder 并非一个public class,虽然里面有一些跟释放链接相关的重要操做,可是却没法直接调用)。

关闭response

  2)关闭response

  执行 response.close() 虽然会正确释放掉该 connection 占用的全部资源,可是这是一种比较暴力的方式,采用这种方式以后,这个 connection 就不能被重复使用了。从源代码中能够看出,response.close() 调用了 connectionHolder 的 abortConnection 方法,它会 close 底层的 socket,而且 release 当前的 connection,并把 reuse 的时间设为0。这种状况下的 connection 称为expired connection,也就是 client 端单方面把链接关闭。还要等待 closeExpiredConnections 方法将它从链接池中清除掉(从链接池中清除掉的含义是把它所对应的链接池的 entry 置为无效,而且关掉对应的 connection,shutdown 对应 socket 的输入和输出流,这个方法的调用时间是须要设置的)。

  关闭stream和response的区别在于前者会尝试保持底层的链接alive,然后者会直接shut down而且丢弃connection。

  socket是和ip以及port绑定的,可是host相同的请求会尽可能复用链接池里已经存在的 connection(由于在链接池里会另外维护一个 route 的子链接池,这个子链接池中每一个 connection 的状态有三种:leased、available 和 pending,只有 available 状态的 connection 才能被使用,而 fully consume entity 就可让该链接变为available状态),若是 host 地址同样,则优先使用connection。若是但愿重复读取 entity 中的内容,就须要把 entity 缓存下来。最简单的方式是用 entity 来 new 一个 BufferedHttpEntity,这一操做会把内容拷贝到内存中,以后使用这个BufferedHttpEntity就能够了。

6.关闭 httpClient

调用 httpClient.close() 会先 shut down connection manager,而后再释放该 HttpClient 所占用的全部资源,关闭全部在使用或者空闲的 connection 包括底层 socket。因为这里把它所使用的connection manager 关闭了,因此在下次还要进行 http 请求的时候,要从新 new 一个 connection manager 来 build 一个 HttpClient(也就是在须要关闭和新建 Client 的状况下,connection manager不能是单例的)。

7.其余一些东西

  (1)关于keep-alive

  在 HttpClient.execute 获得 response 以后的相关代码中,它会先取出 response 的 keep-alive 头来设置 connection 是否 resuable 以及存活的时间。若是服务器返回的响应中包含了Connection:Keep-Alive(默认有的),但没有包含 Keep-Alive 时长的头消息,HttpClient 认为这个链接能够永远保持。不过,不少服务器都会在不通知客户端的状况下,关闭必定时间内不活动的链接,来节省服务器资源。在这种状况下默认的策略显得太乐观,咱们可能须要自定义链接存活策略,也就是在建立 HttpClient 的实例的时候用下面的代码。(xxx为本身写的保活策略)

ClosableHttpClientclient =HttpClients.custom().setKeepAliveStrategy(xxx).build();

  (2)链接池管理

  前面也有说到关于从链接池中取可用链接的部分逻辑。完整的逻辑是:在每收到一个 route 请求后,链接池都会创建一个以这个 route 为 key 的子链接池,当有一个新的链接请求到来的时候,它会优先匹配已经存在的子链接池们,若是以前已经有过以这个 route 为 key 的子链接池,那么就会去试图取这个子链接池中状态为 available 的链接,若是此时有可用的链接,则将取得的 available 链接状态改成 leased 的,取链接成功。若是此时子链接池没有可用链接,那再看是否达到了所设置的最大链接数和每一个 route 所容许的最大链接数的上限,若是还有余量则 new 一个新的链接,或者取得 lastUsedConnection,关闭这个链接、把链接从原来所在的子链接池删除,再 lease 取链接成功。若是此时的状况不容许再new一个新的链接,就把这个请求链接的请求放入一个 queue 中排队等待,直到获得一个链接或者超时才会从 queue 中删去。一个链接被 release 以后,会从等待链接的 queue 中唤醒等待链接的服务进行处理。

  (3)链接回收策略

  当链接被管理器收回后,这个链接仍然存活,可是却没法监控 socket 的状态,也没法对 I/O 事件作出反馈。若是链接被服务器端关闭了,客户端监测不到链接的状态变化(也就没法根据链接状态的变化,关闭本地的socket)。HttpClient 为了缓解这一问题形成的影响,会在使用某个链接前,监测这个链接是否已通过时,若是服务器端关闭了链接,那么链接就会失效。前面提到的RequestConfig 中的 staleConnectionCheckEnabled 就是用来控制是否进行上述操做,相关代码:

if(config.isStaleConnectionCheckEnabled()) {
// validate connection
if(managedConn.isOpen()) {
  this.log.debug("Stale connection check");
    if(managedConn.isStale()) {
      this.log.debug("Stale connection detected");
      managedConn.close();
    }
  }
}

其中的 managedConn.isStale() 就是检查取出的链接是否失效,须要注意的是这种过期检查并非100%有效,而且会给每一个请求增长10到30毫秒额外开销。isStale()有一点比较奇怪的是,若是抛出SocketTimeoutException 的时候会返回 false,即意味着此 managedConn 并非失效的(若是此 managedConn 是长链接的,那么没失效是可理解的,但为何会抛 SocketTimeoutException 异常就不懂了)。而这里 SocketTimeoutException 的发生与咱们前面设置的 RequestConfig.socketTimeout 是没有关系的,它实现的机制是先设置 1ms 的超时时间,看在这 1ms 内是否能从inputBuffer 里面读到数据,若是读到的数据长度为 -1(即没有数据),说明此链接失效。可是很常常随机会发生 SocketTimeoutException,这时会返回 false,而且此时 managedConn 是 open 的状态,这样就会跳事后面的 dns 解析及 socket 从新创建和绑定的过程,直接再次重用以前的 connection 以及它绑定的 socket。

在这里遇到的一个很纠结的问题:

Http1.1 默认进行的长链接并不适用于咱们的应用场景,咱们的 httpClient 是用在服务端代替客户端 sdk 去请求另外一个应用的服务端,而且调用量很是大,在这种状况下,若是使用默认的长链接就会一直只去请求对方的某一台服务器,无论怎么说,虽然调用的确实是相同 host 的主机对功能来讲是没有问题的,但万一对方服务器被这样弄挂了呢?而且这种状况下要是使用了dns负载均衡技术,那么dns的负载均衡将不能被执行到!这显然不是咱们所但愿的。而且经过测试发现,只要是长链接的 connection,在代码中调用各类 close 或者 release 方法都不能把 connection 真正关掉,除非把整个 httpClient.close。

对于这个问题查了一些资料,里面提到的一个可行的解决办法,是创建一个监控线程,来专门回收因为长时间不活动而被断定为失效的链接。这个监控线程能够周期性的调用ClientConnectionManager 类的 closeExpiredConnections() 方法来关闭过时的链接,回收链接池中被关闭的链接。它也能够选择性的调用 ClientConnectionManager 类的 closeIdleConnections() 方法来关闭一段时间内不活动的链接。因为这个解决方案对于咱们的应用来讲太复杂了,因此这个方案的有效性没有验证过。

我原先采用的解决方式是:在每次链接请求到来的时候都 build 一个新的 HttpClient 对象,而且使用 BasicHttpClientConnectionManager 做为 connectionManager。而后在处理完 http response 以后 close掉这个 HttpClient。目前本地自测来看,这种作法不会出现上面的奇怪问题。可是很忧伤的是,新建一个 HttpClient 的逻辑很重,而且链接不能复用,会浪费不少时间。

因为这个平常需求自己作的就是优化性质的工做,加上每一个请求都新建 HttpClient 这一大坨代码,内心老是有点难受。继续找解决办法。

在尝试了改系统的各类 tcp 配置参数还有其余的 socket、系统配置无果后,最终找到的解决方式却异常简单。简单来讲,其实咱们的应用场景下须要的是短链接,这样只要在 request 中添加Connection:close 的头部,就能够保证这个连接在此次请求完成以后就被关掉,只用一次。同时发现,若是头中既有 Connection:Keep-Alive 又有 Connection:close 的话,Connection:close 并不会有更高的优先级,依旧会保持长连。

 

7.总结

使用 HttpClient 的时候特别须要注意的有下面几个地方:

(1)链接池最大链接数,不配置,默认为20

(2)同个 route 的最大链接数,不配置,默认为2

(3)去链接池中取链接的超时时间,不配置则无限期等待

(4)与目标服务器创建链接的超时时间,不配置则无限期等待

(5)去目标服务器取数据的超时时间,不配置则无限期等待

(6)要 fully consumed entity,才能正确释放底层资源

(7)同个 host 但 ip 有多个的状况,请谨慎使用单例的 HttpClient 和链接池

(8)HTTP1.1 默认支持的是长链接,若是想使用短链接,要在 request 上加 Connection:close 的 header,否则长链接是不可能自动被关掉的!

必定要结合实际状况来看是否须要设置,否则可能致使严重的问题。

HttpClient 的内容远不止我上面说到的这些,还包括 Cookie 管理,Fluent API 等内容,因为没有实际使用,理解的并不透彻,后续继续学习后再来补充。

相关文章
相关标签/搜索