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對象

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM