HttpClient詳細(xì)梳理

HttpClient整理資料

1、httpClient

HttpClient是Apache中的一個(gè)開(kāi)源的項(xiàng)目。它實(shí)現(xiàn)了HTTP標(biāo)準(zhǔn)中Client端的所有功能,使用它能夠很容易地進(jìn)行HTTP信息的傳輸。它的各個(gè)版本的使用方式都不太一樣,我使用的版本是4.3.5的,網(wǎng)上比較多的資源是3.+版本的,目前最新已經(jīng)有4.4+版本了,感興趣的都可以看一下。

HttpCLient最關(guān)鍵的方法是執(zhí)行HTTP請(qǐng)求的方法execute。只要把HTTP請(qǐng)求傳入,就可以得到HTTP響應(yīng)。

使用HttpClient請(qǐng)求一個(gè)Http請(qǐng)求的步驟為:

(1)創(chuàng)建一個(gè)HttpClient對(duì)象

(2)創(chuàng)建一個(gè)Request對(duì)象

(3)使用HttpClient來(lái)執(zhí)行Request請(qǐng)求,得到對(duì)方的response

(4)處理response

(5)關(guān)閉HttpClient

下面就針對(duì)這幾個(gè)步驟進(jìn)行展開(kāi)。

2、創(chuàng)建一個(gè)HttpClient對(duì)象

目前最新版的HttpClient的實(shí)現(xiàn)類(lèi)為CloseableHttpClient。創(chuàng)建CloseableHttpClient實(shí)例有兩種方式:

(1)使用CloseableHttpClient的工廠(chǎng)類(lèi)HttpClients的方法來(lái)創(chuàng)建實(shí)例。HttpClients提供了根據(jù)各種默認(rèn)配置來(lái)創(chuàng)建CloseableHttpClient實(shí)例的快捷方法。最簡(jiǎn)單的實(shí)例化方式是調(diào)用HttpClients.createDefault()。

(2)使用CloseableHttpClient的builder類(lèi)HttpClientBuilder,先對(duì)一些屬性進(jìn)行配置(采用裝飾者模式,不斷的.setxxxxx().setxxxxxxxx()就行了),再調(diào)用build方法來(lái)創(chuàng)建實(shí)例。上面的HttpClients.createDefault()實(shí)際上調(diào)用的也就是HttpClientBuilder.create().build()。

build()方法最終是根據(jù)各種配置來(lái)new一個(gè)InternalHttpClient實(shí)例(CloseableHttpClient實(shí)現(xiàn)類(lèi))。IternalHttpClient的定義如下:(忽略方法部分)

classInternalHttpClientextendsCloseableHttpClient{privatefinalLoglog =LogFactory.getLog(getClass());privatefinalClientExecChainexecChain;privatefinalHttpClientConnectionManagerconnManager;privatefinalHttpRoutePlannerroutePlanner;privatefinalLookup cookieSpecRegistry;privatefinalLookup authSchemeRegistry;privatefinalCookieStorecookieStore;privatefinalCredentialsProvidercredentialsProvider;privatefinalRequestConfigdefaultConfig;privatefinalList closeables;}

其中需要注意的有HttpCLientConnectionManager、HttpRoutePlanner和RequestConfig。

(1)HttpClientConnectionManager

HttpClientConnectionManager是一個(gè)HTTP連接管理器。它負(fù)責(zé)新HTTP連接的創(chuàng)建、管理連接的生命周期還有保證一個(gè)HTTP連接在某一時(shí)刻只被一個(gè)線(xiàn)程使用。在內(nèi)部實(shí)現(xiàn)的時(shí)候,manager使用一個(gè)ManagedHttpClientConnection的實(shí)例來(lái)作為一個(gè)實(shí)際connection的代理,負(fù)責(zé)管理connection的狀態(tài)以及執(zhí)行實(shí)際的I/O操作。如果一個(gè)被監(jiān)管的connection被釋放或者被明確關(guān)閉,盡管此時(shí)manager仍持有該連接的代理,但是這個(gè)connection的狀態(tài)不會(huì)被改變也不能再執(zhí)行任何的I/O操作。

