前言
使用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。