HttpClient连接管理

HttpClient连接管理

主机间建立网络连接是个非常复杂跟耗时的过程(例如TCP三次握手bla bla),在HTTP请求中,如果可以复用一个连接来执行多次请求,可以很大地提高吞吐量。
HttpClient中,连接就是一种可以复用的资源。它提供了一系列连接管理的API,帮助我们处理连接管理的各种问题。本文基于4.5.10版本,介绍这些API的使用。

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.10</version>
</dependency>

HttpClient中的连接是有状态且线程不安全的,它使用专门的连接管理器来管理这些连接,作为连接的工厂,负责连接的生命周期管理,以及对连接的并发访问进行同步。连接管理器抽象为 HttpClientConnectionManager 接口,接口有两种实现,分别是 BasicHttpClientConnectionManagerPoolingHttpClientConnectionManager

1 BasicHttpClientConnectionManager

BasicHttpClientConnectionManager,http连接管理器最简单的一种实现,用于创建和管理单个连接,只用于单线程,显然也是线程安全的。

BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager();
HttpRoute route = new HttpRoute(new HttpHost("www.baidu.com", 80));
ConnectionRequest connectionRequest = connectionManager.requestConnection(route, null);

上面的方法是基于 BasicHttpClientConnectionManager 的底层API的使用,requestConnection 方法从 connectionManager 管理的连接池取出一个连接,连接到 route 对象定义的“www.baidu.com”。

HttpRoute
HttpClient可以直接与目标主机建立连接,也可以通过由一系列跳转最终达到目标主机,这个过程称为路由。 RouteInfo 接口定义了连接到目标主机的路由信息,而 HttpRoute
就是该接口的一个具体实现,这个类是不可变的。

2 PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager 可以创建并管理一个连接池,为多个路由或目标主机提供连接。简单的用法如下:

//为一个HttpClient对象配置连接池
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build();
try {
    client.execute(new HttpGet("https://www.baidu.com"));
} catch (IOException e) {
    e.printStackTrace();
}
System.out.println(connectionManager.getTotalStats().getLeased());

单个连接池可以供多个线程的多个HttpClient对象使用

//可以使用一个连接池,管理面向不同目标主机的请求
HttpGet get1 = new HttpGet("https://www.zhihu.com");
HttpGet get2 = new HttpGet("https://www.baidu.com");
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();

CloseableHttpClient client1 = HttpClients.custom().setConnectionManager(connectionManager).build();
CloseableHttpClient client2 = HttpClients.custom().setConnectionManager(connectionManager).build();

MultiHttpClientConnThread t1 = new MultiHttpClientConnThread(client1, get1);
MultiHttpClientConnThread t2 = new MultiHttpClientConnThread(client2, get2);
t1.start();
t2.start();
try {
    t1.join();
    t2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

其中 MultiHttpClientConnThread 是自定义的类,定义如下

@Slf4j
public class MultiHttpClientConnThread extends Thread {
    private final CloseableHttpClient client;
    private final HttpGet get;
    private PoolingHttpClientConnectionManager connectionManager;

    public MultiHttpClientConnThread(final CloseableHttpClient client, final HttpGet get) {
        this.client = client;
        this.get = get;
    }

    public MultiHttpClientConnThread(final CloseableHttpClient client, final HttpGet get, final PoolingHttpClientConnectionManager connectionManager) {
        this.client = client;
        this.get = get;
        this.connectionManager = connectionManager;
    }
    @Override
    public void run() {
        try {
            log.info("Thread Running:" + getName());
            if (connectionManager != null) {
                log.info("Before - Leased Connections = " + connectionManager.getTotalStats().getLeased());
                log.info("Before - Available Connections = " + connectionManager.getTotalStats().getAvailable());
            }
            HttpResponse response = client.execute(get);
            if (connectionManager != null) {
                log.info("After - Leased Connections = " + connectionManager.getTotalStats().getLeased());
                log.info("After - Available Connections = " + connectionManager.getTotalStats().getAvailable());
            }
            //消费response,为了把连接释放回连接池
            EntityUtils.consume(response.getEntity());
        } catch (IOException e) {
            log.error("", e);
        }
    }
}

注意EntityUtils.consume(response.getEntity()),需要消费掉response的全部内容,连接管理器才会把这个连接释放回归连接池。

3 配置连接管理器

PoolingHttpClientConnectionManager 可配置的选项如下:

  • 连接总数,默认值为20
  • 单个普通路由的最大连接数,默认值为2
  • 特定某个路由的最大连接数,默认值为2
//调整默认的连接池参数
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
//总连接数为5
connectionManager.setMaxTotal(5);
//单个路由最大连接数为4
connectionManager.setDefaultMaxPerRoute(4);
//特定路由www.baidu.com的最大连接数为5
HttpHost httpHost = new HttpHost("www.baidu.com", 80);
HttpRoute route = new HttpRoute(httpHost);
connectionManager.setMaxPerRoute(route, 5);

如果使用默认设置,在多线程请求的情况下,单个路由很容易就达到最大连接数了

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build();
MultiHttpClientConnThread t1 = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
MultiHttpClientConnThread t2 = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
MultiHttpClientConnThread t3 = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();

运行以上代码可以看到以下结果:

INFO chenps3.httpclient.MultiHttpClientConnThread(36) - Thread Running:Thread-0
INFO chenps3.httpclient.MultiHttpClientConnThread(36) - Thread Running:Thread-1
INFO chenps3.httpclient.MultiHttpClientConnThread(36) - Thread Running:Thread-2
INFO chenps3.httpclient.MultiHttpClientConnThread(38) - Before - Leased Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(38) - Before - Leased Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(39) - Before - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(38) - Before - Leased Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(39) - Before - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(39) - Before - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(43) - After - Leased Connections = 2
INFO chenps3.httpclient.MultiHttpClientConnThread(43) - After - Leased Connections = 2
INFO chenps3.httpclient.MultiHttpClientConnThread(44) - After - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(44) - After - Available Connections = 0
INFO chenps3.httpclient.MultiHttpClientConnThread(43) - After - Leased Connections = 1
INFO chenps3.httpclient.MultiHttpClientConnThread(44) - After - Available Connections = 1

可以看到,即使有3个线程的请求并发执行,最多只有2个连接被使用。没有拿到连接的线程则会暂时阻塞,直到有连接归还到连接池。

4 keep-alive策略

如果没有在响应头部找到keep-alive,HttpClient假定是无限大,因此通常需要自定义一个keep-alive策略。

//优先使用响应头的keep-alive值,如果没找到,设置为5秒
ConnectionKeepAliveStrategy strategy = new ConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) {
        HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.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")) {
                return Long.parseLong(value) * 1000;
            }
        }
        return 5000;
    }
};
//自定义策略应用到client
CloseableHttpClient client = HttpClients.custom()
        .setKeepAliveStrategy(strategy)
        .build();

