使用Apache的HttpClient發送Http請求
1 基礎概念
1.1 HttpClient、TCP/IP、Socket的區別
HttpClient是Apache中一個開源的項目。它實現了HTTP標準中Client端的所有功能,使用它能夠很容易的進行HTTP信息的傳輸。
網絡從下往上分為:物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層和應用層。
TCP/IP協議:主要解決的是數據傳輸層面的事情,如果沒有規范的應用協議,數據能從網絡里的A節點傳到B節點,但是無法有效識別,建立在TCP/IP上的應用協議很多,比如:rpc、ftp等。所以,無論應用協議多強大,最終都要依靠傳輸層協議進行數據傳輸。
Http、Https:主要解決的是數據規范層面的事情,作用于應用層。而HTTPS是以安全為目標的HTTP通道,簡單講是HTTP的安全版,即HTTP下加入SSL層。
Socket:是tcp/ip的一個編程實現,在程序里,Http請求最終一定要綁定到一個具體的Socket連接進行上行和下行傳輸。Socket本身并不是協議,而是一個調用接口(API)。
1.2 HttpClient的介紹
對于簡單應用,HttpURLConnection完全可以滿足。但是對于
1)系統復雜度高,
2)性能要求高,
3)可靠性要求也高的應用,
則需要一個更強大的組件。HttpClient可以滿足這些條件。
2 HttpClient的使用
2.1 創建一個HttpClient對象
創建HttpClient對象的方式是使用CloseableHttpClient的builder類的HttpClientBuilder,先對一些屬性進行配置(采用裝飾者模式,不斷的setXxx().setXxx() .. 就行),再調用build方法來創建實例。
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、RequestConfig
2.2 創建RequestConfig
RequestConfig是對Request的一些配置,里面比較重要的有三個超時時間,默認情況下這三個時間都是0(如果不設置request的config,會在execute的過程中使用HttpClientParamConfig的getRequestConfig中使用默認參數進行設置,默認設置為-1),這也就意味著無限等待,很容易導致所有的請求阻塞在這個地方無限期等待。
這三個超時時間為:
A :connectionRequestTimeout—從連接池中取連接的超時時間
這個時間定義的是從ConnectionManager管理的連接池中取出連接的超時時間,如果連接池中沒有可用的連接,則request會被阻塞,最長等待connectionRequestTimeout的時間,如果還沒有服務,則拋出ConnectionPoolTimeoutException異常,不繼續等待。
B :connectTimeout—連接超時時間
這個時間定義了通過網絡與服務器建立連接的超時時間,也就是取得了連接池中某個連接之后接通目標url等待時間,會發生超時,拋出ConnectionTimeoutException異常。
C :socketTimeout—請求超時時間
這個時間定義了socket讀數據的超時時間,也就是連接到服務器之后到從服務器獲取響應數據需要等待的時間,或者說是連接上一個url之后到獲取response的返回等待時間。發生超時,會拋出SocketTimeoutException異常。
2.3 創建HttpClientConnectionManager
HttpClientConnectionManager是一個HTTP連接管理器,它負責新的HTTP連接的創建、管理連接的生命周期和保證一個HTTP連接在某一時刻只被一個線程使用。
關鍵的概念——Route
在HttpClient中,一個Route指運行環境機器 --> 目標機器(域名)的一條線路,也就是如果目標url的域名是同一個,那么它們的Route也是一樣的。
HttpClientConnectionManager有兩種具體實現:
A : BasicHttpClientConnectionManager:每次只管理一個connection,但由于它只管理一個連接,所以只能被一個線程使用,它在管理連接的時候如果發現有相同的Route請求,會復用之前已經創建的連接,如果新來的請求不能復用之前的連接,它會關閉現有連接并重新打開它來相應新的請求。
B:PoolingHttpClientConnectionManager:與A不同(這個是默認設置),它管理著一個連接池,可以同時為多個線程服務,每次新來一個請求,如果在連接池中已經存在Route相同并且可用的connection,連接池就會直接復用這個connection;當不存在Route相同的connection,就新建一個connection為之服務;如果連接池已滿,則請求會等待直到被服務或者超時。
整個PoolingHttpClientConnectionManager生命周期從HttpClient初始化開始到整個應用結束調用httpClient.close()。 在PoolingHttpClientConnectionManager的配置中有兩個最大連接數量,分別控制著總的最大連接數量和每個route的最大連接數量。如果沒有顯式設置,默認每個route只允許最多2個connection,總的connection數量不超過20。
2.4 創建一個Request對象以及執行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(url);
執行request請求的方式是:
httpclient.execute(HttpUriRequest request)
重點強調的是:HttpClient允許http連接在特定的Http上下文中執行,HttpContext是跟一個連接相關聯的,所以它也只能屬于一個線程,如果沒有特別設定,在execute的過程中,HttpClient會自動為每一個connection new一個HttpClientHttpContext。
final HttpClientContext localContext
= HttpClientContext.adapt(context != null ? context : new BasicHttpContext);
整個執行execute的過程如下:
3 重點關注
3.1 連接池管理
連接池的獲取的完整邏輯是:在每收到一個route請求后,連接池都會建立一個以這個route為key的子連接池,當有一個新的連接請求到來的時候,它會優先匹配已經存在的子連接池們,如果之前已經有過以這個route為key的子連接池,那么就會去試圖取這個子連接池中狀態為available的連接,如果此時有可用的連接,則將取得的available連接狀態改為leased的,取連接成功。 如果此時子連接池沒有可用連接,那再看是否達到了所設置的最大連接數和每個route所允許的最大連接數的上限,如果還有余量則new一個新的連接,或者取得lastUsedConnection,關閉這個連接、把連接從原來所在的子連接池刪除,再lease取連接成功。 如果此時的情況不允許再new一個新的連接,就把這個請求連接的請求放入一個queue中排隊等待,直到得到一個連接或者超時才會從queue中刪去。
3.2 連接池回收
目前連接池的回收機制有二種:
第一種設置 HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); httpClientBuilder.evictExpiredConnections();//制定過期連接,httpClientBuilder.evictIdleConnections(60, TimeUnit.SECONDS); 用來關閉閑置連接,它會啟動一個守護線程進行清理工作。用戶可以通過builder#evictIdleConnections開啟該組件,并通過builder#setmaxIdleTime設置最大空閑時間。
第二種設置:初始化PoolingHttpClientConnectionManager設置每個連接的生命周期 PoolingHttpClientConnectionManager connManager =new PoolingHttpClientConnectionManager(socketFactoryRegistry, null, null, null, 360, TimeUnit.SECONDS);
3.3 keep-alive
在HttpClient.execute得到response之后的相關代碼中,它會先取出response的keep-alive頭來設置connection是否resuable以及存活的時間。如果服務器返回的響應中包含了Connection:Keep-Alive(默認有的),但沒有包含Keep-Alive時長的頭消息,HttpClient認為這個連接可以永遠保持,也就是長連接。
3.4 常見問題
有時會出現線程阻塞,經過上面的介紹,主要原因是:
一:沒有增加連接池的回收機制,造成長連接一直保持。
二:未設置connectionRequestTimeout,造成請求在連接池中一直等待,未在服務器中拋出異常。
4 小結
本篇文章簡單介紹了發送Http請求——Apache的HttpClient,由于純手打,難免會有紕漏,如果發現錯誤的地方,請第一時間告訴我,這將是我進步的一個很重要的環節。