HttpClient 詳解

做者:小白豆豆5
連接:https://www.jianshu.com/p/14c005e9287c
來源:簡書
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
java

1.HTTP 請求建立流程

使用 HttpClient 執行一個 Http 請求的步驟爲:緩存

(1)建立一個 HttpClient 對象服務器

(2)建立一個 HttpRequest 對象cookie

(3)使用 HttpClient 來執行 HttpRequest請求,獲得對方的 HttpResponse網絡

(4)處理 HttpResponse併發

(5)關閉這次請求鏈接負載均衡

2.建立一個 HttpClient 對象

目前最新版的 HttpClient 的實現類爲 CloseableHttpClient。建立 CloseableHttpClient 實例有兩種方式:socket

  1. 使用 CloseableHttpClient 的工廠類 HttpClients 的方法來建立實例。HttpClients 提供了根據各類默認配置來建立 CloseableHttpClient 實例的快捷方法。最簡單的實例化方式是調用HttpClients.createDefault()。
  2. 使用 CloseableHttpClient 的 builder 類 HttpClientBuilder,先對一些屬性進行配置(採用裝飾者模式,不斷的.setxxxxx().setxxxxxxxx()就好了),再調用 build() 方法來建立實例。上面的HttpClients.createDefault() 實際上調用的也就是HttpClientBuilder.create().build()。

build() 方法最終是根據各類配置來 new 一個 InternalHttpClient 實例(CloseableHttpClient 實現類)。tcp

IternalHttpClient 類的實現以下:(忽略方法部分)ide

class InternalHttpClient extends CloseableHttpClient implements Configurable { private final Log log = LogFactory.getLog(this.getClass()); private final ClientExecChain execChain; private final HttpClientConnectionManager connManager; private final HttpRoutePlanner routePlanner; private final Lookup<CookieSpecProvider> cookieSpecRegistry; private final Lookup<AuthSchemeProvider> authSchemeRegistry; private final CookieStore cookieStore; private final CredentialsProvider credentialsProvider; private final RequestConfig defaultConfig; private final List<Closeable> closeables; public InternalHttpClient(ClientExecChain execChain, HttpClientConnectionManager connManager, HttpRoutePlanner routePlanner, Lookup<CookieSpecProvider> cookieSpecRegistry, Lookup<AuthSchemeProvider> authSchemeRegistry, CookieStore cookieStore, CredentialsProvider credentialsProvider, RequestConfig defaultConfig, List<Closeable> closeables) { Args.notNull(execChain, "HTTP client exec chain"); Args.notNull(connManager, "HTTP connection manager"); Args.notNull(routePlanner, "HTTP route planner"); this.execChain = execChain; this.connManager = connManager; this.routePlanner = routePlanner; this.cookieSpecRegistry = cookieSpecRegistry; this.authSchemeRegistry = authSchemeRegistry; this.cookieStore = cookieStore; this.credentialsProvider = credentialsProvider; this.defaultConfig = defaultConfig; this.closeables = closeables; } ... }

其中須要注意的配置字段包括: HttpClientConnectionManager、HttpRoutePlanner 和 RequestConfig:

    1)HttpClientConnectionManager

            HttpClientConnectionManager 是一個 HTTP 鏈接管理器。它負責新 HTTP 鏈接的建立、管理鏈接的生命週期還有保證一個 HTTP 鏈接在某一時刻只被一個線程使用。在內部實現的時候,manager 使用一個 ManagedHttpClientConnection 的實例來做爲一個實際 connection 的代理,負責管理 connection 的狀態以及執行實際的 I/O 操做。若是一個被監管的 connection 被釋放或者被明確關閉,儘管此時 manager 仍持有該鏈接的代理,可是這個 connection 的狀態不會被改變也不能再執行任何的 I/O 操做。