HttpClientConnectionManager有兩種具體實(shí)現(xiàn):

a、BasicHttpClientConnectionManager

BasicHttpClientConnectionManager每次只管理一個(gè)connection。不過(guò),雖然它是thread-safe的,但由于它只管理一個(gè)連接,所以只能被一個(gè)線(xiàn)程使用。它在管理連接的時(shí)候如果發(fā)現(xiàn)有相同route的請(qǐng)求,會(huì)復(fù)用之前已經(jīng)創(chuàng)建的連接,如果新來(lái)的請(qǐng)求不能復(fù)用之前的連接,它會(huì)關(guān)閉現(xiàn)有的連接并重新打開(kāi)它來(lái)響應(yīng)新的請(qǐng)求。

b、PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager與BasicHttpClientConnectionManager不同,它管理著一個(gè)連接池(連接池管理部分在第7部分有詳細(xì)介紹)。它可以同時(shí)為多個(gè)線(xiàn)程服務(wù)。每次新來(lái)一個(gè)請(qǐng)求,如果在連接池中已經(jīng)存在route相同并且可用的connection,連接池就會(huì)直接復(fù)用這個(gè)connection;當(dāng)不存在route相同的connection,就新建一個(gè)connection為之服務(wù);如果連接池已滿(mǎn),則請(qǐng)求會(huì)等待直到被服務(wù)或者超時(shí)。

默認(rèn)不對(duì)HttpClientBuilder進(jìn)行配置的話(huà),new出來(lái)的CloeableHttpClient實(shí)例使用的是PoolingHttpClientConnectionManager,這種情況下HttpClientBuilder創(chuàng)建出的HttpClient實(shí)例就可以被多個(gè)連接&多個(gè)線(xiàn)程共用,在應(yīng)用容器起來(lái)的時(shí)候?qū)嵗淮?,在整個(gè)應(yīng)用結(jié)束的時(shí)候再調(diào)用httpClient.close()就行了。在PoolingHttpClientConnectionManager的配置中有兩個(gè)最大連接數(shù)量,分別控制著總的最大連接數(shù)量和每個(gè)route的最大連接數(shù)量。如果沒(méi)有顯式設(shè)置,默認(rèn)每個(gè)route只允許最多2個(gè)connection,總的connection數(shù)量不超過(guò)20。這個(gè)值對(duì)于很多并發(fā)度高的應(yīng)用來(lái)說(shuō)是不夠的,必須根據(jù)實(shí)際的情況設(shè)置合適的值,思路和線(xiàn)程池的大小設(shè)置方式是類(lèi)似的,如果所有的連接請(qǐng)求都是到同一個(gè)url,那可以把MaxPerRoute的值設(shè)置成和MaxTotal一致,這樣就能更高效地復(fù)用連接。HttpClient 4.3.5的設(shè)置方法如下:

privatefinalstaticPoolingHttpClientConnectionManager poolingHttpClientConnectionManager =newPoolingHttpClientConnectionManager();poolingHttpClientConnectionManager.setMaxTotal(MAX_CONNECTION);poolingHttpClientConnectionManager.setDefaultMaxPerRoute(MAX_CONNECTION);CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(poolingHttpClientConnectionManager).build();

(2)HttpRoutePlanner

HttpClient不僅支持簡(jiǎn)單的直連、復(fù)雜的路由策略以及代理。HttpRoutePlanner是基于http上下文情況下,客戶(hù)端到服務(wù)器的路由計(jì)算策略,一般沒(méi)有代理的話(huà),就不用設(shè)置這個(gè)東西。這里有一個(gè)很關(guān)鍵的概念—Route:在HttpClient中,一個(gè)Route指運(yùn)行環(huán)境機(jī)器->目標(biāo)機(jī)器host的一條線(xiàn)路,也就是如果目標(biāo)url的host是同一個(gè),那么它們的route也是一樣的。

(3)RequestConfig

