534
技術社區[雲棲]
HttpClient連接池原理及一次連接時序圖
1. httpClient介紹
HttpClient是一個實現了http協議的開源Java客戶端工具庫,可以通過程序發送http請求。
1.1. HttpClient發送請求和接收響應
1.1.1. 代碼示例
以Get請求為例,以下代碼獲得google主頁內容並將返回結果打印出來。
public final static void main(String[] args) throws Exception {
HttpClient httpclient = new DefaultHttpClient();
try {
HttpGet httpget = new HttpGet("https://www.google.com/");
System.out.println("executing request " + httpget.getURI());
// 創建response處理器
ResponseHandler<String> responseHandler = new BasicResponseHandler();
String responseBody = httpclient.execute(httpget, responseHandler);
System.out.println("----------------------------------------");
System.out.println(responseBody);
System.out.println("----------------------------------------");
} finally {
//HttpClient不再使用時,關閉連接管理器以保證所有資源的釋放
httpclient.getConnectionManager().shutdown();
}
}
1.1.2. 時序圖
httpClient執行一次請求,即運行一次httpclient.execute()方法,時序圖如下:
1.1.3. 時序圖說明
1.1.3.1. 時序圖編號說明
² 1.1、1.2、1.3等均為操作1的子操作,即:操作1 execute()中又分別調用了操作1.1 createClientConnectionManager()、操作1.2 createClientRequestDirector()以及操作1.3 requestDirector 對象的execute()方法等,以此類推。
² 按時間先後順序分別編號為1,2,3等,以此類推。
1.1.3.2. 主要類說明
² 對於圖中各對象,httpClient jar包中均提供對應的接口及相應的實現類。
² 圖中直接與服務器進行socket通信的是最右端接口OperatedClientConnection某一實現類的對象,圖中從右到左進行了層層的封裝,最終開發人員直接使用的是接口HttpClient某一實現類的對象進行請求的發送和響應的接收(如2.1.1代碼示例)。
² 時序圖中各對象所在類關係如下圖類圖所示(僅列出圖中所出現的各個類及方法,參數多的方法省略部分參數,其他類屬性和操作請參照源碼):
1.1.3.2.1. 接口OperatedClientConnection
² 該接口對應一個http連接,與服務器端建立socket連接進行通信。
1.1.3.2.2. 接口ManagedClientConnection
² 該接口對一個http連接OperatedClientConnection進行封裝,ManagedClientConnection維持一個PoolEntry<HttpRoute, OperatedClientConnection>路由和連接的對應。提供方法獲得對應連接管理器,對http連接的各類方法,如建立連接,獲得相應,關閉連接等進行封裝。
1.1.3.2.3. 接口RequestDirector
² RequestDirector為消息的發送執行者,該接口負責消息路由的選擇和可能的重定向,消息的鑒權,連接的分配回收(調用ClientConnectionManager相關方法),建立,關閉等並控製連接的保持。
² 連接是否保持以及保持時間默認原則如下:
n 連接是否保持:客戶端如果希望保持長連接,應該在發起請求時告訴服務器希望服務器保持長連接(http 1.0設置connection字段為keep-alive,http 1.1字段默認保持)。根據服務器的響應來確定是否保持長連接,判斷原則如下:
u 檢查返回response報文頭的Transfer-Encoding字段,若該字段值存在且不為chunked,則連接不保持,直接關閉。其他情況進入下一步。
u 檢查返回的response報文頭的Content-Length字段,若該字段值為空或者格式不正確(多個長度,值不是整數),則連接不保持,直接關閉。其他情況進入下一步
u 檢查返回的response報文頭的connection字段(若該字段不存在,則為Proxy-Connection字段)值
l 如果這倆字段都不存在,則http 1.1版本默認為保持,將連接標記為保持, 1.0版本默認為連接不保持,直接關閉。
l 如果字段存在,若字段值為close 則連接不保持,直接關閉;若字段值為keep-alive則連接標記為保持。
n 連接保持時間:連接交換至連接管理時,若連接標記為保持,則將由連接管理器保持一段時間;若連接沒有標記為保持,則直接從連接池中刪除並關閉entry。連接保持時,保持時間規則如下:
u 保持時間計時開始時間為連接交換至連接池的時間。
u 保持時長計算規則為:獲取keep-alive字段中timeout屬性的值,
l 若該字段存在,則保持時間為 timeout屬性值*1000,單位毫秒。
l 若該字段不存在,則連接保持時間設置為-1,表示為無窮。
n 響應頭日誌示例:
17:59:42.051 [main] DEBUG org.apache.http.headers - << Keep-Alive: timeout=5, max=100
17:59:42.051 [main] DEBUG org.apache.http.headers - << Connection: Keep-Alive
17:59:42.051 [main] DEBUG org.apache.http.headers - << Content-Type: text/html; charset=utf-8
17:59:42.062 [main] DEBUG c.ebupt.omp.sop.srmms.SopHttpClient - Connection can be kept alive for 5000 MILLISECONDS
n 若需要修改連接的保持及重用默認原則,則需編寫子類繼承自AbstractHttpClient,分別覆蓋其 createConnectionReuseStrategy() 和createConnectionKeepAliveStrategy() 方法。
1.1.3.2.4. 接口ClientConnectionManager
² ClientConnectionManager為連接池管理器,是線程安全的。Jar包中提供的具體實現類有BasicClientConnectionManager和PoolingClientConnectionManager。其中BasicClientConnectionManager隻管理一個連接。PoolingClientConnectionManager管理連接池。
² 若有特殊需要,開發人員可自行編寫連接管理器實現該接口。
² 連接管理器自動管理連接的分配以及回收工作,並支持連接保持以及重用。連接保持以及重用由RequestDirector進行控製。
1.1.3.2.5. 接口HttpClient
² 接口HttpClient為開發人員直接使用的發送請求和接收響應的接口,是線程安全的。jar包中提供的實現類有:AbstractHttpClient, DefaultHttpClient, AutoRetryHttpClient, ContentEncodingHttpClient, DecompressingHttpClient, SystemDefaultHttpClient。其中其他所有類都繼承自抽象類AbStractHttpClient,該類使用了門麵模式,對http協議的處理進行了默認的封裝,包括默認連接管理器,默認消息頭,默認消息發送等,開發人員可以覆蓋其中的方法更改其默認設置。
² AbstractHttpClient默認設置連接管理器為BasicClientConnectionManager。若要修改連接管理器,則應該采用以下方式之一:
n 初始化時,傳入連接池,例如:
ClientConnectionManager connManager = new PoolingClientConnectionManager();
HttpClient httpclient = new DefaultHttpClient(connManager);
n 編寫httpClient接口的實現類,繼承自AbstractHttpClient並覆蓋其createClientConnectionManager()方法,在方法中創建自己的連接管理器。
1.1.3.3. 方法說明
² createClientConnectionManager(),創建連接池,該方法為protected。子類可覆蓋修改默認連接池。
² createClientRequestDirector(),創建請求執行者,該方法為protected。子類可覆蓋但一般不需要。
² httpClient中調用1.2方法所創建的請求執行者requestDirector的execute()方法。該方法中依次調用如下方法:
n 1.3.1調用連接管理器的requestConnection(route, userToken)方法,該方法調用連接池httpConnPool的lease方法,創建一個Future<HttpPoolEntry>。Futrue用法參見Java標準API。返回clientConnectionRequest。
n 1.3.2.調用clientConnectionRequest的getConnection(timeout, TimeUnit.MILLISECONDS)方法,該方法負責將連接池中可用連接分配給當前請求,具體如下:
u 創建clientConnectionOperator。
u 執行1.3.1中創建的Future的任務,該任務獲得當前可用的poolEntry<router,OperatedClientConnection>並封裝成managedClientConnectionImpl返回。
n 1.3.3. 調用 tryConnect(roureq, context)方法,該方法最終調用OperatedClientConnection的openning方法,與服務器建立socket連接。
n 1.3.4. 調用 tryExecute(roureq, context)方法,該方法最終調用OperatedClientConnection的receiveResponseHeader()和receiveResponseEntity()獲得服務器響應。
n 1.3.5 判斷連接是否保持用來重用,若保持,則設置保持時間,並將連接標記為可重用不保持則調用managedClientConnectionImpl的close方法關閉連接,該方法最終調用OperatedClientConnection的close()方法關閉連接。
² 最終respose返回至httpClient。
² 發送請求的線程需處理當前連接,若已被標記為重用,則交還至連接池管理器;否則,關閉當前連接。(使用響應處理器ResponseHanler)。本次請求結束。
1.2. httpClient連接池
若連接管理器配置為PoolingClientConnectionManager,則httpClient將使用連接池來管理連接的分配,回收等操作。
1.2.1. 連接池結構
連接池結構圖如下,其中:
l PoolEntry<HttpRoute, OperatedClientConnection>為路由和連接的對應。
l routeToPool可以多個(圖中僅示例兩個);圖中各隊列大小動態變化,並不相等;
l maxTotal限製的是外層httpConnPool中leased集合和available隊列的總和的大小,leased和available的大小沒有單獨限製;
l 同理:maxPerRoute限製的是routeToPool中leased集合和available隊列的總和的大小;
1.2.2. 連接池工作原理
1.2.2.1. 分配連接
分配連接給當前請求包括兩部分:1. 從連接池獲取可用連接PoolEntry;2.將連接與當前請求綁定。其中第一部分從連接池獲取可用連接的過程為:
1. 獲取route對應連接池routeToPool中可用的連接,有則返回該連接。若沒有則轉入下一步。
2. 若routeToPool和外層HttpConnPool連接池均還有可用的空間,則新建連接,並將該連接作為可用連接返回;否則進行下一步
3. 將當前請求放入pending隊列,等待執行。
4. 上述過程中包含各個隊列和集合的刪除,添加等操作以及各種判斷條件,具體流程如下:
1.2.2.2. 回收連接
連接用完之後連接池需要進行回收,具體流程如下:
1. 若當前連接標記為重用,則將該連接從routeToPool中的leased集合刪除,並添加至available隊列,同樣的將該請求從外層httpConnPool的leased集合刪除,並添加至其available隊列。同時喚醒該routeToPool的pending隊列的第一個PoolEntryFuture。將其從pending隊列刪除,並將其從外層httpConnPool的pending隊列中刪除。
2. 若連接沒有標記為重用,則分別從routeToPool和外層httpConnPool中刪除該連接,並關閉該連接。
1.2.2.3. 過期和空閑連接的關閉
² 連接如果標記為保持時,將由連接管理器保持一段時間,此時連接可能出現的情況是:
n 連接處於空閑狀態,時間已超過連接保持時間
n 連接處於空閑狀態,時間沒有超過連接保持時間
n 以上兩種情況中,隨時都會出現連接的服務端已關閉的情況,而此時連接的客戶端並沒有阻塞著去接受服務端的數據,所以客戶端不知道連接已關閉,無法關閉自身的socket。
² 連接池提供的方法:
n 首先連接池在每個請求獲取連接時,都會在RouteToPool的available隊列獲取Entry並檢測此時Entry是否已關閉或者已過期,若是則關閉並移除該Entry。
n closeExpiredConnections()該方法關閉超過連接保持時間的空閑連接。
n closeIdleConnections(timeout,tunit)該方法關閉空閑時間超過timeout的連接,空閑時間從交還給連接管理器時開始,不管是否已過期超過空閑時間則關閉。所以Idle時間應該設置的盡量長一點。
n 以上兩個方法連接關閉的過程均是:
u 關閉entry;
u RouteToPool中刪除當前entry。先刪available隊列中的,如果沒有,再刪除leased集合中的。
u httpConnPool中刪除當前entry。刪除過程同RouteToPool
u 喚醒阻塞在RouteToPool中的第一個future。
1.3. 相關原理說明
1.3.1. Tcp連接的關閉
Http連接實際上在傳輸層建立的是tcp連接,最終利用的是socket進行通信。http連接的保持和關閉實際上都和TCP連接的關閉有關。TCP關閉過程如下圖:
說明:
² TCP連接程序中使用socket編程進行實現。一條TCP是一條抽象的連接通道,由通信雙方的IP+端口號唯一確定,兩端分別通過socket實例進行操作,一個socket實例包括一個輸入通道和輸出通道,一端的輸出通道為另一端的輸入通道。
² Tcp連接的關閉是連接的兩端分別都需要進行關閉(調用close(socket),該函數執行發送FIN,等待ACK等圖示操作)。實際上沒有客戶端和服務端的區別,隻有主動關閉和被動關閉的區別。對於上層的其http連接,實際上也就是http服務端主動關閉或者http客戶端主動關閉,而不管誰主動,最終服務端和客戶端都需要調用close(socket)關閉連接。
² 主動關閉的一端A調用了close函數之後,若另一端B並沒有阻塞著等待著數據,就無法檢測到連接的A端已關閉,就沒法關閉自身的socket,造成資源的浪費。http連接都是一次請求和響應,之後便交回給連接管理池,因此在http連接池中應當能夠移除已過期或者空閑太久的連接,因為他們可能已經被服務器端關閉或者客戶端短期內不再使用。
² TIME_WAIT狀態:
n 可靠地實現TCP全雙工連接的終止
在進行關閉連接四路握手協議時,最後的ACK是由主動關閉端發出的,如果這個最終的ACK丟失,被動關閉端將重發最終的FIN,因此主動關閉端必須維護狀態信息允許它重發最終的ACK。如果不維持這個狀態信息,那麼主動關閉端將發送RST分節(複位),被動關閉端將此分節解釋成一個錯誤(在java中會拋出connection reset的SocketException)。因而,要實現TCP全雙工連接的正常終止,主動關閉的客戶端必須維持狀態信息進入TIME_WAIT狀態。
n 允許老的重複分節在網絡中消逝
TCP分節可能由於路由器異常而“迷途”,在迷途期間,TCP發送端可能因確認超時而重發這個分節,迷途的分節在路由器修複後也會被送到最終目的地,這個原來的迷途分節就稱為lost duplicate。在關閉一個TCP連接後,馬上又重新建立起一個相同的IP地址和端口之間的TCP連接,後一個連接被稱為前一個連接的化身(incarnation),那麼有可能出現這種情況,前一個連接的迷途重複分組在前一個連接終止後出現,從而被誤解成從屬於新的化身。為了避免這個情況,TCP不允許處於TIME_WAIT狀態的連接啟動一個新的化身,因為TIME_WAIT狀態持續2MSL,就可以保證當成功建立一個TCP連接的時候,來自連接先前化身的重複分組已經在網絡中消逝。
2. httpClient最佳實踐
2.1. 總原則
2.1.1. 版本
原Commons HttpClient:3.x不再升級維護,使用Apache HttpComponents的HttpClient代替。Pom文件修改如下:
1. 原maven依賴:
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
2. 替換為:
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.1</version>
</dependency>
2.1.2. 使用http連接池管理器
² 編寫類繼承自DefaultHttpClient(以下假設為SopHttpClient),覆蓋其createClientConnectionManager()方法,方法中創建連接池管理器。
² 開啟一個線程(假設為IdleConnectionMonitorThread)用來清除連接池中空閑和過期的連接。
2.1.3. 保持HttpClient單例
Spring配置中使用默認scope,即單例模式,其他類使用時由Spring配置進行依賴注入,不要使用new方法。SopHttpClient應該提供方法destroy()並配置在Spring銷毀該bean前調用,destory()方法中關閉對應連接池管理器和監控線程IdleConnectionMonitorThread。
2.1.4. 異常處理機製(請求和響應):
編寫類實現接口HttpRequestRetryHandler(可參照默認實現DefaultHttpRequestRetryHandler),並覆蓋AbstractHttpClient中的createHttpRequestRetryHandler()方法創建新的重試處理機製。
2.1.5. 參數可配置
各參數(連接池默認ip、端口和大小等,超時時間等)盡量都集中在SopHttpClient類中,設置為由Spring進行統一配置,且提供接口在程序中修改。
2.1.6. 保證連接交回至連接池管理器
2.1.6.1. 方式
HttpResponse response = httpclient.execute(httpMethod);
HttpEntity entity = response.getEntity();
這兩段代碼返回的entity是HttpEntity的實現類BasicManagedEntity。此時與本次請求關聯的連接尚未歸還至連接管理器。需要調用以下兩條語句:
InputStream instream = entity.getContent();//獲得響應具體內容
//處理響應:代碼省略
instream.close();//關閉輸入流同時會將連接交回至連接處理器
2.1.6.2. 使用默認的響應處理器BasicResponseHandler
² httpClient Jar包中提供BasicResponseHandler。如果返回的類型能確定需要解碼為String類型的話,推薦使用該響應處理器。
² 該處理器解碼http連接響應字節流為String類型,對返回碼>=300的響應進行了異常封裝,並能夠保證連接交還給連接池管理器。
² 該處理器將字節解碼為字符的過程依次如下:
1. 如果響應http報文Head部分由指定的charset,則使用該charset進行解碼,否則進行下一步。例如使用UTF-8解碼以下響應:
17:59:42.051 [main] DEBUG org.apache.http.headers - << Content-Type: text/html; charset=utf-8
2. 如果響應報文未執行charset,則使用傳入EntityUntils.toString()時指定的charset進行解碼。否則進行下一步
3. 使用ISO-8859-1進行解碼。
2.1.6.3. BasicManagedEntity關閉連接池管理器原理
1. BasicManagedEntity實現了三個接口:HttpEntity,ConnectionReleaseTrigger, EofSensorWatcher。
調用BasicManagedEntity的getContent方法時,實際上初始化了EofSensorInputStream的實例,並將BasicManagedEntity當前對象自身作為EofSensorWatcher傳入。
//BasicManagedEntity類的繼承體係,HttpEntityWrapper實現了接口HttpEntity
public class BasicManagedEntity extends HttpEntityWrapper
implements ConnectionReleaseTrigger, EofSensorWatcher
// BasicManagedEntity的getContent方法:
@Override
public InputStream getContent() throws IOException {
return new EofSensorInputStream(wrappedEntity.getContent(), this);
}
// EofSensorInputStream構造函數聲明
public EofSensorInputStream(final InputStream in,final EofSensorWatcher watcher);
2. 調用EofSensorInputStream的close方法,該方法調用自身的checkClose()方法,checkClose()方法中調入了傳入的EofSensorWatcher watcher的streamClosed()方法並關閉輸入流,由於上一步驟中實際傳入的watcher是BasicManagedEntity的實例,因此實際上調用的是BasicManagedEntity的streamClose()方法。
//close方法
@Override
public void close() throws IOException {
// tolerate multiple calls to close()
selfClosed = true;
checkClose();
}
//checkClose方法
protected void checkClose() throws IOException {
if (wrappedStream != null) {
try {
boolean scws = true; // should close wrapped stream?
if (eofWatcher != null)
scws = eofWatcher.streamClosed(wrappedStream);
if (scws)
wrappedStream.close();
} finally {
wrappedStream = null;
}
}
}
3. BasicManagedEntity的streamClose()方法中將連接交回至連接池管理器。
public boolean streamClosed(InputStream wrapped) throws IOException {
try {
if (attemptReuse && (managedConn != null)) {
boolean valid = managedConn.isOpen();
// this assumes that closing the stream will
// consume the remainder of the response body:
try {
wrapped.close();
managedConn.markReusable();
} catch (SocketException ex) {
if (valid) {
throw ex;
}
}
}
} finally {
releaseManagedConnection();
}
return false;
}
2.1.7. 其他
httpClient 提供了非常靈活的架構,同時提供了很多接口,需要修改時,找到對應接口和默認實現類,參照默認實現類進行修改即可(或繼承默認實現類,覆蓋其對應方法)。通常需要更改的類有AbstractHttpClient和各種handler以及Strategy
最後更新:2017-04-03 12:55:12