HttpClientConnectionManager 有兩種具體實現:

  • BasicHttpClientConnectionManager

  BasicHttpClientConnectionManager 每次只管理一個 connection。不過,雖然它是 thread-safe 的,但因爲它只管理一個鏈接,因此只能被一個線程使用。它在管理鏈接的時候若是發現有相同route 的請求,會複用以前已經建立的鏈接,若是新來的請求不能複用以前的鏈接,它會關閉現有的鏈接並從新打開它來響應新的請求。

  • PoolingHttpClientConnectionManager

  PoolingHttpClientConnectionManager 與 BasicHttpClientConnectionManager 不一樣,它管理着一個鏈接池(鏈接池管理部分在第7部分有詳細介紹)。它能夠同時爲多個線程服務。每次新來一個請求,若是在鏈接池中已經存在 route 相同而且可用的 connection,鏈接池就會直接複用這個 connection;當不存在 route 相同的 connection,就新建一個 connection 爲之服務;若是鏈接池已滿,則請求會等待直到被服務或者超時(Timeout waiting for connection from pool)。

  默認不對 HttpClientBuilder 進行配置的話,new 出來的 CloeableHttpClient 實例使用的是 PoolingHttpClientConnectionManager,這種狀況下 HttpClientBuilder 建立出的 HttpClient 實例就能夠被多個鏈接和多個線程共用,在應用容器起來的時候實例化一次,在整個應用結束的時候再調用 httpClient.close() 就好了。在 PoolingHttpClientConnectionManager 的配置中有兩個最大鏈接數量,分別控制着總的最大鏈接數量(MaxTotal)和每一個 route 的最大鏈接數量(DefaultMaxPerRoute)。若是沒有顯式設置,默認每一個 route 只容許最多2個connection,總的 connection 數量不超過 20。這個值對於不少併發度高的應用來講是不夠的,必須根據實際的狀況設置合適的值,思路和線程池的大小設置方式是相似的,若是全部的鏈接請求都是到同一個url,那能夠把 MaxPerRoute 的值設置成和 MaxTotal 一致,這樣就能更高效地複用鏈接。HttpClient 4.3.5的設置方法以下:

private final static PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(MAX_CONNECTION);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(MAX_CONNECTION);
CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(poolingHttpClientConnectionManager).build();

    2)HttpRoutePlanner

        HttpClient 不只支持簡單的直連、複雜的路由策略以及代理。HttpRoutePlanner 是基於 http 上下文狀況下,客戶端到服務器的路由計算策略,通常沒有代理的話,就不用設置這個東西。這裏有一個很關鍵的概念—route:在 HttpClient 中,一個 route 指運行環境機器->目標機器 host 的一條線路,也就是若是目標 url 的 host 是同一個,那麼它們的 route 也是同樣的。

    3)RequestConfig

    RequestConfig 是對 request 的一些配置。裏面比較重要的有三個超時時間,默認的狀況下這三個超時時間都爲-1(若是不設置request的Config,會在execute的過程當中使用HttpClientParamConfig 的 getRequestConfig 中用默認參數進行設置),這也就意味着無限等待,很容易致使全部的請求阻塞在這個地方無限期等待。這三個超時時間爲:

    (1)connectionRequestTimeout——從鏈接池中取鏈接的超時時間

      這個時間定義的是從 ConnectionManager 管理的鏈接池中取出鏈接的超時時間, 若是鏈接池中沒有可用的鏈接,則 request 會被阻塞,最長等待 connectionRequestTimeout 的時間,若是尚未被服務,則拋出 ConnectionPoolTimeoutException 異常,不繼續等待。

    (2)connectTimeout——鏈接超時時間

      這個時間定義了經過網絡與服務器創建鏈接的超時時間,也就是取得了鏈接池中的某個鏈接以後到接通目標 url 的鏈接等待時間。發生超時,會拋出ConnectionTimeoutException異常。

    (3)socketTimeout——請求超時時間

      這個時間定義了 socket 讀數據的超時時間,也就是鏈接到服務器以後到從服務器獲取響應數據須要等待的時間,或者說是鏈接上一個 url 以後到獲取 response 的返回等待時間。發生超時會拋出SocketTimeoutException異常。

注意,4.3.5版本超時設置方法和以前的版本不一樣,下面是一個設置各個超時時間的例子。注意,這樣設置的是該 HttpClient 處理的全部 request 的默認配置,若是在構造 request 實例的時候不特別設置,則會使用默認配置。

RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(CON_RST_TIME_OUT).setConnectTimeout(CON_TIME_OUT).setSocketTimeout(SOCKET_TIME_OUT).build();
HttpEntityEnclosingRequestBase httpRequest = new HttpEntityEnclosingRequestBase() {
@Override
public String getMethod() {
return method;
}
};
httpRequest.setConfig(requestConfig);

3.建立一個 Request 對象

HttpClient 支持全部的 HTTP1.1 中的全部定義的請求類型:GET、HEAD、POST、PUT、DELETE、TRACE 和 OPTIONS。對使用的類爲 HttpGet、HttpHead、HttpPost、HttpPut、HttpDelete、HttpTrace 和 HttpOptions。Request的對象創建很簡單,通常用目標url來構造就行了。下面是一個HttpPost的建立代碼:

HttpPost httpPost = new HttpPost(someGwUrl);

一個 Request 還能夠 addHeader、setEntity、setConfig 等,通常這三個用的比較多。

固然,你也能夠經過建立一個 HttpEntityEnclosingRequestBase 對象做爲 Request 對象,配置代碼以下:

HttpEntityEnclosingRequestBase httpRequest = new HttpEntityEnclosingRequestBase() { @Override public String getMethod() { return method;   // 對應的GET,POST,DELETE等 } };
httpRequest.setURI();
httpRequest.setEntity();

4.執行 Request 請求

執行 Request 請求就是調用 HttpClient 的execute方法。最簡單的使用方法是調用 execute(final HttpUriRequest request)。

HttpClient 容許 http 鏈接在特定的 Http 上下文中執行,HttpContext 是跟一個鏈接相關聯的,因此它也只能屬於一個線程,若是沒有特別設定,在 execute 的過程當中,HttpClient 會自動爲每個connection new 一個 HttpClientHttpContext。

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

整個 execute 執行的常規流程爲:

  1. new一個 http context
  2. 取出 Request 和URL
  3. 根據 HttpRoute 的配置看是否須要重寫URL
  4. 根據 URL 的host、port和scheme設置target
  5. 在發送前用 http 協議攔截器處理 request 的各個部分
  6. 取得驗證狀態、user token來驗證身份
  7. 從鏈接池中取一個可用的鏈接
  8. 根據request的各類配置參數以及取得的connection構造一個connManaged
  9. 打開managed的connection(包括建立route、dns解析、綁定socket、socket鏈接等)
  10. 請求數據(包括髮送請求和接收response兩個階段)
  11. 查看keepAlive策略,判斷鏈接是否要複用,並設置相應標識
  12. 返回response
  13. 用http協議攔截器處理response的各個部分

5.處理 response

HttpReaponse 是將服務端發回的 Http 響應解析後的對象。CloseableHttpClient 的 execute 方法返回的 response 都是 CloseableHttpResponse 類型。能夠 getFirstHeader(String)、getLastHeader(String)、headerIterator(String)取得某個Header name對應的迭代器、getAllHeaders()、getEntity、getStatus等,通常這幾個方法比較經常使用。在這個部分中,對於 entity 的處理須要特別注意一下。通常來講一個 response 中的 entity 只能被使用一次,它是一個流,這個流被處理完就再也不存在了。先 response.getEntity() 再使用 HttpEntity.getContent()來獲得一個java.io.InputStream,而後再對內容進行相應的處理。

有一點很是重要,想要複用一個 connection 就必需要讓它佔有的系統資源獲得正確釋放。釋放資源有兩種方法:

  1)關閉和 entity 相關的 content stream

  若是是使用 outputStream 就要保證整個 entity 都被 write out,若是是 inputStream,則在最後要記得調用 inputStream.close()。或者使用 EntityUtils.consume(entity) 或EntityUtils.consumeQuietly(entity) 來讓 entity 被徹底耗盡(後者不拋異常)來作這一工做。EntityUtils 中有個 toString 方法也很方便的(調用這個方法最後也會自動把 inputStream close掉的),不過只有在能夠肯定收到的 entity 不是特別大的狀況下才能使用。

作過實驗,若是沒有讓整個 entity 被 fully consumed,則該鏈接是不能被複用的,很快就會由於在鏈接池中取不到可用的鏈接超時或者阻塞在這裏(由於該鏈接的狀態將會一直是 leased 的,即正在被使用的狀態)。因此若是想要複用 connection,必定必定要記得把 entity fully consume 掉,只要檢測到 stream 的 eof,纔會自動調用 ConnectionHolder 的 releaseConnection 方法進行處理(注意,ConnectionHolder 並非一個public class,雖然裏面有一些跟釋放鏈接相關的重要操做,可是卻沒法直接調用)。

關閉response

  2)關閉response

  執行 response.close() 雖然會正確釋放掉該 connection 佔用的全部資源,可是這是一種比較暴力的方式,採用這種方式以後,這個 connection 就不能被重複使用了。從源代碼中能夠看出,response.close() 調用了 connectionHolder 的 abortConnection 方法,它會 close 底層的 socket,而且 release 當前的 connection,並把 reuse 的時間設爲0。這種狀況下的 connection 稱爲expired connection,也就是 client 端單方面把鏈接關閉。還要等待 closeExpiredConnections 方法將它從鏈接池中清除掉(從鏈接池中清除掉的含義是把它所對應的鏈接池的 entry 置爲無效,而且關掉對應的 connection,shutdown 對應 socket 的輸入和輸出流,這個方法的調用時間是須要設置的)。

  關閉stream和response的區別在於前者會嘗試保持底層的鏈接alive,然後者會直接shut down而且丟棄connection。

  socket是和ip以及port綁定的,可是host相同的請求會盡可能複用鏈接池裏已經存在的 connection(由於在鏈接池裏會另外維護一個 route 的子鏈接池,這個子鏈接池中每一個 connection 的狀態有三種:leased、available 和 pending,只有 available 狀態的 connection 才能被使用,而 fully consume entity 就可讓該鏈接變爲available狀態),若是 host 地址同樣,則優先使用connection。若是但願重複讀取 entity 中的內容,就須要把 entity 緩存下來。最簡單的方式是用 entity 來 new 一個 BufferedHttpEntity,這一操做會把內容拷貝到內存中,以後使用這個BufferedHttpEntity就能夠了。