RequestConfig是對(duì)request的一些配置。里面比較重要的有三個(gè)超時(shí)時(shí)間,默認(rèn)的情況下這三個(gè)超時(shí)時(shí)間都為0(如果不設(shè)置request的Config,會(huì)在execute的過(guò)程中使用HttpClientParamConfig的getRequestConfig中用默認(rèn)參數(shù)進(jìn)行設(shè)置),這也就意味著無(wú)限等待,很容易導(dǎo)致所有的請(qǐng)求阻塞在這個(gè)地方無(wú)限期等待。這三個(gè)超時(shí)時(shí)間為:

a、connectionRequestTimeout—從連接池中取連接的超時(shí)時(shí)間

這個(gè)時(shí)間定義的是從ConnectionManager管理的連接池中取出連接的超時(shí)時(shí)間, 如果連接池中沒(méi)有可用的連接,則request會(huì)被阻塞,最長(zhǎng)等待connectionRequestTimeout的時(shí)間,如果還沒(méi)有被服務(wù),則拋出ConnectionPoolTimeoutException異常,不繼續(xù)等待。

b、connectTimeout—連接超時(shí)時(shí)間

這個(gè)時(shí)間定義了通過(guò)網(wǎng)絡(luò)與服務(wù)器建立連接的超時(shí)時(shí)間,也就是取得了連接池中的某個(gè)連接之后到接通目標(biāo)url的連接等待時(shí)間。發(fā)生超時(shí),會(huì)拋出ConnectionTimeoutException異常。

c、socketTimeout—請(qǐng)求超時(shí)時(shí)間

這個(gè)時(shí)間定義了socket讀數(shù)據(jù)的超時(shí)時(shí)間,也就是連接到服務(wù)器之后到從服務(wù)器獲取響應(yīng)數(shù)據(jù)需要等待的時(shí)間,或者說(shuō)是連接上一個(gè)url之后到獲取response的返回等待時(shí)間。發(fā)生超時(shí),會(huì)拋出SocketTimeoutException異常。

注意,4.3.5版本超時(shí)設(shè)置方法和之前的版本不同,下面是一個(gè)設(shè)置各個(gè)超時(shí)時(shí)間的例子。注意,這樣設(shè)置的是該HttpClientc處理的所有request的默認(rèn)配置,如果在構(gòu)造request實(shí)例的時(shí)候不特別設(shè)置,則會(huì)使用默認(rèn)配置。

RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(CON_RST_TIME_OUT).setConnectTimeout(CON_TIME_OUT).setSocketTimeout(SOCKET_TIME_OUT).build();CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();

3、創(chuàng)建一個(gè)Request對(duì)象

HttpClient支持所有的HTTP1.1中的所有定義的請(qǐng)求類(lèi)型:GET、HEAD、POST、PUT、DELETE、TRACE和OPTIONS。對(duì)使用的類(lèi)為HttpGet、HttpHead、HttpPost、HttpPut、HttpDelete、HttpTrace和HttpOptions。Request的對(duì)象建立很簡(jiǎn)單,一般用目標(biāo)url來(lái)構(gòu)造就好了。下面是一個(gè)HttpPost的創(chuàng)建代碼:

HttpPost httpPost = new HttpPost(someGwUrl);

一個(gè)Request還可以addHeader、setEntity、setConfig等,一般這三個(gè)用的比較多。

RequestConfig這個(gè)類(lèi)比較關(guān)鍵,就是request的配置,除了上面說(shuō)到的三個(gè)超時(shí)時(shí)間外,還有一些可能有助于理解處理過(guò)程的配置:

staleConnectionCheckEnabled:這個(gè)配置默認(rèn)為true,HttpClient的execute方法中有下面的代碼,也就是說(shuō)如果這個(gè)設(shè)置為true的話(huà),是會(huì)自動(dòng)關(guān)閉那些狀態(tài)為stale的managed connection所管理的connection和socket(和remote ip)。(這里有個(gè)問(wèn)題,在第7部分中再說(shuō))

