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 接口,接口有兩種實現,分別是 BasicHttpClientConnectionManager 和 PoolingHttpClientConnectionManager。
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 連接驅逐
連接驅逐是指,探測空閑和過期的連接,並關閉它們。連接驅逐有兩種實現方式:
- 在HttpClient執行請求前檢測連接是否過期
- 使用一個監控線程來探測並關閉過期連接
//通過定義一個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 關閉連接
正確關閉連接的步驟如下:
- 消費並關閉響應
- 關閉client對象
- 關閉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對象