6.關閉 httpClient

調用 httpClient.close() 會先 shut down connection manager,而後再釋放該 HttpClient 所佔用的全部資源,關閉全部在使用或者空閒的 connection 包括底層 socket。因爲這裏把它所使用的connection manager 關閉了,因此在下次還要進行 http 請求的時候,要從新 new 一個 connection manager 來 build 一個 HttpClient(也就是在須要關閉和新建 Client 的狀況下,connection manager不能是單例的)。

7.其餘一些東西

  (1)關於keep-alive

  在 HttpClient.execute 獲得 response 以後的相關代碼中,它會先取出 response 的 keep-alive 頭來設置 connection 是否 resuable 以及存活的時間。若是服務器返回的響應中包含了Connection:Keep-Alive(默認有的),但沒有包含 Keep-Alive 時長的頭消息,HttpClient 認爲這個鏈接能夠永遠保持。不過,不少服務器都會在不通知客戶端的狀況下,關閉必定時間內不活動的鏈接,來節省服務器資源。在這種狀況下默認的策略顯得太樂觀,咱們可能須要自定義鏈接存活策略,也就是在建立 HttpClient 的實例的時候用下面的代碼。(xxx爲本身寫的保活策略)

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

  (2)鏈接池管理

  前面也有說到關於從鏈接池中取可用鏈接的部分邏輯。完整的邏輯是:在每收到一個 route 請求後,鏈接池都會創建一個以這個 route 爲 key 的子鏈接池,當有一個新的鏈接請求到來的時候,它會優先匹配已經存在的子鏈接池們,若是以前已經有過以這個 route 爲 key 的子鏈接池,那麼就會去試圖取這個子鏈接池中狀態爲 available 的鏈接,若是此時有可用的鏈接,則將取得的 available 鏈接狀態改成 leased 的,取鏈接成功。若是此時子鏈接池沒有可用鏈接,那再看是否達到了所設置的最大鏈接數和每一個 route 所容許的最大鏈接數的上限,若是還有餘量則 new 一個新的鏈接,或者取得 lastUsedConnection,關閉這個鏈接、把鏈接從原來所在的子鏈接池刪除,再 lease 取鏈接成功。若是此時的狀況不容許再new一個新的鏈接,就把這個請求鏈接的請求放入一個 queue 中排隊等待,直到獲得一個鏈接或者超時纔會從 queue 中刪去。一個鏈接被 release 以後,會從等待鏈接的 queue 中喚醒等待鏈接的服務進行處理。

  (3)鏈接回收策略

  當鏈接被管理器收回後,這個鏈接仍然存活,可是卻沒法監控 socket 的狀態,也沒法對 I/O 事件作出反饋。若是鏈接被服務器端關閉了,客戶端監測不到鏈接的狀態變化(也就沒法根據鏈接狀態的變化,關閉本地的socket)。HttpClient 爲了緩解這一問題形成的影響,會在使用某個鏈接前,監測這個鏈接是否已通過時,若是服務器端關閉了鏈接,那麼鏈接就會失效。前面提到的RequestConfig 中的 staleConnectionCheckEnabled 就是用來控制是否進行上述操做,相關代碼:

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