if(config.isStaleConnectionCheckEnabled()) {// validate connectionif(managedConn.isOpen()) {this.log.debug("Stale connection check");if(managedConn.isStale()) {this.log.debug("Stale connection detected");? ? ? ? ? ? ? ? ? ? managedConn.close();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }

4、執(zhí)行Request請(qǐng)求

執(zhí)行Request請(qǐng)求就是調(diào)用HttpClient的execute方法。最簡(jiǎn)單的使用方法是調(diào)用execute(final HttpUriRequest request)。

HttpClient允許http連接在特定的Http上下文中執(zhí)行,HttpContext是跟一個(gè)連接相關(guān)聯(lián)的,所以它也只能屬于一個(gè)線(xiàn)程,如果沒(méi)有特別設(shè)定,在execute的過(guò)程中,HttpClient會(huì)自動(dòng)為每一個(gè)connectionnew一個(gè)HttpClientHttpContext。

HttpClientContext localcontext = HttpClientContext.adapt(context!=null?context:newBasicHttpContext());

整個(gè)execute執(zhí)行的常規(guī)流程為:

new一個(gè)http context

|

取出Request和URL

|

根據(jù)HttpRoute的配置看是否需要重寫(xiě)URL

|

根據(jù)URL的host、port和scheme設(shè)置target

|

在發(fā)送前用http協(xié)議攔截器處理request的各個(gè)部分

|

取得驗(yàn)證狀態(tài)、user token來(lái)驗(yàn)證身份

|

從連接池中取一個(gè)可用的連接

|

根據(jù)request的各種配置參數(shù)以及取得的connection構(gòu)造一個(gè)connManaged

|

打開(kāi)managed的connection(包括創(chuàng)建route、dns解析、綁定socket、socket連接等)

|

請(qǐng)求數(shù)據(jù)(包括發(fā)送請(qǐng)求和接收response兩個(gè)階段)

|

查看keepAlive策略,判斷連接是否要復(fù)用,并設(shè)置相應(yīng)標(biāo)識(shí)

|

返回response

|

用http協(xié)議攔截器處理response的各個(gè)部分

5、處理response

HttpReaponse是將服務(wù)端發(fā)回的Http響應(yīng)解析后的對(duì)象。CloseableHttpClient的execute方法返回的response都是CloseableHttpResponse類(lèi)型。可以getFirstHeader(String)、getLastHeader(String)、headerIterator(String)取得某個(gè)Header name對(duì)應(yīng)的迭代器、getAllHeaders()、getEntity、getStatus等,一般這幾個(gè)方法比較常用。

在這個(gè)部分中,對(duì)于entity的處理需要特別注意一下。

一般來(lái)說(shuō)一個(gè)response中的entity只能被使用一次,它是一個(gè)流,這個(gè)流被處理完就不再存在了。

先response.getEntity()再使用HttpEntity#getContent()來(lái)得到一個(gè)java.io.InputStream,然后再對(duì)內(nèi)容進(jìn)行相應(yīng)的處理。

有一點(diǎn)非常重要,想要復(fù)用一個(gè)connection就必須要讓它占有的系統(tǒng)資源得到正確釋放。釋放資源有兩種方法:

a、關(guān)閉和entity相關(guān)的content stream

如果是使用outputStream就要保證整個(gè)entity都被write out,如果是inputStream,則再最后要記得調(diào)用inputStream.close()?;蛘呤褂肊ntityUtils.consume(entity)或EntityUtils.consumeQuietly(entity)來(lái)讓entity被完全耗盡(后者不拋異常)來(lái)做這一工作。EntityUtils中有個(gè)toString方法也很方便的(調(diào)用這個(gè)方法最后也會(huì)自動(dòng)把inputStream close掉的),不過(guò)只有在可以確定收到的entity不是特別大的情況下才能使用。

做過(guò)實(shí)驗(yàn),如果沒(méi)有讓整個(gè)entity被fully consumed,則該連接是不能被復(fù)用的,很快就會(huì)因?yàn)樵谶B接池中取不到可用的連接超時(shí)或者阻塞在這里(因?yàn)樵撨B接的狀態(tài)將會(huì)一直是leased的,即正在被使用的狀態(tài))。所以如果想要復(fù)用connection,一定一定要記得把entity fully consume掉,只要檢測(cè)到stream的eof,是會(huì)自動(dòng)調(diào)用ConnectionHolder的releaseConnection方法進(jìn)行處理的(注意,ConnectionHolder并不是一個(gè)public class,雖然里面有一些跟釋放連接相關(guān)的重要操作,但是卻無(wú)法直接調(diào)用)。

