閱讀534 返回首頁    go 技術社區[雲棲]


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.         替換為:

<dependency>

       <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

  上一篇:go RabbitMQ消息隊列(五):Routing 消息路由
  下一篇:go Linux增加swap分區大小