HttpClient使用连接池

「这是我参与11月更文挑战的第18天,活动详情查看: 2021最后一次更文挑战

1. http请求存在的问题

Http1.0连接是无状态的,短链接,每次请求时建立连接、请求结束后断开连接。也可以在请求头中设置 Connection:keep-alive 来实现Http的长连接。

Http连接的建立和关闭本质上就是TCP连接的建立和关闭,在建立和关闭时会有三次握手和四次挥手的过程,占用资源多、开销大。

为了降低频繁建立和断开Http连接对资源的消耗,就需要使用Http连接池来管理Http的连接,并保证一定数量的长连接,且系统能拥有更高的并发性能。

2. 创建http连接池

HttpClient中创建http连接池的方式是使用定义的 PoolingHttpClientConnectionManager 类,该类实现了 HttpClientConnectionManager 接口和 ConnPoolControl 接口。

  • 初始化连接池对象时,可以在构造函数中传入时间值参数,指定连接存活时间
  • 使用对象的setMaxTotal()方法来设置连接池的最大连接数
  • 使用对象的setDefaultMaxPerRoute()方法设置路由最大连接数
  • /** http连接池对象 */
    private static PoolingHttpClientConnectionManager cm;
     * 初始化连接池
    public static void init(){
        //创建http连接池,可以同时指定连接超时时间
        cm = new PoolingHttpClientConnectionManager(60000, TimeUnit.MILLISECONDS);
        //最多同时连接20个请求
        cm.setMaxTotal(20);
        //每个路由最大连接数,路由指IP+PORT或者域名
        cm.setDefaultMaxPerRoute(50);
    

    3. 通过连接池获取httpClient

    HttpClient中http请求的连接由HttpClient对象发起,加入连接池后就可以从连接池中获取HttpClient对象信息。

    * 从连接池中获取httpClient连接 public static CloseableHttpClient getHttpClient(PoolingHttpClientConnectionManager cm){    //设置请求参数配置,创建连接时间、从连接池获取连接时间、数据传输时间、是否测试连接可用、构建配置对象    RequestConfig requestConfig = RequestConfig.custom()       .setConnectTimeout(1000)       .setConnectionRequestTimeout(3000)       .setSocketTimeout(10 * 1000)       .setStaleConnectionCheckEnabled(true)       .build();    //创建httpClient时从连接池中获取,并设置连接失败时自动重试(也可以自定义重试策略:setRetryHandler())    CloseableHttpClient httpClient = HttpClients.custom()       .setDefaultRequestConfig(requestConfig)       .setConnectionManager(cm)       .disableAutomaticRetries()       .build();    return httpClient;

    在使用http连接池获取httpClient连接时,需要配置相关参数信息:

  • 定义RequestConfig对象来设置请求时的时间参数,如连接创建时间、从连接池中获取连接时间、数据传输请求时间、是否请求前测试可用性等
  • RequestConfig对象可以为整个httpClient设置,也可以针对GET请求和POST请求分别设置
  • 接收一个CloseableHttpClient对象作为httpClient,使用建造者模式获取时需要设置连接对象的配置信息、失败重连信息以及来源的连接池信息。
  • 4. 设定请求类型并执行请求

    创建httpClient连接后,之后的请求执行流程和单独创建httpClient对象的请求流程基本一致。

    需要注意的就是在使用连接池获取的httpClient请求完成后,如果希望将连接释放到连接池中,就不能使用close()方法关闭,而是调用EntityUtils.consume()方法。

    * 执行请求 public static void doGetRequest(CloseableHttpClient httpClient,String url){    //创建http请求类型    HttpGet httpGet = new HttpGet(url);    CloseableHttpResponse httpResponse = null;    try {        httpResponse = httpClient.execute(httpGet);        if(200 == httpResponse.getStatusLine().getStatusCode()){            System.out.println("请求返回数据内容:" + EntityUtils.toString(httpResponse.getEntity()));   } catch (IOException e) {        e.printStackTrace();   } finally {        if(httpResponse != null){            //执行httpResponse.close关闭对象会关闭连接池,            //如果需要将连接释放到连接池,可以使用EntityUtils.consume()方法            try {                EntityUtils.consume(httpResponse.getEntity());           } catch (IOException e) {                e.printStackTrace();

    5. 连接池状态观察

    使用连接池获取httpClient连接发起请求时,可以通过连接池中连接的存活等信息来观察使用情况。

    当使用单线程执行多次http请求时,连接池中始终保持一个连接,因为单线程总是执行完上一个才会开始下一个请求的执行。

    String url = "http://www.baidu.com";
    for(int i = 0; i < 5; i++){
        //连接池中获取httpClient
        CloseableHttpClient httpClient = getHttpClient(cm);
        //单线程执行请求
        doGetRequest(httpClient, url);
        System.out.println(cm.getTotalStats());
    
  • 因为是单线程,线程池的状态会打印5次,每次结果都是//[leased: 0; pending: 0; available: 1; max: 20]
  • 而使用多线程执行http请求时,每个请求开启一个线程后,最终执行完成并将连接释放至连接池后,连接池中存活了请求中并存的最大连接数。

    String url = "http://www.baidu.com";
    for(int i = 0; i < 5; i++){
        //获取httpClient
        CloseableHttpClient httpClient = getHttpClient(cm);
        //每个请求开启一个单独的线程
        doGetRequestMulitThread(httpClient,url);
        System.out.println(cm.getTotalStats());