一、使用方法
使用HttpClient發送請求、接收響應很簡單,一般需要如下幾步即可。
1. 創建HttpClient對象。
2. 創建請求方法的實例,並指定請求URL。如果需要發送GET請求,創建HttpGet對象;如果需要發送POST請求,創建HttpPost對象。
3. 如果需要發送請求參數,可調用HttpGet、HttpPost共同的setParams(HetpParams params)方法來添加請求參數;對於HttpPost對象而言,也可調用setEntity(HttpEntity entity)方法來設置請求參數。
4. 調用HttpClient對象的execute(HttpUriRequest request)發送請求,該方法返回一個HttpResponse。
5. 調用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可獲取服務器的響應頭;調用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務器的響應內容。程序可通過該對象獲取服務器的響應內容。
6. 釋放連接。無論執行方法是否成功,都必須釋放連接
-
try {
-
// 創建一個默認的HttpClient
-
HttpClient httpclient =
new DefaultHttpClient();
-
// 創建一個GET請求
-
HttpGet request =
new HttpGet(
"www.google.com");
-
// 發送GET請求,並將響應內容轉換成字符串
-
String response = httpclient.execute(request,
new BasicResponseHandler());
-
Log.v(
"response text", response);
-
}
catch (ClientProtocolException e) {
-
e.printStackTrace();
-
}
catch (IOException e) {
-
e.printStackTrace();
-
}
二、多線程的HttpClient
在實際項目中,我們很可能在多處需要進行HTTP通信,這時候我們不需要為每個請求都創建一個新的HttpClient。現在我們的應用程序使用同一個HttpClient來管理所有的Http請求,一旦出現並發請求,那么一定會出現多線程的問題。這就好像我們的瀏覽器只有一個標簽頁卻有多個用戶,A要上google,B要上baidu,這時瀏覽器就會忙不過來了。幸運的是,HttpClient提供了創建線程安全對象的API
-
public
class CustomerHttpClient {
-
private
static
final String CHARSET = HTTP.UTF_8;
-
/**
-
* 最大連接數
-
*/
-
public
final
static
int MAX_TOTAL_CONNECTIONS =
800;
-
/**
-
* 獲取連接的最大等待時間
-
*/
-
public
final
static
int WAIT_TIMEOUT =
60000;
-
/**
-
* 每個路由最大連接數
-
*/
-
public
final
static
int MAX_ROUTE_CONNECTIONS =
400;
-
/**
-
* 連接超時時間
-
*/
-
public
final
static
int CONNECT_TIMEOUT =
10000;
-
/**
-
* 讀取超時時間
-
*/
-
public
final
static
int READ_TIMEOUT =
10000;
-
-
-
private
static HttpClient customerHttpClient;
-
-
private CustomerHttpClient() {
-
}
-
-
public static synchronized HttpClient getHttpClient() {
-
if (
null == customerHttpClient) {
-
HttpParams params =
new BasicHttpParams();
-
// 設置一些基本參數
-
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
-
HttpProtocolParams.setContentCharset(params,
-
CHARSET);
-
HttpProtocolParams.setUseExpectContinue(params,
true);
-
HttpProtocolParams
-
.setUserAgent(
-
params,
-
"Mozilla/5.0(Linux;U;Android 2.2.1;en-us;Nexus One Build.FRG83) "
-
+
"AppleWebKit/553.1(KHTML,like Gecko) Version/4.0 Mobile Safari/533.1");
-
// 超時設置
-
/* 從連接池中取連接的超時時間 */
-
ConnManagerParams.setTimeout(params, WAIT_TIMEOUT);
-
/* 連接超時 */
-
HttpConnectionParams.setConnectionTimeout(params, CONNECT_TIMEOUT);
-
/* 請求超時 */
-
HttpConnectionParams.setSoTimeout(params, READ_TIMEOUT);
-
-
-
// 設置最大連接數
-
ConnManagerParams.setMaxTotalConnections(params, MAX_TOTAL_CONNECTIONS);
-
// 設置每個路由最大連接數
-
ConnPerRouteBean connPerRoute =
new ConnPerRouteBean(MAX_ROUTE_CONNECTIONS);
-
ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute);
-
-
// 設置我們的HttpClient支持HTTP和HTTPS兩種模式
-
SchemeRegistry schReg =
new SchemeRegistry();
-
schReg.register(
new Scheme(
"http", PlainSocketFactory
-
.getSocketFactory(),
80));
-
schReg.register(
new Scheme(
"https", SSLSocketFactory
-
.getSocketFactory(),
443));
-
-
// 使用線程安全的連接管理來創建HttpClient
-
ClientConnectionManager conMgr =
new ThreadSafeClientConnManager(
-
params, schReg);
-
customerHttpClient =
new DefaultHttpClient(conMgr, params);
-
}
-
return customerHttpClient;
-
}
-
}
1、超時配置
上面的代碼提到了3種超時設置,比較容易搞混,HttpClient的3種超時說明
-
/* 從連接池中取連接的超時時間 */
-
ConnManagerParams.setTimeout(params,
1000);
-
/* 連接超時 */
-
HttpConnectionParams.setConnectionTimeout(params,
2000);
-
/* 請求超時 */
-
HttpConnectionParams.setSoTimeout(params,
4000);
第一行設置ConnectionPoolTimeout:這定義了從ConnectionManager管理的連接池中取出連接的超時時間,此處設置為1秒。
第二行設置ConnectionTimeout: 這定義了通過網絡與服務器建立連接的超時時間。Httpclient包中通過一個異步線程去創建與服務器的socket連接,這就是該socket連接的超時時間,此處設置為2秒。
第三行設置SocketTimeout: 這定義了Socket讀數據的超時時間,即從服務器獲取響應數據需要等待的時間,此處設置為4秒。
以上3種超時分別會拋出ConnectionPoolTimeoutException,ConnectionTimeoutException與SocketTimeoutException。
2、線程池配置
ThreadSafeClientConnManager默認使用了連接池
-
//設置最大連接數
-
ConnManagerParams.setMaxTotalConnections(httpParams,
10);
-
//設置最大路由連接數
-
ConnPerRouteBean connPerRoute =
new ConnPerRouteBean(
10);
-
ConnManagerParams.setMaxConnectionsPerRoute(httpParams, connPerRoute);
比較特別的是 每個路由(route)最大連接數。什么是一個route?這里route的概念可以理解為運行環境機器到目標機器的一條線路。舉例來說,我們使用HttpClient的實現來分別請求 www.baidu.com 的資源和 www.bing.com 的資源那么他就會產生兩個route。這里為什么要特別提到route最大連接數這個參數呢,因為這個參數的默認值為2,如果不設置這個參數值默認情況下對於同一個目標機器的最大並發連接只有2個!這意味着如果你正在執行一個針對某一台目標機器的抓取任務的時候,哪怕你設置連接池的最大連接數為200,但是實際上還是只有2個連接在工作,其他剩余的198個連接都在等待,都是為別的目標機器服務的。
3、工具類
有了單例的HttpClient對象,我們就可以把一些常用的發出GET和POST請求的代碼也封裝起來,寫進我們的工具類中了。POST請求示例:
-
private
static
final String TAG =
"CustomerHttpClient";
-
-
public static String post(String url, NameValuePair... params) {
-
try {
-
// 編碼參數
-
List<NameValuePair> formparams =
new ArrayList<NameValuePair>();
// 請求參數
-
for (NameValuePair p : params) {
-
formparams.add(p);
-
}
-
UrlEncodedFormEntity entity =
new UrlEncodedFormEntity(formparams,
-
HTTP.UTF_8);
-
// 創建POST請求
-
HttpPost request =
new HttpPost(url);
-
request.setEntity(entity);
-
// 發送請求
-
HttpClient client = getHttpClient();
-
HttpResponse response = client.execute(request);
-
if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
-
throw
new RuntimeException(
"請求失敗");
-
}
-
HttpEntity resEntity = response.getEntity();
-
return (resEntity ==
null) ?
null : EntityUtils.toString(resEntity, CHARSET);
-
}
catch (UnsupportedEncodingException e) {
-
Log.w(TAG, e.getMessage());
-
return
null;
-
}
catch (ClientProtocolException e) {
-
Log.w(TAG, e.getMessage());
-
return
null;
-
}
catch (IOException e) {
-
throw
new RuntimeException(
"連接失敗", e);
-
}
-
-
}
4、線程池技術
4.1 長連接和短連接
所謂長連接是指客戶端與服務器端一旦建立連接以后,可以進行多次數據傳輸而不需重新建立連接,而短連接則每次數據傳輸都需要客戶端和服務器端建立一次連接。
長連接的優勢在於省去了每次數據傳輸連接建立的時間開銷,能夠大幅度提高數據傳輸的速度,對於P2P應用十分適合。
短連接每次數據傳輸都需要建立連接,我們知道HTTP協議的傳輸層協議是TCP協議,TCP連接的建立和釋放分別需要進行3次握手和4次握手,頻繁的建立連接即增加了時間開銷,同時頻繁的創建和銷毀Socket同樣是對服務器端資源的浪費。
對於諸如Web網站之類的B2C應用,並發請求量大,每一個用戶又不需頻繁的操作的場景下,維護大量的長連接對服務器無疑是一個巨大的考驗。而此時,短連接可能更加適用。
而對於需要頻繁發送HTTP請求的應用,需要在客戶端使用HTTP長連接。
4.2、連接池
連接池管理的對象是長連接。連接池技術作為創建和管理連接的緩沖池技術,目前已廣泛用於諸如數據庫連接等長連接的維護和管理中,能夠有效減少系統的響應時間,節省服務器資源開銷。其優勢主要有兩個:其一是減少創建連接的資源開銷,其二是資源的訪問控制。
HTTP連接是無狀態的,這樣很容易給我們造成HTTP連接是短連接的錯覺,實際上HTTP1.1默認即是持久連接,HTTP1.0也可以通過在請求頭中設置Connection:keep-alive使得連接為長連接。
4.3、HttpConnection
沒有連接池的概念,多少次請求就會建立多少個IO,在訪問量巨大的情況下服務器的IO可能會耗盡。
4.4、HttpClient3
也有連接池的東西在里頭,使用MultiThreadedHttpConnectionManager,大致過程如下:
-
MultiThreadedHttpConnectionManager connectionManager =
new MultiThreadedHttpConnectionManager();
-
HttpClient client =
new HttpClient(connectionManager);...
// 在某個線程中。
-
GetMethod get =
new GetMethod(
"http://jakarta.apache.org/");
-
try {
-
client.executeMethod(get);
// print response to stdout
-
System.out.println(get.getResponseBodyAsStream());
-
}
finally {
-
// be sure the connection is released back to the connection
-
managerget.releaseConnection();
-
}
可以看出來,它的方式與jdbc連接池的使用方式相近,比較不爽的就是需要手動調用releaseConnection去釋放連接。對每一個HttpClient.executeMethod須有一個method.releaseConnection()與之匹配。
4.5、HttpClient4
HTTP Client4.0的ThreadSafeClientConnManager實現了HTTP連接的池化管理,其管理連接的基本單位是Route(路由),每個路由上都會維護一定數量的HTTP連接。這里的Route的概念可以理解為客戶端機器到目標機器的一條線路,例如使用HttpClient的實現來分別請求 www.163.com 的資源和 www.sina.com 的資源就會產生兩個route。缺省條件下對於每個Route,HttpClient僅維護2個連接,總數不超過20個連接,顯然對於大多數應用來講,都是不夠用的,可以通過設置HTTP參數進行調整。
-
HttpParams params =
new BasicHttpParams();
-
//將每個路由的最大連接數增加到200
-
ConnManagerParams.setMaxTotalConnections(params,
200);
-
// 將每個路由的默認連接數設置為20
-
ConnPerRouteBean connPerRoute =
new ConnPerRouteBean(
20);
-
// 設置某一個IP的最大連接數
-
HttpHost localhost =
new HttpHost(
"locahost",
80);
-
connPerRoute.setMaxForRoute(
new HttpRoute(localhost),
50);
-
ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute);
-
SchemeRegistry schemeRegistry =
new SchemeRegistry();
-
schemeRegistry.register(
-
new Scheme(
"http", PlainSocketFactory.getSocketFactory(),
80));
-
schemeRegistry.register(
-
new Scheme(
"https", SSLSocketFactory.getSocketFactory(),
443));
-
ClientConnectionManager cm =
new ThreadSafeClientConnManager(params, schemeRegistry);
-
HttpClient httpClient =
new DefaultHttpClient(cm, params);
可以配置的HTTP參數有:
1) http.conn-manager.timeout 當某一線程向連接池請求分配線程時,如果連接池已經沒有可以分配的連接時,該線程將會被阻塞,直至http.conn-manager.timeout超時,拋出ConnectionPoolTimeoutException。
2) http.conn-manager.max-per-route 每個路由的最大連接數;
3) http.conn-manager.max-total 總的連接數;
4.6、過期長連接
連接的有效性檢測是所有連接池都面臨的一個通用問題,大部分HTTP服務器為了控制資源開銷,並不會永久的維護一個長連接,而是一段時間就會關閉該連接。放回連接池的連接,如果在服務器端已經關閉,客戶端是無法檢測到這個狀態變化而及時的關閉Socket的。這就造成了線程從連接池中獲取的連接不一定是有效的。這個問題的一個解決方法就是在每次請求之前檢查該連接是否已經存在了過長時間,可能已過期。但是這個方法會使得每次請求都增加額外的開銷。HTTP Client4.0的ThreadSafeClientConnManager 提供了closeExpiredConnections()方法和closeIdleConnections()方法來解決該問題。前一個方法是清除連接池中所有過期的連接,至於連接什么時候過期可以設置,設置方法將在下面提到,而后一個方法則是關閉一定時間空閑的連接,可以使用一個單獨的線程完成這個工作。
-
public
static
class IdleConnectionMonitorThread extends Thread {
-
private
final ClientConnectionManager connMgr;
-
private
volatile
boolean shutdown;
-
public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
-
super();
-
this.connMgr = connMgr;
-
}
-
@Override
-
public void run() {
-
try {
-
while (!shutdown) {
-
synchronized (
this) {
-
wait(
5000);
-
// 關閉過期的連接
-
connMgr.closeExpiredConnections();
-
// 關閉空閑時間超過30秒的連接
-
connMgr.closeIdleConnections(
30, TimeUnit.SECONDS);
-
}
-
}
-
}
catch (InterruptedException ex) {
-
// terminate
-
}
-
}
-
public void shutdown() {
-
shutdown =
true;
-
synchronized (
this) {
-
notifyAll();
-
}
剛才提到,客戶端可以設置連接的過期時間,可以通過HttpClient的setKeepAliveStrategy方法設置連接的過期時間,這樣就可以配合closeExpiredConnections()方法解決連接池中連接失效的。
-
DefaultHttpClient httpclient =
new DefaultHttpClient();
-
httpclient.setKeepAliveStrategy(
new ConnectionKeepAliveStrategy() {
-
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
-
// Honor 'keep-alive' header
-
HeaderElementIterator it =
new BasicHeaderElementIterator(
-
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
-
while (it.hasNext()) {
-
HeaderElement he = it.nextElement();
-
String param = he.getName();
-
String value = he.getValue();
-
if (value !=
null && param.equalsIgnoreCase(
"timeout")) {
-
try {
-
return Long.parseLong(value) *
1000;
-
}
catch(NumberFormatException ignore) {
-
}
-
}
-
}
-
HttpHost target = (HttpHost) context.getAttribute(
-
ExecutionContext.HTTP_TARGET_HOST);
-
if (
"www.163.com".equalsIgnoreCase(target.getHostName())) {
-
// 對於163這個路由的連接,保持5秒
-
return
5 *
1000;
-
}
else {
-
// 其他路由保持30秒
-
return
30 *
1000;
-
}
-
}
-
})