5 连接持久化与复用

HTTP/1.1 规范中声明,如果连接没有被关闭,就可以被复用。HttpClient中,连接一旦被连接管理器释放,就会保持可复用的状态。
BasicHttpClientConnectionManager只能使用一个连接,因此使用前必须要先显式释放:

BasicHttpClientConnectionManager basic = new BasicHttpClientConnectionManager();
HttpClientContext ctx = HttpClientContext.create();
HttpGet get = new HttpGet("https://www.baidu.com");
//使用底层api实现一次请求
HttpRoute route = new HttpRoute(new HttpHost("www.baidu.com", 80));
ConnectionRequest request = basic.requestConnection(route, null);
HttpClientConnection connection = request.get(10, TimeUnit.SECONDS);
basic.connect(connection, route, 1000, ctx);
basic.routeComplete(connection, route, ctx);

HttpRequestExecutor executor = new HttpRequestExecutor();
ctx.setTargetHost(new HttpHost("www.baidu.com", 80));

executor.execute(get, connection, ctx);
//显式释放连接,允许被复用
basic.releaseConnection(connection, null, 1, TimeUnit.SECONDS);
//使用高层api实现一次请求
CloseableHttpClient client = HttpClients.custom()
        .setConnectionManager(basic)
        .build();
client.execute(get);

如果没有显式释放连接,执行最后一行代码会有以下异常:

Exception in thread "main" java.lang.IllegalStateException: Connection is still allocated

PoolingHttpClientConnectionManager 可以隐式释放连接。以下代码使用10个线程执行10个请求,共享5个连接:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(5);
connectionManager.setMaxTotal(5);
CloseableHttpClient client = HttpClients.custom()
        .setConnectionManager(connectionManager)
        .build();
MultiHttpClientConnThread[] threads = new MultiHttpClientConnThread[10];
for (int i = 0; i < threads.length; i++) {
    threads[i] = new MultiHttpClientConnThread(client, new HttpGet("http://www.baidu.com"), connectionManager);

}
for (MultiHttpClientConnThread i : threads) {
    i.start();
}
for (MultiHttpClientConnThread i : threads) {
    i.join();
}

6 配置超时时间

虽然HttpClient支持设置多种超时时间,但通过连接管理器,只能设置socket的超时时间。

//设置socket超时时间为5秒
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setSocketConfig(
        new HttpHost("www.baidu.com", 80),
        SocketConfig.custom().setSoTimeout(5000).build());

7 连接驱逐

连接驱逐是指,探测空闲和过期的连接,并关闭它们。连接驱逐有两种实现方式:

  1. 在HttpClient执行请求前检测连接是否过期
  2. 使用一个监控线程来探测并关闭过期连接
//通过定义一个RequestConfig对象,令client在请求前检查连接是否过期,有性能损耗
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
RequestConfig requestConfig = RequestConfig.custom().setStaleConnectionCheckEnabled(true).build();
CloseableHttpClient client = HttpClients.custom()
        .setDefaultRequestConfig(requestConfig)
        .setConnectionManager(connectionManager)
        .build();
//定义一个监视器线程类,探测并关闭过期连接和超过30秒的空闲连接
public class IdleConnectionMonitorThread extends Thread {
    private final HttpClientConnectionManager connectionManager;
    private volatile boolean shutdown;
    public IdleConnectionMonitorThread(HttpClientConnectionManager connectionManager) {
        this.connectionManager = connectionManager;
    }
    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(1000);
                    //关闭过期连接
                    connectionManager.closeExpiredConnections();
                    //关闭空闲超过30秒的连接
                    connectionManager.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException e) {
            showdown();
        }
    }
    private void showdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
}

8 关闭连接

正确关闭连接的步骤如下:

  1. 消费并关闭响应
  2. 关闭client对象
  3. 关闭connection manager对象

如果连接关闭之前就关闭掉了连接管理器,管理器所管理的所有连接和资源都会直接释放。

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
CloseableHttpClient client = HttpClients.custom()
        .setConnectionManager(connectionManager)
        .build();
HttpGet get = new HttpGet("https://www.baidu.com");
CloseableHttpResponse response = client.execute(get);

EntityUtils.consume(response.getEntity());      //消费响应
response.close();           //关闭响应
client.close();             //关闭client对象
connectionManager.close();  //关闭connection manager对象

参考资料

posted @ 2020-01-06 10:52  filozofio  阅读(1220)  评论(0编辑  收藏  举报