HttpClient連接池的一些思考


前言

使用apache的httpclient進(jìn)行http的交互處理已經(jīng)很長時(shí)間了,而httpclient實(shí)例則使用了http連接池,想必大家也沒有關(guān)心過連接池的管理。事實(shí)上,通過分析httpclient源碼,發(fā)現(xiàn)它很優(yōu)雅地隱藏了所有的連接池管理細(xì)節(jié),開發(fā)者完全不用花太多時(shí)間去思考連接池的問題。

Apache官網(wǎng)例子

CloseableHttpClient httpclient = HttpClients.createDefault();

HttpGet httpget =newHttpGet("http://localhost/");

CloseableHttpResponse response = httpclient.execute(httpget);try {

? ? HttpEntity entity = response.getEntity();

? ? if(entity !=null) {

? ? ? ? longlen = entity.getContentLength();

? ? ? ? if(len != -1 && len < 2048) {

? ? ? ? ? ? System.out.println(EntityUtils.toString(entity));

? ? ? ? } else {

? ? ? ? ? ? // Stream content out? ? ? ? }

? ? }

} finally {

? ? response.close();

}

HttpClient及其連接池配置

整個(gè)線程池中最大連接數(shù) MAX_CONNECTION_TOTAL = 800

路由到某臺(tái)主機(jī)最大并發(fā)數(shù),是MAX_CONNECTION_TOTAL(整個(gè)線程池中最大連接數(shù))的一個(gè)細(xì)分 ROUTE_MAX_COUNT = 500

重試次數(shù),防止失敗情況 RETRY_COUNT = 3

客戶端和服務(wù)器建立連接的超時(shí)時(shí)間 CONNECTION_TIME_OUT = 5000

客戶端從服務(wù)器讀取數(shù)據(jù)的超時(shí)時(shí)間 READ_TIME_OUT = 7000

從連接池中獲取連接的超時(shí)時(shí)間 CONNECTION_REQUEST_TIME_OUT = 5000

連接空閑超時(shí),清楚閑置的連接 CONNECTION_IDLE_TIME_OUT = 5000

連接保持存活時(shí)間 DEFAULT_KEEP_ALIVE_TIME_MILLIS = 20 * 1000

MaxtTotal和DefaultMaxPerRoute的區(qū)別

MaxtTotal是整個(gè)池子的大?。?/p>

DefaultMaxPerRoute是根據(jù)連接到的主機(jī)對MaxTotal的一個(gè)細(xì)分;

比如:MaxtTotal=400,DefaultMaxPerRoute=200,而我只連接到http://hjzgg.com時(shí),到這個(gè)主機(jī)的并發(fā)最多只有200;而不是400;而我連接到http://qyxjj.com 和 http://httls.com時(shí),到每個(gè)主機(jī)的并發(fā)最多只有200;即加起來是400(但不能超過400)。所以起作用的設(shè)置是DefaultMaxPerRoute。

HttpClient連接池模型

HttpClient從連接池中獲取連接分析

org.apache.http.pool.AbstractConnPool

private E getPoolEntryBlocking(

? ? ? ? finalT route,final Object state,

? ? ? ? finallongtimeout,final TimeUnit tunit,

? ? ? ? finalPoolEntryFuture future)

? ? ? ? ? ? throws IOException, InterruptedException, TimeoutException {

? ? Date deadline =null;

? ? if(timeout > 0) {

? ? ? ? deadline =new Date

? ? ? ? ? ? (System.currentTimeMillis() + tunit.toMillis(timeout));

? ? }

? ? this.lock.lock();

? ? try {

? ? ? ? finalRouteSpecificPool pool = getPool(route);//這是每一個(gè)路由細(xì)分出來的連接池E entry =null;

? ? ? ? while(entry ==null) {

? ? ? ? ? ? Asserts.check(!this.isShutDown, "Connection pool shut down");

? ? ? ? ? ? //獲從池子中獲取一個(gè)可用連接并返回for (;;) {

? ? ? ? ? ? ? ? entry = pool.getFree(state);

? ? ? ? ? ? ? ? if(entry ==null) {

? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? if (entry.isExpired(System.currentTimeMillis())) {

? ? ? ? ? ? ? ? ? ? entry.close();

? ? ? ? ? ? ? ? } elseif(this.validateAfterInactivity > 0) {

? ? ? ? ? ? ? ? ? ? if(entry.getUpdated() +this.validateAfterInactivity <= System.currentTimeMillis()) {

? ? ? ? ? ? ? ? ? ? ? ? if(!validate(entry)) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? entry.close();

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? if (entry.isClosed()) {

? ? ? ? ? ? ? ? ? ? this.available.remove(entry);

? ? ? ? ? ? ? ? ? ? pool.free(entry, false);

? ? ? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? if(entry !=null) {

? ? ? ? ? ? ? ? this.available.remove(entry);

? ? ? ? ? ? ? ? this.leased.add(entry);

? ? ? ? ? ? ? ? onReuse(entry);

? ? ? ? ? ? ? ? return entry;

? ? ? ? ? ? }

? ? ? ? ? ? //創(chuàng)建新的連接

? ? ? ? ? ? // New connection is neededfinalintmaxPerRoute = getMax(route);//獲取當(dāng)前路由最大并發(fā)數(shù)

? ? ? ? ? ? // Shrink the pool prior to allocating a new connectionfinalintexcess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);

? ? ? ? ? ? if(excess > 0) {//如果當(dāng)前路由對應(yīng)的連接池的連接超過最大路由并發(fā)數(shù),獲取到最后使用的一次連接,釋放掉for(inti = 0; i < excess; i++) {

? ? ? ? ? ? ? ? ? ? finalE lastUsed = pool.getLastUsed();

? ? ? ? ? ? ? ? ? ? if(lastUsed ==null) {

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? lastUsed.close();

? ? ? ? ? ? ? ? ? ? this.available.remove(lastUsed);

? ? ? ? ? ? ? ? ? ? pool.remove(lastUsed);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? //嘗試創(chuàng)建新的連接 if(pool.getAllocatedCount() < maxPerRoute) {//當(dāng)前路由對應(yīng)的連接池可用空閑連接數(shù)+當(dāng)前路由對應(yīng)的連接池已用連接數(shù) < 當(dāng)前路由對應(yīng)的連接池最大并發(fā)數(shù)finalinttotalUsed =this.leased.size();

? ? ? ? ? ? ? ? finalintfreeCapacity = Math.max(this.maxTotal - totalUsed, 0);

? ? ? ? ? ? ? ? if(freeCapacity > 0) {

? ? ? ? ? ? ? ? ? ? finalinttotalAvailable =this.available.size();

? ? ? ? ? ? ? ? ? ? if(totalAvailable > freeCapacity - 1) {//線程池中可用空閑連接數(shù) > (線程池中最大連接數(shù) - 線程池中已用連接數(shù) - 1)if(!this.available.isEmpty()) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? finalE lastUsed =this.available.removeLast();

? ? ? ? ? ? ? ? ? ? ? ? ? ? lastUsed.close();

? ? ? ? ? ? ? ? ? ? ? ? ? ? finalRouteSpecificPool otherpool = getPool(lastUsed.getRoute());

? ? ? ? ? ? ? ? ? ? ? ? ? ? otherpool.remove(lastUsed);

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? finalC conn =this.connFactory.create(route);

? ? ? ? ? ? ? ? ? ? entry = pool.add(conn);

? ? ? ? ? ? ? ? ? ? this.leased.add(entry);

? ? ? ? ? ? ? ? ? ? return entry;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? booleansuccess =false;

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? pool.queue(future);

? ? ? ? ? ? ? ? this.pending.add(future);

? ? ? ? ? ? ? ? success = future.await(deadline);

? ? ? ? ? ? } finally {

? ? ? ? ? ? ? ? // In case of 'success', we were woken up by the

? ? ? ? ? ? ? ? // connection pool and should now have a connection

? ? ? ? ? ? ? ? // waiting for us, or else we're shutting down.

? ? ? ? ? ? ? ? // Just continue in the loop, both cases are checked.? ? ? ? ? ? ? ? pool.unqueue(future);

? ? ? ? ? ? ? ? this.pending.remove(future);

? ? ? ? ? ? }

? ? ? ? ? ? // check for spurious wakeup vs. timeoutif(!success && (deadline !=null) &&? ? ? ? ? ? ? ? (deadline.getTime() <= System.currentTimeMillis())) {

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? thrownewTimeoutException("Timeout waiting for connection");

? ? } finally {

? ? ? ? this.lock.unlock();

? ? }

}

連接重用和保持策略

http的長連接復(fù)用, 其判定規(guī)則主要分兩類。

1. http協(xié)議支持+請求/響應(yīng)header指定

2. 一次交互處理的完整性(響應(yīng)內(nèi)容消費(fèi)干凈)

對于前者, httpclient引入了ConnectionReuseStrategy來處理, 默認(rèn)的采用如下的約定:

HTTP/1.0通過在Header中添加Connection:Keep-Alive來表示支持長連接。

HTTP/1.1默認(rèn)支持長連接, 除非在Header中顯式指定Connection:Close, 才被視為短連接模式。

HttpClientBuilder創(chuàng)建MainClientExec

ConnectionReuseStrategy(連接重用策略)?

org.apache.http.impl.client.DefaultClientConnectionReuseStrategy

MainClientExec處理連接

處理完請求后,獲取到response,通過ConnectionReuseStrategy判斷連接是否可重用,如果是通過ConnectionKeepAliveStrategy獲取到連接最長有效時(shí)間,并設(shè)置連接可重用標(biāo)記。

連接重用判斷邏輯

request首部中包含Connection:Close,不復(fù)用

response中Content-Length長度設(shè)置不正確,不復(fù)用

response首部包含Connection:Close,不復(fù)用

reponse首部包含Connection:Keep-Alive,復(fù)用

都沒命中的情況下,如果HTTP版本高于1.0則復(fù)用

更多參考:https://www.cnblogs.com/mumuxinfei/p/9121829.html

連接釋放原理分析

HttpClientBuilder會(huì)構(gòu)建一個(gè)InternalHttpClient實(shí)例,也是CloseableHttpClient實(shí)例。InternalHttpClient的doExecute方法來完成一次request的執(zhí)行。

會(huì)繼續(xù)調(diào)用MainClientExec的execute方法,通過連接池管理者獲取連接(HttpClientConnection)。

構(gòu)建ConnectionHolder類型對象,傳遞連接池管理者對象和當(dāng)前連接對象。

請求執(zhí)行完返回HttpResponse類型對象,然后包裝成HttpResponseProxy對象(是CloseableHttpResponse實(shí)例)返回。

CloseableHttpClient類其中一個(gè)execute方法如下,finally方法中會(huì)調(diào)用HttpResponseProxy對象的close方法釋放連接。

最終調(diào)用ConnectionHolder的releaseConnection方法釋放連接。

CloseableHttpClient類另一個(gè)execute方法如下,返回一個(gè)HttpResponseProxy對象(是CloseableHttpResponse實(shí)例)。

這種情況下調(diào)用者獲取了HttpResponseProxy對象,可以直接拿到HttpEntity對象。大家關(guān)心的就是操作完HttpEntity對象,使用完InputStream到底需不需要手動(dòng)關(guān)閉流呢?

其實(shí)調(diào)用者不需要手動(dòng)關(guān)閉流,因?yàn)镠ttpResponseProxy構(gòu)造方法里有增強(qiáng)HttpEntity的處理方法,如下。

調(diào)用者最終拿到的HttpEntity對象是ResponseEntityProxy實(shí)例。

ResponseEntityProxy重寫了獲取InputStream的方法,返回的是EofSensorInputStream類型的InputStream對象。

EofSensorInputStream對象每次讀取都會(huì)調(diào)用checkEOF方法,判斷是否已經(jīng)讀取完畢。

checkEOF方法會(huì)調(diào)用ResponseEntityProxy(實(shí)現(xiàn)了EofSensorWatcher接口)對象的eofDetected方法。

EofSensorWatcher#eofDetected方法中會(huì)釋放連接并關(guān)閉流。


綜上,通過CloseableHttpClient實(shí)例處理請求,無需調(diào)用者手動(dòng)釋放連接。

HttpClient在Spring中應(yīng)用

創(chuàng)建ClientHttpRequestFactory

@Bean

public ClientHttpRequestFactory clientHttpRequestFactory()

? ? ? ? throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {

? ? HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();

? ? SSLContext sslContext =newSSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) ->true).build();

? ? httpClientBuilder.setSSLContext(sslContext)

? ? ? ? ? ? .setMaxConnTotal(MAX_CONNECTION_TOTAL)

? ? ? ? ? ? .setMaxConnPerRoute(ROUTE_MAX_COUNT)

? ? ? ? ? ? .evictIdleConnections(CONNECTION_IDLE_TIME_OUT, TimeUnit.MILLISECONDS);

? ? httpClientBuilder.setRetryHandler(newDefaultHttpRequestRetryHandler(RETRY_COUNT,true));

? ? httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());

? ? CloseableHttpClient client = httpClientBuilder.build();

? ? HttpComponentsClientHttpRequestFactory clientHttpRequestFactory =new HttpComponentsClientHttpRequestFactory(client);

? ? clientHttpRequestFactory.setConnectTimeout(CONNECTION_TIME_OUT);

? ? clientHttpRequestFactory.setReadTimeout(READ_TIME_OUT);

? ? clientHttpRequestFactory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIME_OUT);

? ? clientHttpRequestFactory.setBufferRequestBody(false);

? ? return clientHttpRequestFactory;

}

創(chuàng)建RestTemplate

@Bean

public RestTemplate restTemplate()? {

? RestTemplate restTemplate =new RestTemplate(clientHttpRequestFactory());

? ? ? ? restTemplate.setErrorHandler(new DefaultResponseErrorHandler());

? ? ? ? // 修改StringHttpMessageConverter內(nèi)容轉(zhuǎn)換器restTemplate.getMessageConverters().set(1,new StringHttpMessageConverter(StandardCharsets.UTF_8));

? ? ? ? return restTemplate;

}

?Spring官網(wǎng)例子

@SpringBootApplication

public class Application {

? ? privatestaticfinalLogger log = LoggerFactory.getLogger(Application.class);

? ? publicstaticvoid main(String args[]) {

? ? ? ? SpringApplication.run(Application.class);

? ? }

? ? @Bean

? ? public RestTemplate restTemplate(RestTemplateBuilder builder) {

? ? ? ? return builder.build();

? ? }

? ? @Bean

? ? publicCommandLineRunner run(RestTemplate restTemplate)throws Exception {

? ? ? ? returnargs -> {

? ? ? ? ? ? Quote quote = restTemplate.getForObject(

? ? ? ? ? ? ? ? ? ? "https://gturnquist-quoters.cfapps.io/api/random", Quote.class);? ? ? ? ? ? log.info(quote.toString());? ? ? ? };? ? }}

總結(jié)

Apache的HttpClient組件可謂良心之作,細(xì)細(xì)的品味一下源碼可以學(xué)到很多設(shè)計(jì)模式和比編碼規(guī)范。不過在閱讀源碼之前最好了解一下不同版本的HTTP協(xié)議,尤其是HTTP協(xié)議的Keep-Alive模式。使用Keep-Alive模式(又稱持久連接、連接重用)時(shí),Keep-Alive功能使客戶端到服 務(wù)器端的連接持續(xù)有效,當(dāng)出現(xiàn)對服務(wù)器的后繼請求時(shí),Keep-Alive功能避免了建立或者重新建立連接。這里推薦一篇參考鏈接:http://www.itdecent.cn/p/49551bda6619。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容