b、關(guān)閉response

執(zhí)行response.close()雖然會(huì)正確釋放掉該connection占用的所有資源,但是這是一種比較暴力的方式,采用這種方式之后,這個(gè)connection就不能被重復(fù)使用了。

從源代碼中可以看出,response.close()調(diào)用了connectionHolder的abortConnection方法,它會(huì)close底層的socket,并且release當(dāng)前的connection,并把reuse的時(shí)間設(shè)為0。這種情況下的connection稱(chēng)為expired connection,也就是client端單方面把連接關(guān)閉。還要等待closeExpiredConnections方法將它從連接池中清除掉(從連接池中清除掉的含義是把它所對(duì)應(yīng)的連接池的entry置為無(wú)效,并且關(guān)掉對(duì)應(yīng)的connection,shutdown對(duì)應(yīng)socket的輸入和輸出流。這個(gè)方法的調(diào)用時(shí)間是需要設(shè)置的)。

關(guān)閉stream和response的區(qū)別在于前者會(huì)嘗試保持底層的連接alive,而后者會(huì)直接shut down并且丟棄connection。

socket是和ip以及port綁定的,但是host相同的請(qǐng)求會(huì)盡量復(fù)用連接池里已經(jīng)存在的connection(因?yàn)樵谶B接池里會(huì)另外維護(hù)一個(gè)route的子連接池,這個(gè)子連接池中每個(gè)connection的狀態(tài)有三種:leased、available和pending,只有available狀態(tài)的connection才能被使用,而fully consume entity就可以讓該連接變?yōu)閍vailable狀態(tài)),如果host地址一樣,則優(yōu)先使用該connection。

如果希望重復(fù)讀取entity中的內(nèi)容,就需要把entity緩存下來(lái)。最簡(jiǎn)單的方式是用entity來(lái)new一個(gè)BufferedHttpEntity,這一操作會(huì)把內(nèi)容拷貝到內(nèi)存中,之后使用這個(gè)BufferedHttpEntity就可以了。

6、關(guān)閉HttpClient

調(diào)用httpClient.close()會(huì)先shut down connection manager,然后再釋放該HttpClient所占用的所有資源,關(guān)閉所有在使用或者空閑的connection包括底層socket。由于這里把它所使用的connection manager關(guān)閉了,所以在下次還要進(jìn)行http請(qǐng)求的時(shí)候,要重新new一個(gè)connection manager來(lái)build一個(gè)HttpClient(也就是在需要關(guān)閉和新建Client的情況下,connection manager不能是單例的)。

7、其他一些東西

(1)關(guān)于keep-alive

在HttpClient.execute得到response之后的相關(guān)代碼中,它會(huì)先取出response的keep-alive頭來(lái)設(shè)置connection是否resuable以及存活的時(shí)間。如果服務(wù)器返回的響應(yīng)中包含了Connection:Keep-Alive(默認(rèn)有的),但沒(méi)有包含Keep-Alive時(shí)長(zhǎng)的頭消息,HttpClient認(rèn)為這個(gè)連接可以永遠(yuǎn)保持。

不過(guò),很多服務(wù)器都會(huì)在不通知客戶(hù)端的情況下,關(guān)閉一定時(shí)間內(nèi)不活動(dòng)的連接,來(lái)節(jié)省服務(wù)器資源。在這種情況下默認(rèn)的策略顯得太樂(lè)觀(guān),我們可能需要自定義連接存活策略,也就是在創(chuàng)建HttpClient的實(shí)例的時(shí)候用下面的代碼。(xxx為自己寫(xiě)的保活策略)