其中的 managedConn.isStale() 就是檢查取出的鏈接是否失效,須要注意的是這種過期檢查並非100%有效,而且會給每一個請求增長10到30毫秒額外開銷。isStale()有一點比較奇怪的是,若是拋出SocketTimeoutException 的時候會返回 false,即意味着此 managedConn 並非失效的(若是此 managedConn 是長鏈接的,那麼沒失效是可理解的,但爲何會拋 SocketTimeoutException 異常就不懂了)。而這裏 SocketTimeoutException 的發生與咱們前面設置的 RequestConfig.socketTimeout 是沒有關係的,它實現的機制是先設置 1ms 的超時時間,看在這 1ms 內是否能從inputBuffer 裏面讀到數據,若是讀到的數據長度爲 -1(即沒有數據),說明此鏈接失效。可是很常常隨機會發生 SocketTimeoutException,這時會返回 false,而且此時 managedConn 是 open 的狀態,這樣就會跳事後面的 dns 解析及 socket 從新創建和綁定的過程,直接再次重用以前的 connection 以及它綁定的 socket。

在這裏遇到的一個很糾結的問題:

Http1.1 默認進行的長鏈接並不適用於咱們的應用場景,咱們的 httpClient 是用在服務端代替客戶端 sdk 去請求另外一個應用的服務端,而且調用量很是大,在這種狀況下,若是使用默認的長鏈接就會一直只去請求對方的某一臺服務器,無論怎麼說,雖然調用的確實是相同 host 的主機對功能來講是沒有問題的,但萬一對方服務器被這樣弄掛了呢?而且這種狀況下要是使用了dns負載均衡技術,那麼dns的負載均衡將不能被執行到!這顯然不是咱們所但願的。而且經過測試發現,只要是長鏈接的 connection,在代碼中調用各類 close 或者 release 方法都不能把 connection 真正關掉,除非把整個 httpClient.close。

對於這個問題查了一些資料,裏面提到的一個可行的解決辦法,是創建一個監控線程,來專門回收因爲長時間不活動而被斷定爲失效的鏈接。這個監控線程能夠週期性的調用ClientConnectionManager 類的 closeExpiredConnections() 方法來關閉過時的鏈接,回收鏈接池中被關閉的鏈接。它也能夠選擇性的調用 ClientConnectionManager 類的 closeIdleConnections() 方法來關閉一段時間內不活動的鏈接。因爲這個解決方案對於咱們的應用來講太複雜了,因此這個方案的有效性沒有驗證過。

我原先採用的解決方式是:在每次鏈接請求到來的時候都 build 一個新的 HttpClient 對象,而且使用 BasicHttpClientConnectionManager 做爲 connectionManager。而後在處理完 http response 以後 close掉這個 HttpClient。目前本地自測來看,這種作法不會出現上面的奇怪問題。可是很憂傷的是,新建一個 HttpClient 的邏輯很重,而且鏈接不能複用,會浪費不少時間。

因爲這個平常需求自己作的就是優化性質的工做,加上每一個請求都新建 HttpClient 這一大坨代碼,內心老是有點難受。繼續找解決辦法。

在嘗試了改系統的各類 tcp 配置參數還有其餘的 socket、系統配置無果後,最終找到的解決方式卻異常簡單。簡單來講,其實咱們的應用場景下須要的是短鏈接,這樣只要在 request 中添加Connection:close 的頭部,就能夠保證這個連接在此次請求完成以後就被關掉,只用一次。同時發現,若是頭中既有 Connection:Keep-Alive 又有 Connection:close 的話,Connection:close 並不會有更高的優先級,依舊會保持長連。

 

7.總結

使用 HttpClient 的時候特別須要注意的有下面幾個地方:

(1)鏈接池最大鏈接數,不配置,默認爲20

(2)同個 route 的最大鏈接數,不配置,默認爲2

(3)去鏈接池中取鏈接的超時時間,不配置則無限期等待

(4)與目標服務器創建鏈接的超時時間,不配置則無限期等待

(5)去目標服務器取數據的超時時間,不配置則無限期等待

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

(7)同個 host 但 ip 有多個的狀況,請謹慎使用單例的 HttpClient 和鏈接池

(8)HTTP1.1 默認支持的是長鏈接,若是想使用短鏈接,要在 request 上加 Connection:close 的 header,否則長鏈接是不可能自動被關掉的!

必定要結合實際狀況來看是否須要設置,否則可能致使嚴重的問題。

HttpClient 的內容遠不止我上面說到的這些,還包括 Cookie 管理,Fluent API 等內容,因爲沒有實際使用,理解的並不透徹,後續繼續學習後再來補充。

相關文章
相關標籤/搜索