ClosableHttpClientclient =HttpClients.custom().setKeepAliveStrategy(xxx).build();

(2)連接池管理

前面也有說(shuō)到關(guān)于從連接池中取可用連接的部分邏輯。完整的邏輯是:在每收到一個(gè)route請(qǐng)求后,連接池都會(huì)建立一個(gè)以這個(gè)route為key的子連接池,當(dāng)有一個(gè)新的連接請(qǐng)求到來(lái)的時(shí)候,它會(huì)優(yōu)先匹配已經(jīng)存在的子連接池們,如果之前已經(jīng)有過(guò)以這個(gè)route為key的子連接池,那么就會(huì)去試圖取這個(gè)子連接池中狀態(tài)為available的連接,如果此時(shí)有可用的連接,則將取得的available連接狀態(tài)改為leased的,取連接成功。如果此時(shí)子連接池沒(méi)有可用連接,那再看是否達(dá)到了所設(shè)置的最大連接數(shù)和每個(gè)route所允許的最大連接數(shù)的上限,如果還有余量則new一個(gè)新的連接,或者取得lastUsedConnection,關(guān)閉這個(gè)連接、把連接從原來(lái)所在的子連接池刪除,再lease取連接成功。如果此時(shí)的情況不允許再new一個(gè)新的連接,就把這個(gè)請(qǐng)求連接的請(qǐng)求放入一個(gè)queue中排隊(duì)等待,直到得到一個(gè)連接或者超時(shí)才會(huì)從queue中刪去。

一個(gè)連接被release之后,會(huì)從等待連接的queue中喚醒等待連接的服務(wù)進(jìn)行處理。

(3)連接回收策略

當(dāng)連接被管理器收回后,這個(gè)連接仍然存活,但是卻無(wú)法監(jiān)控socket的狀態(tài),也無(wú)法對(duì)I/O事件做出反饋。如果連接被服務(wù)器端關(guān)閉了,客戶(hù)端監(jiān)測(cè)不到連接的狀態(tài)變化(也就無(wú)法根據(jù)連接狀態(tài)的變化,關(guān)閉本地的socket)。

HttpClient為了緩解這一問(wèn)題造成的影響,會(huì)在使用某個(gè)連接前,監(jiān)測(cè)這個(gè)連接是否已經(jīng)過(guò)時(shí),如果服務(wù)器端關(guān)閉了連接,那么連接就會(huì)失效。前面提到的RequestConfig中的staleConnectionCheckEnabled就是用來(lái)控制是否進(jìn)行上述操作,相關(guān)代碼:

if(config.isStaleConnectionCheckEnabled()) {// validate connectionif(managedConn.isOpen()) {this.log.debug("Stale connection check");if(managedConn.isStale()) {this.log.debug("Stale connection detected");? ? ? ? ? ? ? ? ? ? managedConn.close();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }

其中的managedConn.isStale()就是檢查取出的連接是否失效,需要注意的是這種過(guò)時(shí)檢查并不是100%有效,并且會(huì)給每個(gè)請(qǐng)求增加10到30毫秒額外開(kāi)銷(xiāo)。isStale()有一點(diǎn)比較奇怪的是,如果拋出SocketTimeoutException的時(shí)候會(huì)返回false,即意味著此managedConn并不是失效的(如果此managedConn是長(zhǎng)連接的,那么沒(méi)失效是可理解的,但為什么會(huì)拋SocketTimeoutException異常就不懂了)。而這里SocketTimeoutException的發(fā)生與我們前面設(shè)置的RequestConfig.sotimeout是沒(méi)有關(guān)系的,它實(shí)現(xiàn)的機(jī)制是先設(shè)置1ms的超時(shí)時(shí)間,看在這1ms內(nèi)是否能從inputBuffer里面讀到數(shù)據(jù),如果讀到的數(shù)據(jù)長(zhǎng)度為-1(即沒(méi)有數(shù)據(jù)),說(shuō)明此連接失效。但是很經(jīng)常隨機(jī)會(huì)發(fā)生SocketTimeoutException,這時(shí)會(huì)返回false,并且此時(shí)managedConn是open的狀態(tài),這樣就會(huì)跳過(guò)后面的dns解析及socket重新建立和綁定的過(guò)程,直接再次重用之前的connection以及它綁定的socket。

在這里遇到的一個(gè)很糾結(jié)的問(wèn)題:

Http1.1默認(rèn)進(jìn)行的長(zhǎng)連接并不適用于我們的應(yīng)用場(chǎng)景,我們的httpClient是用在服務(wù)端代替客戶(hù)端sdk ?去請(qǐng)求另一個(gè)應(yīng)用的服務(wù)端,并且調(diào)用量非常大,在這種情況下,如果使用默認(rèn)的長(zhǎng)連接就會(huì)一直只去請(qǐng)求對(duì)方的某一臺(tái)服務(wù)器,不管怎么說(shuō),雖然調(diào)用的確實(shí)是相同host的主機(jī)對(duì)功能來(lái)說(shuō)是沒(méi)有問(wèn)題的,但萬(wàn)一對(duì)方服務(wù)器被這樣弄掛了呢?并且這種情況下要是使用了dns負(fù)載均衡技術(shù),那么dns的負(fù)載均衡將不能被執(zhí)行到!這顯然不是我們所希望的。

并且通過(guò)測(cè)試發(fā)現(xiàn),只要是長(zhǎng)連接的connection,在代碼中調(diào)用各種close或者release方法都不能把connection真正關(guān)掉,除非把整個(gè)httpClient.close。

對(duì)于這個(gè)問(wèn)題查了一些資料,里面提到的一個(gè)可行的解決辦法,是建立一個(gè)監(jiān)控線(xiàn)程,來(lái)專(zhuān)門(mén)回收由于長(zhǎng)時(shí)間不活動(dòng)而被判定為失效的連接。這個(gè)監(jiān)控線(xiàn)程可以周期性的調(diào)用ClientConnectionManager類(lèi)的closeExpiredConnections()方法來(lái)關(guān)閉過(guò)期的連接,回收連接池中被關(guān)閉的連接。它也可以選擇性的調(diào)用ClientConnectionManager類(lèi)的closeIdleConnections()方法來(lái)關(guān)閉一段時(shí)間內(nèi)不活動(dòng)的連接。由于這個(gè)解決方案對(duì)于我們的應(yīng)用來(lái)說(shuō)太復(fù)雜了,所以這個(gè)方案的有效性沒(méi)有驗(yàn)證過(guò)。

我原先采用的解決方式是:在每次連接請(qǐng)求到來(lái)的時(shí)候都build一個(gè)新的HttpClient對(duì)象,并且使用BasicHttpClientConnectionManager作為connectionManager。然后在處理完http response之后 close掉這個(gè)HttpClient。目前本地自測(cè)來(lái)看,這種做法不會(huì)出現(xiàn)上面的奇怪問(wèn)題。但是很憂(yōu)傷的是,新建一個(gè)HttpClient的邏輯很重,并且連接不能復(fù)用,會(huì)浪費(fèi)很多時(shí)間。

由于這個(gè)日常需求本身做的就是優(yōu)化性質(zhì)的工作,加上每個(gè)請(qǐng)求都新建HttpClient這一大坨代碼,心里總是有點(diǎn)難受。繼續(xù)找解決辦法。

在嘗試了改系統(tǒng)的各種tcp配置參數(shù)還有其他的socket、系統(tǒng)配置無(wú)果后,最終找到的解決方式卻異常簡(jiǎn)單。簡(jiǎn)單來(lái)說(shuō),其實(shí)我們的應(yīng)用場(chǎng)景下需要的是短連接,這樣只要在request中添加Connection:close的頭部,就可以保證這個(gè)鏈接在這次請(qǐng)求完成之后就被關(guān)掉,只用一次。同時(shí)發(fā)現(xiàn),如果頭中既有Connection:Keep-Alive又有Connection:close的話(huà),Connection:close并不會(huì)有更高的優(yōu)先級(jí),依舊會(huì)保持長(zhǎng)連。

8、總結(jié)

使用HttpClient的時(shí)候特別需要注意的有下面幾個(gè)地方:

(1)連接池最大連接數(shù),不配置為20

(2)同個(gè)route的最大連接數(shù),不配置為2

(3)去連接池中取連接的超時(shí)時(shí)間,不配置則無(wú)限期等待

(4)與目標(biāo)服務(wù)器建立連接的超時(shí)時(shí)間,不配置則無(wú)限期等待

(5)去目標(biāo)服務(wù)器取數(shù)據(jù)的超時(shí)時(shí)間,不配置則無(wú)限期等待

(6)要fully consumed entity,才能正確釋放底層資源

(7)同個(gè)host但ip有多個(gè)的情況,請(qǐng)謹(jǐn)慎使用單例的HttpClient和連接池

(8)HTTP1.1默認(rèn)支持的是長(zhǎng)連接,如果想使用短連接,要在request上加Connection:close的header,不然長(zhǎng)連接是不可能自動(dòng)被關(guān)掉的!

一定要結(jié)合實(shí)際情況來(lái)看是否需要設(shè)置,不然可能導(dǎo)致嚴(yán)重的問(wèn)題。

HttpClient的內(nèi)容遠(yuǎn)不止我上面說(shuō)到的這些,還包括Cookie管理,F(xiàn)luent API等內(nèi)容,由于沒(méi)有實(shí)際使用,理解的并不透徹,后續(xù)繼續(xù)學(xué)習(xí)后再來(lái)補(bǔ)充。

下面是回復(fù)里提到的一個(gè)問(wèn)題:

連接池里的連接是長(zhǎng)連接嗎?還是說(shuō)調(diào)用方拿到這個(gè)連接還要與server三次握手?

TCP的三次握手是發(fā)生在socket的connect方法被調(diào)用的時(shí)候,從代碼里看,這部分的調(diào)用鏈路是MainClientExec#execute->(條件if (!managedConn.isOpen()) )MainClientExec#establishRoute->PoolingHttpClientConnectionManager#connect->HttpClientConnectionOperator#connect->PlainConnectionSocketFactory#connectSocket->Socket#connect。也就是文中第四點(diǎn)講的“打開(kāi)managed的connection(包括創(chuàng)建route、dns解析、綁定socket、socket連接等)”這部分實(shí)現(xiàn)。

如果某個(gè)連接在response的header中帶了keep-alive,那么它是以長(zhǎng)連接的形式存在的,下次有相同目標(biāo)host的請(qǐng)求,它會(huì)優(yōu)先取得這個(gè)連接(包括底層socket的ip和post),如果底層的socket依然可用,那么就用它直接進(jìn)行通信,不會(huì)再進(jìn)行三次握手的過(guò)程。

關(guān)于如何讓一個(gè)放回pool的connection以長(zhǎng)連接存在,這是在MainClientExec#execute中有if (reuseStrategy.keepAlive(response, context)) 里的相關(guān)邏輯給connection打上reusable的標(biāo)并設(shè)置有效時(shí)間。然后在response.entity被fully consumed之后,會(huì)自動(dòng)調(diào)用EofSensorInputStream#close,這個(gè)方法中惠對(duì)connection進(jìn)行release操作,最終會(huì)調(diào)用到ConnectionHolder#releaseConnection(),在這個(gè)方法中對(duì)是否reusable的連接進(jìn)行不同的release操作,對(duì)于reusable的類(lèi)型,并不會(huì)去close底層的socket。所以它就一直保持長(zhǎng)連接。

不過(guò)為什么會(huì)出現(xiàn)明明是長(zhǎng)連接,間隔時(shí)間較長(zhǎng)的話(huà)調(diào)用isStable()卻返回true,然后把socket關(guān)掉呢?個(gè)人猜測(cè)有可能是由于鏈接空閑了一段時(shí)間對(duì)方把長(zhǎng)連接關(guān)掉了,這種情況下是會(huì)重新進(jìn)行三次握手的。

當(dāng)然短連接的情況下,socket也是關(guān)掉了的。

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

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