HttpClient 教程

httpClient 教程

 
前言

超文本傳輸協議(HTTP)也許是當今互聯網上使用的最重要的協議了。Web服務,有網絡功能的設備和網絡計算的發展,都持續擴展了HTTP協議的角色,超越了用戶使用的Web瀏覽器範疇,同時,也增加了需要HTTP協議支持的應用程序的數量。

儘管java.net包提供了基本通過HTTP訪問資源的功能,但它沒有提供全面的靈活性和其它很多應用程序需要的功能。HttpClient就是尋求彌補這項空白的組件,通過提供一個有效的,保持更新的,功能豐富的軟件包來實現客戶端最新的HTTP標準和建議。

爲擴展而設計,同時爲基本的HTTP協議提供強大的支持,HttpClient組件也許就是構建HTTP客戶端應用程序,比如web瀏覽器,web服務端,利用或擴展HTTP協議進行分佈式通信的系統的開發人員的關注點。

1. HttpClient的範圍

  • 基於HttpCore[http://hc.apache.org/httpcomponents-core/index.html]的客戶端HTTP運輸實現庫
  • 基於經典(阻塞)I/O
  • 內容無關

2. 什麼是HttpClient不能做的

  • HttpClient不是一個瀏覽器。它是一個客戶端的HTTP通信實現庫。HttpClient的目標是發送和接收HTTP報文。HttpClient不會去緩存內容,執行嵌入在HTML頁面中的javascript代碼,猜測內容類型,重新格式化請求/重定向URI,或者其它和HTTP運輸無關的功能。

第一章 基礎

1.1 執行請求

HttpClient最重要的功能是執行HTTP方法。一個HTTP方法的執行包含一個或多個HTTP請求/HTTP響應交換,通常由HttpClient的內部來處理。而期望用戶提供一個要執行的請求對象,而HttpClient期望傳輸請求到目標服務器並返回對應的響應對象,或者當執行不成功時拋出異常。

很自然地,HttpClient API的主要切入點就是定義描述上述規約的HttpClient接口。

這裏有一個很簡單的請求執行過程的示例:

HttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
int l;
byte[] tmp = new byte[2048];
while ((l = instream.read(tmp)) != -1) {
}
}

1.1.1 HTTP請求

所有HTTP請求有一個組合了方法名,請求URI和HTTP協議版本的請求行。

HttpClient支持所有定義在HTTP/1.1版本中的HTTP方法:GET,HEAD,POST,PUT,DELETE,TRACE和OPTIONS。對於每個方法類型都有一個特殊的類:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace和HttpOptions。

請求的URI是統一資源定位符,它標識了應用於哪個請求之上的資源。HTTP請求URI包含一個協議模式,主機名稱,可選的端口,資源路徑,可選的查詢和可選的片段。

HttpGet httpget = new HttpGet(
"http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient提供很多工具方法來簡化創建和修改執行URI。
URI也可以編程來拼裝:
URI uri = URIUtils.createURI("http", "www.google.com", -1, "/search",
"q=httpclient&btnG=Google+Search&aq=f&oq=", null);
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());

輸出內容爲:

http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=

查詢字符串也可以從獨立的參數中來生成:

List<NameValuePair> qparams = new ArrayList<NameValuePair>();
qparams.add(new BasicNameValuePair("q", "httpclient"));
qparams.add(new BasicNameValuePair("btnG", "Google Search"));
qparams.add(new BasicNameValuePair("aq", "f"));
qparams.add(new BasicNameValuePair("oq", null));
URI uri = URIUtils.createURI("http", "www.google.com", -1, "/search",
URLEncodedUtils.format(qparams, "UTF-8"), null);
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());

輸出內容爲:

http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=

1.1.2 HTTP響應

HTTP響應是由服務器在接收和解釋請求報文之後返回發送給客戶端的報文。響應報文的第一行包含了協議版本,之後是數字狀態碼和相關聯的文本段。

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_OK, "OK");
System.out.println(response.getProtocolVersion());
System.out.println(response.getStatusLine().getStatusCode());
System.out.println(response.getStatusLine().getReasonPhrase());
System.out.println(response.getStatusLine().toString());

輸出內容爲:

HTTP/1.1
200
OK
HTTP/1.1 200 OK

1.1.3 處理報文頭部

一個HTTP報文可以包含很多描述如內容長度,內容類型等信息屬性的頭部信息。

HttpClient提供獲取,添加,移除和枚舉頭部信息的方法。

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie",
"c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie",
"c2=b; path=\"/\", c3=c; domain=\"localhost\"");
Header h1 = response.getFirstHeader("Set-Cookie");
System.out.println(h1);
Header h2 = response.getLastHeader("Set-Cookie");
System.out.println(h2);
Header[] hs = response.getHeaders("Set-Cookie");
System.out.println(hs.length);

輸出內容爲:

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"

獲得給定類型的所有頭部信息最有效的方式是使用HeaderIterator接口。

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie",
"c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie",
"c2=b; path=\"/\", c3=c; domain=\"localhost\"");
HeaderIterator it = response.headerIterator("Set-Cookie");
while (it.hasNext()) {
System.out.println(it.next());
}

輸出內容爲:

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"

它也提供解析HTTP報文到獨立頭部信息元素的方法方法。

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie",
"c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie",
"c2=b; path=\"/\", c3=c; domain=\"localhost\"");
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator("Set-Cookie"));
while (it.hasNext()) {
HeaderElement elem = it.nextElement();
System.out.println(elem.getName() + " = " + elem.getValue());
NameValuePair[] params = elem.getParameters();
for (int i = 0; i < params.length; i++) {
System.out.println(" " + params[i]);
}
}

輸出內容爲:

c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost

1.1.4 HTTP實體

HTTP報文可以攜帶和請求或響應相關的內容實體。實體可以在一些請求和響應中找到,因爲它們也是可選的。使用了實體的請求被稱爲封閉實體請求。HTTP規範定義了兩種封閉實體的方法:POST和PUT。響應通常期望包含一個內容實體。這個規則也有特例,比如HEAD方法的響應和204 No Content,304 Not Modified和205 Reset Content響應。

HttpClient根據其內容出自何處區分三種類型的實體:

  • streamed流式:內容從流中獲得,或者在運行中產生。特別是這種分類包含從HTTP響應中獲取的實體。流式實體是不可重複生成的。
  • self-contained自我包含式:內容在內存中或通過獨立的連接或其它實體中獲得。自我包含式的實體是可以重複生成的。這種類型的實體會經常用於封閉HTTP請求的實體。
  • wrapping包裝式:內容從另外一個實體中獲得。

當從一個HTTP響應中獲取流式內容時,這個區別對於連接管理很重要。對於由應用程序創建而且只使用HttpClient發送的請求實體,流式和自我包含式的不同就不那麼重要了。這種情況下,建議考慮如流式這種不能重複的實體,和可以重複的自我包含式實體。

1.1.4.1 重複實體

實體可以重複,意味着它的內容可以被多次讀取。這就僅僅是自我包含式的實體了(像ByteArrayEntity或StringEntity)。

1.1.4.2 使用HTTP實體

因爲一個實體既可以代表二進制內容又可以代表字符內容,它也支持字符編碼(支持後者也就是字符內容)。

實體是當使用封閉內容執行請求,或當請求已經成功執行,或當響應體結果發功到客戶端時創建的。

要從實體中讀取內容,可以通過HttpEntity#getContent()方法從輸入流中獲取,這會返回一個java.io.InputStream對象,或者提供一個輸出流到HttpEntity#writeTo(OutputStream)方法中,這會一次返回所有寫入到給定流中的內容。

當實體通過一個收到的報文獲取時,HttpEntity#getContentType()方法和HttpEntity#getContentLength()方法可以用來讀取通用的元數據,如Content-Type和Content-Length頭部信息(如果它們是可用的)。因爲頭部信息Content-Type可以包含對文本MIME類型的字符編碼,比如text/plain或text/html,HttpEntity#getContentEncoding()方法用來讀取這個信息。如果頭部信息不可用,那麼就返回長度-1,而對於內容類型返回NULL。如果頭部信息Content-Type是可用的,那麼就會返回一個Header對象。

當爲一個傳出報文創建實體時,這個元數據不得不通過實體創建器來提供。

StringEntity myEntity = new StringEntity("important message",
"UTF-8");
System.out.println(myEntity.getContentType());
System.out.println(myEntity.getContentLength());
System.out.println(EntityUtils.getContentCharSet(myEntity));
System.out.println(EntityUtils.toString(myEntity));
System.out.println(EntityUtils.toByteArray(myEntity).length);

輸出內容爲

Content-Type: text/plain; charset=UTF-8
17
UTF-8
important message
17

1.1.5 確保低級別資源釋放

當完成一個響應實體,那麼保證所有實體內容已經被完全消耗是很重要的,所以連接可以安全的放回到連接池中,而且可以通過連接管理器對後續的請求重用連接。處理這個操作的最方便的方法是調用HttpEntity#consumeContent()方法來消耗流中的任意可用內容。HttpClient探測到內容流尾部已經到達後,會立即會自動釋放低層連接,並放回到連接管理器。HttpEntity#consumeContent()方法調用多次也是安全的。

也可能會有特殊情況,當整個響應內容的一小部分需要獲取,消耗剩餘內容而損失性能,還有重用連接的代價太高,則可以僅僅通過調用HttpUriRequest#abort()方法來中止請求。

HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
int byteOne = instream.read();
int byteTwo = instream.read();
// Do not need the rest
httpget.abort();
}

連接不會被重用,但是由它持有的所有級別的資源將會被正確釋放。

1.1.6 消耗實體內容

推薦消耗實體內容的方式是使用它的HttpEntity#getContent()或HttpEntity#writeTo(OutputStream)方法。HttpClient也自帶EntityUtils類,這會暴露出一些靜態方法,這些方法可以更加容易地從實體中讀取內容或信息。代替直接讀取java.io.InputStream,也可以使用這個類中的方法以字符串/字節數組的形式獲取整個內容體。然而,EntityUtils的使用是強烈不鼓勵的,除非響應實體源自可靠的HTTP服務器和已知的長度限制。

HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
long len = entity.getContentLength();
if (len != -1 && len < 2048) {
System.out.println(EntityUtils.toString(entity));
} else {
// Stream content out
}
}

在一些情況下可能會不止一次的讀取實體。此時實體內容必須以某種方式在內存或磁盤上被緩衝起來。最簡單的方法是通過使用BufferedHttpEntity類來包裝源實體完成。這會引起源實體內容被讀取到內存的緩衝區中。在其它所有方式中,實體包裝器將會得到源實體。

HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}

1.1.7 生成實體內容

HttpClient提供一些類,它們可以用於生成通過HTTP連接獲得內容的有效輸出流。爲了封閉實體從HTTP請求中獲得的輸出內容,那些類的實例可以和封閉如POST和PUT請求的實體相關聯。HttpClient爲很多公用的數據容器,比如字符串,字節數組,輸入流和文件提供了一些類:StringEntity,ByteArrayEntity,InputStreamEntity和FileEntity。

File file = new File("somefile.txt");
FileEntity entity = new FileEntity(file, "text/plain; charset=\"UTF-8\"");
HttpPost httppost = new HttpPost("http://localhost/action.do");
httppost.setEntity(entity);

請注意InputStreamEntity是不可重複的,因爲它僅僅能從低層數據流中讀取一次內容。通常來說,我們推薦實現一個定製的HttpEntity類,這是自我包含式的,用來代替使用通用的InputStreamEntity。FileEntity也是一個很好的起點。

1.1.7.1 動態內容實體

通常來說,HTTP實體需要基於特定的執行上下文來動態地生成。通過使用EntityTemplate實體類和ContentProducer接口,HttpClient提供了動態實體的支持。內容生成器是按照需求生成它們內容的對象,將它們寫入到一個輸出流中。它們是每次被請求時來生成內容。所以用EntityTemplate創建的實體通常是自我包含而且可以重複的。

ContentProducer cp = new ContentProducer() {
public void writeTo(OutputStream outstream) throws IOException {
Writer writer = new OutputStreamWriter(outstream, "UTF-8");
writer.write("<response>");
writer.write(" <content>");
writer.write(" important stuff");
writer.write(" </content>");
writer.write("</response>");
writer.flush();
}
};
HttpEntity entity = new EntityTemplate(cp);
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);
1.1.7.2 HTML表單

許多應用程序需要頻繁模擬提交一個HTML表單的過程,比如,爲了來記錄一個Web應用程序或提交輸出數據。HttpClient提供了特殊的實體類UrlEncodedFormEntity來這個滿足過程。

List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("param1", "value1"));
formparams.add(new BasicNameValuePair("param2", "value2"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8");
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);

UrlEncodedFormEntity實例將會使用URL編碼來編碼參數,生成如下的內容:

param1=value1&param2=value2

1.1.7.3 內容分塊

通常,我們推薦讓HttpClient選擇基於被傳遞的HTTP報文屬性的最適合的編碼轉換。這是可能的,但是,設置HttpEntity#setChunked()方法爲true是通知HttpClient分塊編碼的首選。請注意HttpClient將會使用標識作爲提示。當使用的HTTP協議版本,如HTTP/1.0版本,不支持分塊編碼時,這個值會被忽略。

StringEntity entity = new StringEntity("important message",
"text/plain; charset=\"UTF-8\"");
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);

1.1.8 響應控制器

控制響應的最簡便和最方便的方式是使用ResponseHandler接口。這個放完完全減輕了用戶關於連接管理的擔心。當使用ResponseHandler時,HttpClient將會自動關注並保證釋放連接到連接管理器中去,而不管請求執行是否成功或引發了異常。

HttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://localhost/");
ResponseHandler<byte[]> handler = new ResponseHandler<byte[]>() {
public byte[] handleResponse(
HttpResponse response) throws ClientProtocolException, IOException {
HttpEntity entity = response.getEntity();
if (entity != null) {
return EntityUtils.toByteArray(entity);
} else {
return null;
}
}
};
byte[] response = httpclient.execute(httpget, handler);

1.2 HTTP執行的環境

最初,HTTP是被設計成無狀態的,面向請求-響應的協議。然而,真實的應用程序經常需要通過一些邏輯相關的請求-響應交換來持久狀態信息。爲了開啓應用程序來維持一個過程狀態,HttpClient允許HTTP請求在一個特定的執行環境中來執行,簡稱爲HTTP上下文。如果相同的環境在連續請求之間重用,那麼多種邏輯相關的請求可以參與到一個邏輯會話中。HTTP上下文功能和java.util.Map<String,Object>很相似。它僅僅是任意命名參數值的集合。應用程序可以在請求之前或在檢查上下文執行完成之後來填充上下文屬性。

在HTTP請求執行的這一過程中,HttpClient添加了下列屬性到執行上下文中:

  • 'http.connection':HttpConnection實例代表了連接到目標服務器的真實連接。
  • 'http.target_host':HttpHost實例代表了連接目標。
  • 'http.proxy_host':如果使用了,HttpHost實例代表了代理連接。
  • 'http.request':HttpRequest實例代表了真實的HTTP請求。
  • 'http.response':HttpResponse實例代表了真實的HTTP響應。
  • 'http.request_sent':java.lang.Boolean對象代表了暗示真實請求是否被完全傳送到目標連接的標識。

比如,爲了決定最終的重定向目標,在請求執行之後,可以檢查http.target_host屬性的值:

DefaultHttpClient httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
HttpGet httpget = new HttpGet("http://www.google.com/");
HttpResponse response = httpclient.execute(httpget, localContext);
HttpHost target = (HttpHost) localContext.getAttribute(
ExecutionContext.HTTP_TARGET_HOST);
System.out.println("Final target: " + target);
HttpEntity entity = response.getEntity();
if (entity != null) {
entity.consumeContent();
}

輸出內容爲:

Final target: http://www.google.ch

1.3 異常處理

HttpClient能夠拋出兩種類型的異常:在I/O失敗時,如套接字連接超時或被重置的java.io.IOException異常,還有標誌HTTP請求失敗的信號,如違反HTTP協議的HttpException異常。通常I/O錯誤被認爲是非致命的和可以恢復的,而HTTP協議錯誤則被認爲是致命的而且是不能自動恢復的。

1.3.1 HTTP運輸安全

要理解HTTP協議並不是對所有類型的應用程序都適合的,這一點很重要。HTTP是一個簡單的面向請求/響應的協議,最初被設計用來支持取回靜態或動態生成的內容。它從未向支持事務性操作方向發展。比如,如果成功收到和處理請求,HTTP服務器將會考慮它的其中一部分是否完成,生成一個響應併發送一個狀態碼到客戶端。如果客戶端因爲讀取超時,請求取消或系統崩潰導致接收響應實體失敗時,服務器不會試圖回滾事務。如果客戶端決定重新這個請求,那麼服務器將不可避免地不止一次執行這個相同的事務。在一些情況下,這會導致應用數據損壞或者不一致的應用程序狀態。

儘管HTTP從來都沒有被設計來支持事務性處理,但它也能被用作於一個傳輸協議對關鍵的任務應用提供被滿足的確定狀態。要保證HTTP傳輸層的安全,系統必須保證HTTP方法在應用層的冪等性。

1.3.2 冪等的方法

HTTP/1.1 明確地定義了冪等的方法,描述如下

[方法也可以有“冪等”屬性在那些(除了錯誤或過期問題)N的副作用>0的相同請求和獨立的請求是相同的]

換句話說,應用程序應該保證準備着來處理多個相同方法執行的實現。這是可以達到的,比如,通過提供一個獨立的事務ID和其它避免執行相同邏輯操作的方法。

請注意這個問題對於HttpClient是不具體的。基於應用的瀏覽器特別受和非冪等的HTTP方法相關的相同問題的限制。

HttpClient假設沒有實體包含方法,比如GET和HEAD是冪等的,而實體包含方法,比如POST和PUT則不是。

1.3.3 異常自動恢復

默認情況下,HttpClient會試圖自動從I/O異常中恢復。默認的自動恢復機制是受很少一部分已知的異常是安全的這個限制。

  • HttpClient不會從任意邏輯或HTTP協議錯誤(那些是從HttpException類中派生出的)中恢復的。
  • HttpClient將會自動重新執行那麼假設是冪等的方法。
  • HttpClient將會自動重新執行那些由於運輸異常失敗,而HTTP請求仍然被傳送到目標服務器(也就是請求沒有完全被送到服務器)失敗的方法。
  • HttpClient將會自動重新執行那些已經完全被送到服務器,但是服務器使用HTTP狀態碼(服務器僅僅丟掉連接而不會發回任何東西)響應時失敗的方法。在這種情況下,假設請求沒有被服務器處理,而應用程序的狀態也沒有改變。如果這個假設可能對於你應用程序的目標Web服務器來說不正確,那麼就強烈建議提供一個自定義的異常處理器。

1.3.4 請求重試處理

爲了開啓自定義異常恢復機制,應該提供一個HttpRequestRetryHandler接口的實現。

DefaultHttpClient httpclient = new DefaultHttpClient();
HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
public boolean retryRequest(IOException exception,
int executionCount,HttpContext context) {
if (executionCount >= 5) {
// 如果超過最大重試次數,那麼就不要繼續了
return false;
}
if (exception instanceof NoHttpResponseException) {
// 如果服務器丟掉了連接,那麼就重試
return true;
}
if (exception instanceof SSLHandshakeException) {
// 不要重試SSL握手異常
return false;
}
HttpRequest request = (HttpRequest) context.getAttribute(
ExecutionContext.HTTP_REQUEST);
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
// 如果請求被認爲是冪等的,那麼就重試
return true;
}
return false;
}
};
httpclient.setHttpRequestRetryHandler(myRetryHandler);

1.4 中止請求

在一些情況下,由於目標服務器的高負載或客戶端有很多活動的請求,那麼HTTP請求執行會在預期的時間框內而失敗。這時,就可能不得不過早地中止請求,解除封鎖在I/O執行中的線程封鎖。被HttpClient執行的HTTP請求可以在執行的任意階段通過調用HttpUriRequest#abort()方法而中止。這個方法是線程安全的,而且可以從任意線程中調用。當一個HTTP請求被中止時,它的執行線程就封鎖在I/O操作中了,而且保證通過拋出InterruptedIOException異常來解鎖。

1.5 HTTP協議攔截器

HTTP協議攔截器是一個實現了特定HTPP協議方面的慣例。通常協議攔截器希望作用於一個特定頭部信息上,或者一族收到報文的相關頭部信息,或使用一個特定的頭部或一族相關的頭部信息填充發出的報文。協議攔截器也可以操縱包含在報文中的內容實體,透明的內容壓縮/解壓就是一個很好的示例。通常情況下這是由包裝器實體類使用了“裝飾者”模式來裝飾原始的實體完成的。一些協議攔截器可以從一個邏輯單元中來結合。

協議攔截器也可以通過共享信息來共同合作-比如處理狀態-通過HTTP執行上下文。協議攔截器可以使用HTTP內容來爲一個或多個連續的請求存儲一個處理狀態。

通常攔截器執行的順序不應該和它們基於的特定執行上下文狀態有關。如果協議攔截器有相互依存關係,那麼它們必須按特定順序來執行,正如它們希望執行的順序一樣,它們應該在相同的序列中被加到協議處理器。

協議攔截器必須實現爲線程安全的。和Servlet相似,協議攔截器不應該使用實例變量,除非訪問的那些變量是同步的。

這個示例給出了本地內容在連續的請求中怎麼被用於持久一個處理狀態的:

DefaultHttpClient httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
AtomicInteger count = new AtomicInteger(1);
localContext.setAttribute("count", count);
httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
public void process(final HttpRequest request,
final HttpContext context) throws HttpException, IOException {
AtomicInteger count = (AtomicInteger) context.getAttribute("count");
request.addHeader("Count", Integer.toString(count.getAndIncrement()));
}
});
HttpGet httpget = new HttpGet("http://localhost/");
for (int i = 0; i < 10; i++) {
HttpResponse response = httpclient.execute(httpget, localContext);
HttpEntity entity = response.getEntity();
if (entity != null) {
entity.consumeContent();
}
}

1.6 HTTP參數

HttpParams接口代表了定義組件運行時行爲的一個不變的值的集合。很多情況下,HttpParams和HttpContext相似。二者之間的主要區別是它們在運行時使用的不同。這兩個接口表示了對象的集合,它們被視作爲訪問對象值的鍵的Map,但是服務於不同的目的:

  • HttpParams旨在包含簡單對象:整型,浮點型,字符串,集合,還有運行時不變的對象。
  • HttpParams希望被用在“一次寫入-多處準備”模式下。HttpContext旨在包含很可能在HTTP報文處理這一過程中發生改變的複雜對象
  • HttpParams的目標是定義其它組件的行爲。通常每一個複雜的組件都有它自己的HttpParams對象。HttpContext的目標是來表示一個HTTP處理的執行狀態。通常相同的執行上下文在很多合作的對象中共享。

1.6.1 參數層次

在HTTP請求執行過程中,HttpRequest對象的HttpParams是和用於執行請求的HttpClient實例的HttpParams聯繫在一起的。這使得設置在HTTP請求級別的參數優先於設置在HTTP客戶端級別的HttpParams。推薦的做法是設置普通參數對所有的在HTTP客戶端級別的HTTP請求共享,而且可以選擇性重寫具體在HTTP請求級別的參數。

DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION,HttpVersion.HTTP_1_0);
httpclient.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET,"UTF-8");
HttpGet httpget = new HttpGet("http://www.google.com/");
httpget.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION,HttpVersion.HTTP_1_1);
httpget.getParams().setParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE,Boolean.FALSE);
httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
public void process(final HttpRequest request,
final HttpContext context) throws HttpException, IOException {
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.PROTOCOL_VERSION));
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.HTTP_CONTENT_CHARSET));
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.USE_EXPECT_CONTINUE));
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.STRICT_TRANSFER_ENCODING));
}
});

輸出內容爲:

HTTP/1.1
UTF-8
false
null

1.6.2 HTTP參數bean

HttpParams接口允許在處理組件的配置上很大的靈活性。很重要的是,新的參數可以被引入而不會影響老版本的二進制兼容性。然而,和常規的Java bean相比,HttpParams也有一個缺點:HttpParams不能使用DI框架來組合。爲了緩解這個限制,HttpClient包含了一些bean類,它們可以用來按順序使用標準的Java eban慣例初始化HttpParams對象。

HttpParams params = new BasicHttpParams();
HttpProtocolParamBean paramsBean = new HttpProtocolParamBean(params);
paramsBean.setVersion(HttpVersion.HTTP_1_1);
paramsBean.setContentCharset("UTF-8");
paramsBean.setUseExpectContinue(true);
System.out.println(params.getParameter(
CoreProtocolPNames.PROTOCOL_VERSION));
System.out.println(params.getParameter(
CoreProtocolPNames.HTTP_CONTENT_CHARSET));
System.out.println(params.getParameter(
CoreProtocolPNames.USE_EXPECT_CONTINUE));
System.out.println(params.getParameter(
CoreProtocolPNames.USER_AGENT));

輸出內容爲:

HTTP/1.1
UTF-8
false
null

1.7 HTTP請求執行參數

這些參數會影響到請求執行的過程:

  • 'http.protocol.version':如果沒有在請求對象中設置明確的版本信息,它就定義了使用的HTTP協議版本。這個參數期望得到一個ProtocolVersion類型的值。如果這個參數沒有被設置,那麼就使用HTTP/1.1。
  • 'http.protocol.element-charset':定義了編碼HTTP協議元素的字符集。這個參數期望得到一個java.lang.String類型的值。如果這個參數沒有被設置,那麼就使用US-ASCII。
  • 'http.protocol.eontent-charset':定義了爲每個內容主體編碼的默認字符集。這個參數期望得到一個java.lang.String類型的值。如果這個參數沒有被設置,那麼就使用ISO-8859-1。
  • 'http.useragent':定義了頭部信息User-Agent的內容。這個參數期望得到一個java.lang.String類型的值。如果這個參數沒有被設置,那麼HttpClient將會爲它自動生成一個值。
  • 'http.protocol.strict-transfer-encoding':定義了響應頭部信息中是否含有一個非法的Transfer-Encoding,都要拒絕掉。
  • 'http.protocol.expect-continue':爲包含方法的實體激活Expect: 100-Continue握手。Expect: 100-Continue握手的目的是允許客戶端使用請求體發送一個請求信息來決定源服務器是否希望在客戶端發送請求體之前得到這個請求(基於請求頭部信息)。Expect: 100-Continue握手的使用可以對需要目標服務器認證的包含請求的實體(比如POST和PUT)導致明顯的性能改善。Expect: 100-Continue握手應該謹慎使用,因爲它和HTTP服務器,不支持HTTP/1.1協議的代理使用會引起問題。這個參數期望得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,那麼HttpClient將會試圖使用握手。
  • 'http.protocol.wait-for-continue':定義了客戶端應該等待100-Continue響應最大的毫秒級時間間隔。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,那麼HttpClient將會在恢復請求體傳輸之前爲確認等待3秒。
第二章 連接管理

HttpClient有一個對連接初始化和終止,還有在活動連接上I/O操作的完整控制。而連接操作的很多方面可以使用一些參數來控制。

2.1 連接參數

這些參數可以影響連接操作:

  • 'http.socket.timeout':定義了套接字的毫秒級超時時間(SO_TIMEOUT),這就是等待數據,換句話說,在兩個連續的數據包之間最大的閒置時間。如果超時時間是0就解釋爲是一個無限大的超時時間。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,那麼讀取操作就不會超時(無限大的超時時間)。
  • 'http.tcp.nodelay':決定了是否使用Nagle算法。Nagle算法視圖通過最小化發送的分組數量來節省帶寬。當應用程序希望降低網絡延遲並提高性能時,它們可以關閉Nagle算法(也就是開啓TCP_NODELAY)。數據將會更早發送,增加了帶寬消耗的成文。這個參數期望得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,那麼TCP_NODELAY就會開啓(無延遲)。
  • 'http.socket.buffer-size':決定了內部套接字緩衝使用的大小,來緩衝數據同時接收/傳輸HTTP報文。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,那麼HttpClient將會分配8192字節的套接字緩存。
  • 'http.socket.linger':使用指定的秒數拖延時間來設置SO_LINGER。最大的連接超時值是平臺指定的。值0暗示了這個選項是關閉的。值-1暗示了使用了JRE默認的。這個設置僅僅影響套接字關閉操作。如果這個參數沒有被設置,那麼就假設值爲-1(JRE默認)。
  • 'http.connection.timeout':決定了直到連接建立時的毫秒級超時時間。超時時間的值爲0解釋爲一個無限大的時間。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,連接操作將不會超時(無限大的超時時間)。
  • 'http.connection.stalecheck':決定了是否使用舊的連接檢查。當在一個連接之上執行一個請求而服務器端的連接已經關閉時,關閉舊的連接檢查可能導致在獲得一個I/O錯誤風險時顯著的性能提升(對於每一個請求,檢查時間可以達到30毫秒)。這個參數期望得到一個java.lang.Boolean類型的值。出於性能的關鍵操作,檢查應該被關閉。如果這個參數沒有被設置,那麼舊的連接將會在每個請求執行之前執行。
  • 'http.connection.max-line-length':決定了最大請求行長度的限制。如果設置爲一個正數,任何HTTP請求行超過這個限制將會引發java.io.IOException異常。負數或零將會關閉這個檢查。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,那麼就不強制進行限制了。
  • 'http.connection.max-header-count':決定了允許的最大HTTP頭部信息數量。如果設置爲一個正數,從數據流中獲得的HTTP頭部信息數量超過這個限制就會引發java.io.IOException異常。負數或零將會關閉這個檢查。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,那麼就不
  • 強制進行限制了。
  • 'http.connection.max-status-line-garbage':決定了在期望得到HTTP響應狀態行之前可忽略請求行的最大數量。使用HTTP/1.1持久性連接,這個問題產生的破碎的腳本將會返回一個錯誤的Content-Length(有比指定的字節更多的發送)。不幸的是,在某些情況下,這個不能在錯誤響應後來偵測,只能在下一次之前。所以HttpClient必須以這種方式跳過那些多餘的行。這個參數期望得到一個java.lang.Integer類型的值。0是不允許在狀態行之前的所有垃圾/空行。使用java.lang.Integer#MAX_VALUE來設置不限制的數字。如果這個參數沒有被設置那就假設是不限制的。

2.2 持久連接

從一個主機向另外一個建立連接的過程是相當複雜的,而且包含了兩個終端之間的很多包的交換,它是相當費時的。連接握手的開銷是很重要的,特別是對小量的HTTP報文。如果打開的連接可以被重用來執行多次請求,那麼就可以達到很高的數據吞吐量。

HTTP/1.1強調HTTP連接默認情況可以被重用於多次請求。HTTP/1.0兼容的終端也可以使用相似的機制來明確地交流它們的偏好來保證連接處於活動狀態,也使用它來處理多個請求。HTTP代理也可以保持空閒連接處於一段時間的活動狀態,防止對相同目標主機的一個連接也許對隨後的請求需要。保持連接活動的能力通常被稱作持久性連接。HttpClient完全支持持久性連接。

2.3 HTTP連接路由

HttpClient能夠直接或通過路由建立連接到目標主機,這會涉及多箇中間連接,也被稱爲跳。HttpClient區分路由和普通連接,通道和分層。通道連接到目標主機的多箇中間代理的使用也稱作是代理鏈。

普通路由由連接到目標或僅第一次的代理來創建。通道路由通過代理鏈到目標連接到第一通道來建立。沒有代理的路由不是通道的,分層路由通過已存在連接的分層協議來建立。協議僅僅可以在到目標的通道上或在沒有代理的直接連接上分層。

2.3.1 路由計算

RouteInfo接口代表關於最終涉及一個或多箇中間步驟或跳的目標主機路由的信息。HttpRoute是RouteInfo的具體實現,這是不能改變的(是不變的)。HttpTracker是可變的RouteInfo實現,由HttpClient在內部使用來跟蹤到最大路由目標的剩餘跳數。HttpTracker可以在成功執行向路由目標的下一跳之後更新。HttpRouteDirector是一個幫助類,可以用來計算路由中的下一跳。這個類由HttpClient在內部使用。

HttpRoutePlanner是一個代表計算到基於執行上下文到給定目標完整路由策略的接口。HttpClient附帶兩個默認的HttpRoutePlanner實現。ProxySelectorRoutePlanner是基於java.net.ProxySelector的。默認情況下,它會從系統屬性中或從運行應用程序的瀏覽器中選取JVM的代理設置。DefaultHttpRoutePlanner實現既不使用任何Java系統屬性,也不使用系統或瀏覽器的代理設置。它只基於HTTP如下面描述的參數計算路由。

2.3.2 安全HTTP連接

如果信息在兩個不能由非認證的第三方進行讀取或修改的終端之間傳輸,HTTP連接可以被認爲是安全的。SSL/TLS協議是用來保證HTTP傳輸安全使用最廣泛的技術。而其它加密技術也可以被使用。通常來說,HTTP傳輸是在SSL/TLS加密連接之上分層的。

2.4 HTTP路由參數

這些參數可以影響路由計算:
  • 'http.route.default-proxy':定義可以被不使用JRE設置的默認路由規劃者使用的代理主機。這個參數期望得到一個HttpHost類型的值。如果這個參數沒有被設置,那麼就會嘗試直接連接到目標。
  • 'http.route.local-address':定義一個本地地址由所有默認路由規劃者來使用。有多個網絡接口的機器中,這個參數可以被用於從連接源中選擇網絡接口。這個參數期望得到一個java.net.InetAddress類型的值。如果這個參數沒有被設置,將會自動使用本地地址。
  • 'http.route.forced-route':定義一個由所有默認路由規劃者使用的強制路由。代替了計算路由,給定的強制路由將會被返回,儘管它指向一個完全不同的目標主機。這個參數期望得到一個HttpRoute類型的值。如果這個參數沒有被設置,那麼就使用默認的規則建立連接到目標服務器。

2.5 套接字工廠

LayeredSocketFactory是SocketFactory接口的擴展。分層的套接字工廠可HTTP連接內部使用java.net.Socket對象來處理數據在線路上的傳輸。它們依賴SocketFactory接口來創建,初始化和連接套接字。這會使得HttpClient的用戶可以提供在運行時指定套接字初始化代碼的應用程序。PlainSocketFactory是創建和初始化普通的(不加密的)套接字的默認工廠。

創建套接字的過程和連接到主機的過程是不成對的,所以套接字在連接操作封鎖時可以被關閉。

PlainSocketFactory sf = PlainSocketFactory.getSocketFactory();
Socket socket = sf.createSocket();
HttpParams params = new BasicHttpParams();
params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 1000L);
sf.connectSocket(socket, "locahost", 8080, null, -1, params);

2.5.1 安全套接字分層

LayeredSocketFactory是SocketFactory接口的擴展。分層的套接字工廠可以創建在已經存在的普通套接字之上的分層套接字。套接字分層主要通過代理來創建安全的套接字。HttpClient附帶實現了SSL/TLS分層的SSLSocketFactory。請注意HttpClient不使用任何自定義加密功能。它完全依賴於標準的Java密碼學(JCE)和安全套接字(JSEE)擴展。

2.5.2 SSL/TLS的定製

HttpClient使用SSLSocketFactory來創建SSL連接。SSLSocketFactory允許高度定製。它可以使用javax.net.ssl.SSLContext的實例作爲參數,並使用它來創建定製SSL連接。

TrustManager easyTrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// 哦,這很簡單!
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
//哦,這很簡單!
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
SSLContext sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, new TrustManager[] { easyTrustManager }, null);
SSLSocketFactory sf = new SSLSocketFactory(sslcontext);
SSLSocket socket = (SSLSocket) sf.createSocket();
socket.setEnabledCipherSuites(new String[] { "SSL_RSA_WITH_RC4_128_MD5" });
HttpParams params = new BasicHttpParams();
params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 1000L);
sf.connectSocket(socket, "locahost", 443, null, -1, params);
SSLSocketFactory的定製暗示出一定程度SSL/TLS協議概念的熟悉,這個詳細的解釋超出了本文檔的範圍。請參考Java的安全套接字擴展[http://java.sun.com/j2se/1.5.0/docs/guide/
security/jsse/JSSERefGuide.html],這是javax.net.ssl.SSLContext和相關工具的詳細描述。

2.5.3 主機名驗證

除了信任驗證和客戶端認證在SSL/TLS協議級上進行,一旦連接建立之後,HttpClient能可選地驗證目標主機名匹配存儲在服務器的X.509認證中的名字。這個認證可以提供額外的服務器信任材料的真實保證。X509主機名驗證接口代表了主機名驗證的策略。HttpClient附帶了3個X509主機名驗證器。很重要的一點是:主機名驗證不應該混淆SSL信任驗證。
  • StrictHostnameVerifier:嚴格的主機名驗證在Sun Java 1.4,Sun Java 5和Sun Java 6中是相同的。而且也非常接近IE6。這個實現似乎是兼容RFC 2818處理通配符的。主機名必須匹配第一個CN或任意的subject-alt。在CN和其它任意的subject-alt中可能會出現通配符。
  • BrowserCompatHostnameVerifier:主機名驗證器和Curl和Firefox的工作方式是相同的。主機名必須匹配第一個CN或任意的subject-alt。在CN和其它任意的subject-alt中可能會出現通配符。BrowserCompatHostnameVerifier和StrictHostnameVerifier的唯一不同是使用BrowserCompatHostnameVerifier匹配所有子域的通配符(比如”*.foo.com”),包括”a.b.foo.com”。
  • AllowAllHostnameVerifier:這個主機名驗證器基本上是關閉主機名驗證的。這個實現是一個空操作,而且不會拋出javax.net.ssl.SSLException異常。

每一個默認的HttpClient使用BrowserCompatHostnameVerifier的實現。如果需要的話,它可以指定不同的主機名驗證器實現。

SSLSocketFactory sf = new SSLSocketFactory(SSLContext.getInstance("TLS"));
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

2.6 協議模式

Scheme類代表了一個協議模式,比如“http”或“https”同時包含一些協議屬性,比如默認端口,用來爲給定協議創建java.net.Socket實例的套接字工廠。SchemeRegistry類用來維持一組Scheme,當去通過請求URI建立連接時,HttpClient可以從中選擇:

Scheme http = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
SSLSocketFactory sf = new SSLSocketFactory(SSLContext.getInstance("TLS"));
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
Scheme https = new Scheme("https", sf, 443);
SchemeRegistry sr = new SchemeRegistry();
sr.register(http);
sr.register(https);

2.7 HttpClient代理配置

儘管HttpClient瞭解複雜的路由模式和代理鏈,它僅支持簡單直接的或開箱的跳式代理連接。

告訴HttpClient通過代理去連接到目標主機的最簡單方式是通過設置默認的代理參數:

DefaultHttpClient httpclient = new DefaultHttpClient();
HttpHost proxy = new HttpHost("someproxy", 8080);
httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);

也可以構建HttpClient使用標準的JRE代理選擇器來獲得代理信息:

DefaultHttpClient httpclient = new DefaultHttpClient();
ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
httpclient.getConnectionManager().getSchemeRegistry(),
ProxySelector.getDefault());
httpclient.setRoutePlanner(routePlanner);

另外一種選擇,可以提供一個定製的RoutePlanner實現來獲得HTTP路由計算處理上的複雜的控制:

DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.setRoutePlanner(new HttpRoutePlanner() {
public HttpRoute determineRoute(HttpHost target,
HttpRequest request,
HttpContext context) throws HttpException {
return new HttpRoute(target, null, new HttpHost("someproxy", 8080),
"https".equalsIgnoreCase(target.getSchemeName()));
}
});

2.8 HTTP連接管理器

2.8.1 連接操作器

連接操作是客戶端的低層套接字或可以通過外部實體,通常稱爲連接操作的被操作的狀態的連接。OperatedClientConnection接口擴展了HttpClientConnection接口而且定義了額外的控制連接套接字的方法。ClientConnectionOperator接口代表了創建實例和更新那些對象低層套接字的策略。實現類最有可能利用SocketFactory來創建java.net.Socket實例。ClientConnectionOperator接口可以讓HttpClient的用戶提供一個連接操作的定製策略和提供可選實現OperatedClientConnection接口的能力。

2.8.2 管理連接和連接管理器

HTTP連接是複雜的,有狀態的,線程不安全的對象需要正確的管理以便正確地執行功能。HTTP連接在同一時間僅僅只能由一個執行線程來使用。HttpClient採用一個特殊實體來管理訪問HTTP連接,這被稱爲HTTP連接管理器,代表了ClientConnectionManager接口。一個HTTP連接管理器的目的是作爲工廠服務於新的HTTP連接,管理持久連接和同步訪問持久連接來確保同一時間僅有一個線程可以訪問一個連接。

內部的HTTP連接管理器和OperatedClientConnection實例一起工作,但是它們爲服務消耗器ManagedClientConnection提供實例。ManagedClientConnection扮演連接之上管理狀態控制所有I/O操作的OperatedClientConnection實例的包裝器。它也抽象套接字操作,提供打開和更新去創建路由套接字便利的方法。ManagedClientConnection實例瞭解產生它們到連接管理器的鏈接,而且基於這個事實,當不再被使用時,它們必須返回到管理器。ManagedClientConnection類也實現了ConnectionReleaseTrigger接口,可以被用來觸發釋放連接返回給管理器。一旦釋放連接操作被觸發了,被包裝的連接從ManagedClientConnection包裝器中脫離,OperatedClientConnection實例被返回給管理器。儘管服務消耗器仍然持有ManagedClientConnection實例的引用,它也不再去執行任何I/O操作或有意無意地改變的OperatedClientConnection狀態。

這裏有一個從連接管理器中獲取連接的示例:

HttpParams params = new BasicHttpParams();
Scheme http = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
SchemeRegistry sr = new SchemeRegistry();
sr.register(http);
ClientConnectionManager connMrg = new SingleClientConnManager(params, sr);
// 請求新連接。這可能是一個很長的過程。
ClientConnectionRequest connRequest = connMrg.requestConnection(
new HttpRoute(new HttpHost("localhost", 80)), null);
// 等待連接10秒
ManagedClientConnection conn = connRequest.getConnection(10, TimeUnit.SECONDS);
try {
// 用連接在做有用的事情。當完成時釋放連接。
conn.releaseConnection();
} catch (IOException ex) {
// 在I/O error之上終止連接。
conn.abortConnection();
throw ex;
}

如果需要,連接請求可以通過調用來ClientConnectionRequest#abortRequest()方法過早地中斷。這會解鎖在ClientConnectionRequest#getConnection()方法中被阻止的線程。

一旦響應內容被完全消耗後,BasicManagedEntity包裝器類可以用來保證自動釋放低層的連接。HttpClient內部使用這個機制來實現透明地對所有從HttpClient#execute()方法中獲得響應釋放連接:

ClientConnectionRequest connRequest = connMrg.requestConnection(
new HttpRoute(new HttpHost("localhost", 80)), null);
ManagedClientConnection conn = connRequest.getConnection(10, TimeUnit.SECONDS);
try {
BasicHttpRequest request = new BasicHttpRequest("GET", "/");
conn.sendRequestHeader(request);
HttpResponse response = conn.receiveResponseHeader();
conn.receiveResponseEntity(response);
HttpEntity entity = response.getEntity();
if (entity != null) {
BasicManagedEntity managedEntity = new BasicManagedEntity(entity, conn, true);
// 替換實體
response.setEntity(managedEntity);
}
// 使用響應對象做有用的事情。當響應內容被消耗後這個連接將會自動釋放。
} catch (IOException ex) {
//在I/O error之上終止連接。
conn.abortConnection();
throw ex;
}

2.8.3 簡單連接管理器

SingleClientConnManager是一個簡單的連接管理器,在同一時間它僅僅維護一個連接。儘管這個類是線程安全的,但它應該被用於一個執行線程。SingleClientConnManager對於同一路由的後續請求會盡量重用連接。而如果持久連接的路由不匹配連接請求的話,它也會關閉存在的連接之後對給定路由再打開一個新的。如果連接已經被分配,將會拋出java.lang.IllegalStateException異常。

對於每個默認連接,HttpClient使用SingleClientConnManager。

2.8.4 連接池管理器

ThreadSafeClientConnManager是一個複雜的實現來管理客戶端連接池,它也可以從多個執行線程中服務連接請求。對每個基本的路由,連接都是池管理的。對於路由的請求,管理器在池中有可用的持久性連接,將被從池中租賃連接服務,而不是創建一個新的連接。

ThreadSafeClientConnManager維護每個基本路由的最大連接限制。每個默認的實現對每個給定路由將會創建不超過兩個的併發連接,而總共也不會超過20個連接。對於很多真實的應用程序,這個限制也證明很大的制約,特別是他們在服務中使用HTTP作爲傳輸協議。連接限制,也可以使用HTTP參數來進行調整。

這個示例展示了連接池參數是如何來調整的:

HttpParams params = new BasicHttpParams();
// 增加最大連接到200
ConnManagerParams.setMaxTotalConnections(params, 200);
// 增加每個路由的默認最大連接到20
ConnPerRouteBean connPerRoute = new ConnPerRouteBean(20);
// 對localhost:80增加最大連接到50
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);

2.8.5 連接管理器關閉

當一個HttpClient實例不再需要時,而且即將走出使用範圍,那麼關閉連接管理器來保證由管理器保持活動的所有連接被關閉,由連接分配的系統資源被釋放是很重要的。

DefaultHttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://www.google.com/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
System.out.println(response.getStatusLine());
if (entity != null) {
entity.consumeContent();
}
httpclient.getConnectionManager().shutdown();

2.9 連接管理參數

這些是可以用於定製標準HTTP連接管理器實現的參數:
  • 'http.conn-manager.timeout':定義了當從ClientConnectionManager中檢索ManagedClientConnection實例時使用的毫秒級的超時時間。這個參數期望得到一個java.lang.Long類型的值。如果這個參數沒有被設置,連接請求就不會超時(無限大的超時時間)。
  • 'http.conn-manager.max-per-route':定義了每個路由連接的最大數量。這個限制由客戶端連接管理器來解釋,而且應用於獨立的管理器實例。這個參數期望得到一個ConnPerRoute類型的值。
  • 'http.conn-manager.max-total':定義了總共連接的最大數目。這個限制由客戶端連接管理器來解釋,而且應用於獨立的管理器實例。這個參數期望得到一個java.lang.Integer類型的值。

2.10 多線程執行請求

當配備連接池管理器時,比如ThreadSafeClientConnManager,HttpClient可以同時被用來執行多個請求,使用多線程執行。

ThreadSafeClientConnManager將會分配基於它的配置的連接。如果對於給定路由的所有連接都被租出了,那麼連接的請求將會阻塞,直到一個連接被釋放回連接池。它可以通過設置'http.conn-manager.timeout'爲一個正數來保證連接管理器不會在連接請求執行時無限期的被阻塞。如果連接請求不能在給定的時間週期內被響應,將會拋出ConnectionPoolTimeoutException異常。

HttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(
new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
HttpClient httpClient = new DefaultHttpClient(cm, params);
// 執行GET方法的URI
String[] urisToGet = {
"http://www.domain1.com/",
"http://www.domain2.com/",
"http://www.domain3.com/",
"http://www.domain4.com/"
};
// 爲每個URI創建一個線程
GetThread[] threads = new GetThread[urisToGet.length];
for (int i = 0; i < threads.length; i++) {
HttpGet httpget = new HttpGet(urisToGet[i]);
threads[i] = new GetThread(httpClient, httpget);
}
// 開始執行線程
for (int j = 0; j < threads.length; j++) {
threads[j].start();
}
// 合併線程
for (int j = 0; j < threads.length; j++) {
threads[j].join();
}
 
static class GetThread extends Thread {
private final HttpClient httpClient;
private final HttpContext context;
private final HttpGet httpget;
public GetThread(HttpClient httpClient, HttpGet httpget) {
this.httpClient = httpClient;
this.context = new BasicHttpContext();
this.httpget = httpget;
}
@Override
public void run() {
try {
HttpResponse response = this.httpClient.execute(this.httpget, this.context);
HttpEntity entity = response.getEntity();
if (entity != null) {
// 對實體做些有用的事情...
// 保證連接能釋放回管理器
entity.consumeContent();
}
} catch (Exception ex) {
this.httpget.abort();
}
}
}

2.11 連接收回策略

一個經典的阻塞I/O模型的主要缺點是網絡套接字僅當I/O操作阻塞時纔可以響應I/O事件。當一個連接被釋放返回管理器時,它可以被保持活動狀態而卻不能監控套接字的狀態和響應任何I/O事件。如果連接在服務器端關閉,那麼客戶端連接也不能去偵測連接狀態中的變化和關閉本端的套接字去作出適當響應。

HttpClient通過測試連接是否是過時的來嘗試去減輕這個問題,這已經不再有效了,因爲它已經在服務器端關閉了,之前使用執行HTTP請求的連接。過時的連接檢查也並不是100%的穩定,反而對每次請求執行還要增加10到30毫秒的開銷。唯一可行的而不涉及到每個對空閒連接的套接字模型線程解決方案,是使用專用的監控線程來收回因爲長時間不活動而被認爲是過期的連接。監控線程可以週期地調用ClientConnectionManager#closeExpiredConnections()方法來關閉所有過期的連接,從連接池中收回關閉的連接。它也可以選擇性調用ClientConnectionManager#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) {
// 終止
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}

2.12 連接保持活動的策略

HTTP規範沒有確定一個持久連接可能或應該保持活動多長時間。一些HTTP服務器使用非標準的頭部信息Keep-Alive來告訴客戶端它們想在服務器端保持連接活動的週期秒數。如果這個信息可用,HttClient就會利用這個它。如果頭部信息Keep-Alive在響應中不存在,HttpClient假設連接無限期的保持活動。然而許多現實中的HTTP服務器配置了在特定不活動週期之後丟掉持久連接來保存系統資源,往往這是不通知客戶端的。如果默認的策略證明是過於樂觀的,那麼就會有人想提供一個定製的保持活動策略。

DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
// 兌現'keep-alive'頭部信息
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.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// 只保持活動5秒
return 5 * 1000;
} else {
// 否則保持活動30秒
return 30 * 1000;
}
}
});
第三章 HTTP狀態管理

原始的HTTP是被設計爲無狀態的,面向請求/響應的協議,沒有特殊規定有狀態的,貫穿一些邏輯相關的請求/響應交換的會話。由於HTTP協議變得越來越普及和受歡迎,越來越多的從前沒有打算使用它的系統也開始爲應用程序來使用它,比如作爲電子商務應用程序的傳輸方式。因此,支持狀態管理就變得非常必要了。

網景公司,一度成爲Web客戶端和服務器軟件開發者的領導方向,在它們基於專有規範的產品中實現了對HTTP狀態管理的支持。之後,網景公司試圖通過發佈規範草案來規範這種機制。它們的努力通過RFC標準跟蹤促成了這些規範定義。然而,在很多應用程序中的狀態管理仍然基於網景公司的草案而不兼容官方的規範。很多主要的Web瀏覽器開發者覺得有必要保留那些極大促進標準片段應用程序的兼容性。

3.1 HTTP cookies

Cookie是HTTP代理和目標服務器可以交流保持會話的狀態信息的令牌或短包。網景公司的工程師用它來指“魔法小甜餅”和粘住的名字。

HttpClient使用Cookie接口來代表抽象的cookie令牌。在它的簡單形式中HTTP的cookie幾乎是名/值對。通常一個HTTP的cookie也包含一些屬性,比如版本號,合法的域名,指定cookie應用所在的源服務器URL子集的路徑,cookie的最長有效時間。

SetCookie接口代表由源服務器發送給HTTP代理的響應中的頭部信息Set-Cookie來維持一個對話狀態。SetCookie2接口和指定的Set-Cookie2方法擴展了SetCookie。

SetCookie接口和額外的如獲取原始cookie屬性的能力,就像它們由源服務器指定的客戶端特定功能擴展了Cookie接口。這對生成Cookie頭部很重要,因爲一些cookie規範需要。Cookie頭部應該包含在Set-Cookie或Set-Cookie2頭部中指定的特定屬性。

3.1.1 Cookie版本

Cookie兼容網景公司的草案標準,但是版本0被認爲是不符合官方規範的。符合標準的cookie的期望版本是1。HttpClient可以處理基於不同版本的cookie。

這裏有一個重新創建網景公司草案cookie示例:

BasicClientCookie netscapeCookie = new BasicClientCookie("name", "value");
netscapeCookie.setVersion(0);
netscapeCookie.setDomain(".mycompany.com");
netscapeCookie.setPath("/");

這是一個重新創建標準cookie的示例。要注意符合標準的cookie必須保留由源服務器發送的所有屬性:

BasicClientCookie stdCookie = new BasicClientCookie("name", "value");
stdCookie.setVersion(1);
stdCookie.setDomain(".mycompany.com");
stdCookie.setPath("/");
stdCookie.setSecure(true);
// 精確設置由服務器發送的屬性
stdCookie.setAttribute(ClientCookie.VERSION_ATTR, "1");
stdCookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");

這是一個重新創建Set-Cookie2兼容cookie的實例。要注意符合標準的cookie必須保留由源服務器發送的所有屬性:

BasicClientCookie2 stdCookie = new BasicClientCookie2("name", "value");
stdCookie.setVersion(1);
stdCookie.setDomain(".mycompany.com");
stdCookie.setPorts(new int[] {80,8080});
stdCookie.setPath("/");
stdCookie.setSecure(true);
// 精確設置由服務器發送的屬性
stdCookie.setAttribute(ClientCookie.VERSION_ATTR, "1");
stdCookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");
stdCookie.setAttribute(ClientCookie.PORT_ATTR, "80,8080");

3.2 Cookie規範

CookieSpec接口代表了cookie管理的規範。Cookie管理規範希望如下幾點:
  • 解析的Set-Cookie規則還有可選的Set-Cookie2頭部信息。
  • 驗證解析cookie的規則。
  • 格式化給定主機的Cookie頭部信息,原始端口和路徑。

HttpClient附帶了一些CookieSpec的實現:

  • 網景公司草案:這個規範符合由網景通訊發佈的原始草案規範。應當避免,除非有絕對的必要去兼容遺留代碼。
  • RFC 2109:官方HTTP狀態管理規範並取代的老版本,被RFC 2965取代。
  • RFC 2965:官方HTTP狀態管理規範。
  • 瀏覽器兼容性:這個實現努力去密切模仿(mis)通用Web瀏覽器應用程序的實現。比如微軟的Internet Explorer和Mozilla的FireFox瀏覽器。
  • 最佳匹配:’Meta’(元)cookie規範採用了一些基於又HTTP響應發送的cookie格式的cookie策略。它基本上聚合了以上所有的實現到以一個類中。
強烈建議使用Best Match策略,讓HttpClient在運行時基於執行上下文采用一些合適的兼容等級。

3.3 HTTP cookie和狀態管理參數

這些是用於定製HTTP狀態管理和獨立的cookie規範行爲的參數。
  • 'http.protocol.cookie-datepatterns':定義了用於解析非標準的expires屬性的合法日期格式。只是對兼容不符合規定的,仍然使用網景公司草案定義的expires而不使用標準的max-age屬性服務器需要。這個參數期望得到一個java.util.Collection類型的值。集合元素必須是java.lang.String類型,來兼容java.text.SimpleDateFormat的語法。如果這個參數沒有被設置,那麼默認的選擇就是CookieSpec實現規範的值。要注意這個參數的應用。
  • 'http.protocol.single-cookie-header':定義了是否cookie應該強制到一個獨立的Cookie請求頭部信息中。否則,每個cookie就被當作分離的Cookie頭部信息來格式化。這個參數期望得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,那麼默認的選擇就是CookieSpec實現規範的值。要注意這個參數僅僅嚴格應用於cookie規範(RFC 2109和RFC 2965)。瀏覽器兼容性和網景公司草案策略將會放置所有的cookie到一個請求頭部信息中。
  • 'http.protocol.cookie-policy':定義了用於HTTP狀態管理的cookie規範的名字。這個參數期望得到一個java.lang.String類型的值。如果這個參數沒有被設置,那麼合法的日期格式就是CookieSpec實現規範的值。

3.4 Cookie規範註冊表

HttpClient使用CookieSpecRegistry類維護一個可用的cookie規範註冊表。下面的規範對於每個默認都是註冊過的:
  • 兼容性:瀏覽器兼容性(寬鬆策略)。
  • 網景:網景公司草案。
  • rfc2109:RFC 2109(過時的嚴格策略)。
  • rfc2965:RFC 2965(嚴格策略的標準符合)。
  • best-match:最佳匹配meta(元)策略。

3.5 選擇cookie策略

Cookie策略可以在HTTP客戶端被設置,如果需要,在HTTP請求級重寫。
HttpClient httpclient = new DefaultHttpClient();
// 對每個默認的強制嚴格cookie策略
httpclient.getParams().setParameter(
ClientPNames.COOKIE_POLICY, CookiePolicy.RFC_2965);
HttpGet httpget = new HttpGet("http://www.broken-server.com/");
// 對這個請求覆蓋默認策略
httpget.getParams().setParameter(
ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);

3.6 定製cookie策略

爲了實現定製cookie策略,我們應該創建CookieSpec接口的定製實現類,創建一個CookieSpecFactory實現來創建和初始化定製實現的實例並和HttpClient註冊這個工廠。一旦定製實現被註冊了,它可以和標準的cookie實現有相同的活性。
CookieSpecFactory csf = new CookieSpecFactory() {
public CookieSpec newInstance(HttpParams params) {
return new BrowserCompatSpec() {
@Override
public void validate(Cookie cookie, CookieOrigin origin)
throws MalformedCookieException {
// 這相當簡單
}
};
}
};
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getCookieSpecs().register("easy", csf);
httpclient.getParams().setParameter(
ClientPNames.COOKIE_POLICY, "easy");

3.7 Cookie持久化

HttpClient可以和任意物理表示的實現了CookieStore接口的持久化cookie存儲一起使用。默認的CookieStore實現稱爲BasicClientCookie,這是憑藉java.util.ArrayList的一個簡單實現。在BasicClientCookie對象中存儲的cookie當容器對象被垃圾回收機制回收時會丟失。如果需要,用戶可以提供更復雜的實現。
DefaultHttpClient httpclient = new DefaultHttpClient();
// 創建一個本地的cookie store實例
CookieStore cookieStore = new MyCookieStore();
// 如果需要填充cookie
BasicClientCookie cookie = new BasicClientCookie("name", "value");
cookie.setVersion(0);
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
cookieStore.addCookie(cookie);
// 設置存儲
httpclient.setCookieStore(cookieStore);

3.8 HTTP狀態管理和執行上下文

在HTTP請求執行的過程中,HttpClient添加了下列和狀態管理相關的對象到執行上下文中:
  • 'http.cookiespec-registry':CookieSpecRegistry實例代表了實際的cookie規範註冊表。這個屬性的值設置在本地內容中,優先於默認的。
  • 'http.cookie-spec':CookieSpec實例代表真實的cookie規範。
  • 'http.cookie-origin':CookieOrigin實例代表了真實的源服務器的詳細信息。
  • 'http.cookie-store':CookieStore實例代表了真實的cookie存儲。設置在本地內容中的這個屬性的值優先於默認的。

本地的HttpContext對象可以被用來定製HTTP狀態管理內容,先於請求執行或在請求執行之後檢查它的狀態:

HttpClient httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
HttpGet httpget = new HttpGet("http://localhost:8080/");
HttpResponse response = httpclient.execute(httpget, localContext);
CookieOrigin cookieOrigin = (CookieOrigin) localContext.getAttribute(
ClientContext.COOKIE_ORIGIN);
System.out.println("Cookie origin: " + cookieOrigin);
CookieSpec cookieSpec = (CookieSpec) localContext.getAttribute(
ClientContext.COOKIE_SPEC);
System.out.println("Cookie spec used: " + cookieSpec);

3.9 每個用戶/線程的狀態管理

我們可以使用獨立的本地執行上下文來實現對每個用戶(或每個線程)狀態的管理。定義在本地內容中的cookie規範註冊表和cookie存儲將會優先於設置在HTTP客戶端級別中默認的那些。
HttpClient httpclient = new DefaultHttpClient();
// 創建cookie store的本地實例
CookieStore cookieStore = new BasicCookieStore();
// 創建本地的HTTP內容
HttpContext localContext = new BasicHttpContext();
// 綁定定製的cookie store到本地內容中
localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
HttpGet httpget = new HttpGet("http://www.google.com/");
// 作爲參數傳遞本地內容
HttpResponse response = httpclient.execute(httpget, localContext)
第四章 HTTP認證
HttpClient提供對由HTTP標準規範定義的認證模式的完全支持。HttpClient的認證框架可以擴展支持非標準的認證模式,比如NTLM和SPNEGO。

4.1 用戶憑證

任何用戶身份驗證的過程都需要一組可以用於建立用戶身份的憑據。用戶憑證的最簡單的形式可以僅僅是用戶名/密碼對。UsernamePasswordCredentials代表了一組包含安全規則和明文密碼的憑據。這個實現對由HTTP標準規範中定義的標準認證模式是足夠的

UsernamePasswordCredentials creds = new UsernamePasswordCredentials("user", "pwd");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());

輸出內容爲:

user
pwd

NTCredentials是微軟Windows指定的實現,它包含了除了用戶名/密碼對外,一組額外的Windows指定的屬性,比如用戶域名的名字,比如在微軟的Windows網絡中,相同的用戶使用不同設置的認證可以屬於不同的域。

NTCredentials creds = new NTCredentials("user", "pwd", "workstation", "domain");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());

輸出內容爲:

DOMAIN/user
pwd

4.2 認證模式

AuthScheme接口代表了抽象的,面向挑戰-響應的認證模式。一個認證模式期望支持如下的功能:
  • 解析和處理由目標服務器在對受保護資源請求的響應中發回的挑戰。
  • 提供處理挑戰的屬性:認證模式類型和它的參數,如果可用,比如這個認證模型可應用的領域。
  • 對給定的憑證組和HTTP請求對響應真實認證挑戰生成認證字符串。
要注意認證模式可能是有狀態的,涉及一系列的挑戰-響應交流。HttpClient附帶了一些AuthScheme實現:
  • Basic(基本):Basic認證模式定義在RFC 2617中。這個認證模式是不安全的,因爲憑據以明文形式傳送。儘管它不安全,如果用在和TLS/SSL加密的組合中,Basic認證模式是完全夠用的。
  • Digest(摘要):Digest認證模式定義在RFC 2617中。Digest認證模式比Basic有顯著的安全提升,對不想通過TLS/SL加密在完全運輸安全上開銷的應用程序來說也是很好的選擇。
  • NTLM:NTLM是一個由微軟開發的優化Windows平臺的專有認證模式。NTLM被認爲是比Digest更安全的模式。這個模式需要外部的NTLM引擎來工作。要獲取更多詳情請參考包含在HttpClient發佈包中的NTLM_SUPPORT.txt文檔。

4.3 HTTP認證參數

有一些可以用於定製HTTP認證過程和獨立認證模式行爲的參數:
  • 'http.protocol.handle-authentication':定義了是否認證應該被自動處理。這個參數期望的得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,HttpClient將會自動處理認證。
  • 'http.auth.credential-charset':定義了當編碼用戶憑證時使用的字符集。這個參數期望得到一個java.lang.String類型的值。如果這個參數沒有被設置,那麼就會使用US-ASCII。

4.4 認證模式註冊表

HttpClient使用AuthSchemeRegistry類維護一個可用的認證模式的註冊表。對於每個默認的下面的模式是註冊過的:
  • Basic:基本認證模式
  • Digest:摘要認證模式
請注意NTLM模式沒有對每個默認的進行註冊。NTLM不能對每個默認開啓是應爲許可和法律上的原因。要獲取更詳細的關於如何開啓NTLM支持的內容請看這部分。

4.5 憑據提供器

憑據提供器意來維護一組用戶憑據,還有能夠對特定認證範圍生產用戶憑據。認證範圍包括主機名,端口號,領域名稱和認證模式名稱。當使用憑據提供器來註冊憑據時,我們可以提供一個通配符(任意主機,任意端口,任意領域,任意模式)來替代確定的屬性值。如果直接匹配沒有發現,憑據提供器期望被用來發現最匹配的特定範圍。

HttpClient可以和任意實現了CredentialsProvider接口的憑據提供器的物理代表一同工作。默認的CredentialsProvider實現被稱爲BasicCredentialsProvider,它是簡單的憑藉java.util.HashMap的實現。
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope("somehost", AuthScope.ANY_PORT),
new UsernamePasswordCredentials("u1", "p1"));
credsProvider.setCredentials(
new AuthScope("somehost", 8080),
new UsernamePasswordCredentials("u2", "p2"));
credsProvider.setCredentials(
new AuthScope("otherhost", 8080, AuthScope.ANY_REALM, "ntlm"),
new UsernamePasswordCredentials("u3", "p3"));
System.out.println(credsProvider.getCredentials(
new AuthScope("somehost", 80, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
new AuthScope("somehost", 8080, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
new AuthScope("otherhost", 8080, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
new AuthScope("otherhost", 8080, null, "ntlm")));

輸出內容爲:

[principal: u1]
[principal: u2]
null
[principal: u3]

4.6 HTTP認證和執行上下文

HttpClient依賴於AuthState類來跟蹤關於認證過程狀態的詳細信息。在HTTP請求執行過程中,HttpClient創建2個AuthState的實例:一個對於目標主機認證,另外一個對於代理認證。如果目標服務器或代理需要用戶認證,那麼各自的AuthState實例將會被在認證處理過程中使用的AuthScope,AuthScheme和Crednetials來填充。AuthState可以被檢查來找出請求的認證是什麼類型的,是否匹配AuthScheme的實現,是否憑據提供器對給定的認證範圍去找用戶憑據。

在HTTP請求執行的過程中,HttpClient添加了下列和認證相關的對象到執行上下文中:

  • 'http.authscheme-registry':AuthSchemeRegistry實例代表真實的認證模式註冊表。在本地內容中設置的這個屬性的值優先於默認的。
  • 'http.auth.credentials-provider':CookieSpec實例代表了真實的憑據提供器。在本地內容中設置的這個屬性的值優先於默認的。
  • 'http.auth.target-scope':AuthState實例代表了真實的目標認證狀態。在本地內容中設置的這個屬性的值優先於默認的。
  • 'http.auth.proxy-scope':AuthState實例代表了真實的代理認證狀態。在本地內容中設置的這個屬性的值優先於默認的。

本地的HttpContext對象可以用於定製HTTP認證內容,並先於請求執行或在請求被執行之後檢查它的狀態:

HttpClient httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
HttpGet httpget = new HttpGet("http://localhost:8080/");
HttpResponse response = httpclient.execute(httpget, localContext);
AuthState proxyAuthState = (AuthState) localContext.getAttribute(
ClientContext.PROXY_AUTH_STATE);
System.out.println("Proxy auth scope: " + proxyAuthState.getAuthScope());
System.out.println("Proxy auth scheme: " + proxyAuthState.getAuthScheme());
System.out.println("Proxy auth credentials: " + proxyAuthState.getCredentials());
AuthState targetAuthState = (AuthState) localContext.getAttribute(
ClientContext.TARGET_AUTH_STATE);
System.out.println("Target auth scope: " + targetAuthState.getAuthScope());
System.out.println("Target auth scheme: " + targetAuthState.getAuthScheme());
System.out.println("Target auth credentials: " + targetAuthState.getCredentials());

4.7 搶佔認證

HttpClient不支持開箱的搶佔認證,因爲濫用或重用不正確的搶佔認證可能會導致嚴重的安全問題,比如將用戶憑據以明文形式發送給未認證的第三方。因此,用戶期望評估搶佔認證和在它們只能應用程序環境內容安全風險潛在的好處,而且要求使用如協議攔截器的標準HttpClient擴展機制添加對搶佔認證的支持。

這是一個簡單的協議攔截器,如果沒有企圖認證,來搶先引入BasicScheme的實例到執行上下文中。請注意攔截器必須在標準認證攔截器之前加入到協議處理鏈中。

HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() {
public void process(final HttpRequest request,
final HttpContext context) throws HttpException, IOException {
AuthState authState = (AuthState) context.getAttribute(
ClientContext.TARGET_AUTH_STATE);
CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER);
HttpHost targetHost = (HttpHost) context.getAttribute(
ExecutionContext.HTTP_TARGET_HOST);
// 如果沒有初始化auth模式
if (authState.getAuthScheme() == null) {
AuthScope authScope = new AuthScope(
targetHost.getHostName(),
targetHost.getPort());
// 獲得匹配目標主機的憑據
Credentials creds = credsProvider.getCredentials(authScope);
// 如果發現了,搶先生成BasicScheme
if (creds != null) {
authState.setAuthScheme(new BasicScheme());
authState.setCredentials(creds);
}
}
}
};
DefaultHttpClient httpclient = new DefaultHttpClient();
// 作爲第一個攔截器加入到協議鏈中
httpclient.addRequestInterceptor(preemptiveAuth, 0);

4.8 NTLM 認證

當前HttpClient沒有提對開箱的NTLM認證模式的支持也可能永遠也不會。這個原因是法律上的而不是技術上的。然而,NTLM認證可以使用外部的NTLM引擎比如JCIFS[http://jcifs.samba.org/]來開啓,類庫由Samba[http://www.samba.org/]項目開發,作爲它們Windows的交互操作程序套裝的一部分。要獲取詳細內容請參考HttpClient發行包中包含的NTLM_SUPPORT.txt文檔。

4.8.1 NTLM連接持久化

NTLM認證模式是在計算開銷方面昂貴的多的,而且對標準的Basic和Digest模式的性能影響也很大。這很可能是爲什麼微軟選擇NTLM認證模式爲有狀態的主要原因之一。也就是說,一旦認證通過,用戶標識是和連接的整個生命週期相關聯的。NTLM連接的狀態特性使得連接持久化非常複雜,對於明顯的原因,持久化NTLM連接不能被使用不同用戶標識的用戶重用。標準的連接管理器附帶HttpClient是完全能夠管理狀態連接的。而邏輯相關的,使用同一session和執行上下文爲了讓它們瞭解到當前的用戶標識的請求也是極爲重要的。否則,HttpClient將會終止對每個基於NTLM保護資源的HTTP請求創建新的HTTP連接。要獲取關於有狀態的HTTP連接的詳細討論,請參考這個部分。

因爲NTLM連接是有狀態的,通常建議使用相對簡單的方法觸發NTLM認證,比如GET或HEAD,而重用相同的連接來執行代價更大的方法,特別是它們包含請求實體,比如POST或PUT。

DefaultHttpClient httpclient = new DefaultHttpClient();
NTCredentials creds = new NTCredentials("user", "pwd", "myworkstation", "microsoft.com");
httpclient.getCredentialsProvider().setCredentials(AuthScope.ANY, creds);
HttpHost target = new HttpHost("www.microsoft.com", 80, "http");
// 保證相同的內容來用於執行邏輯相關的請求
HttpContext localContext = new BasicHttpContext();
// 首先執行簡便的方法。這會觸發NTLM認證
HttpGet httpget = new HttpGet("/ntlm-protected/info");
HttpResponse response1 = httpclient.execute(target, httpget, localContext);
HttpEntity entity1 = response1.getEntity();
if (entity1 != null) {
entity1.consumeContent();
}
//之後使用相同的內容(和連接)執行開銷大的方法。
HttpPost httppost = new HttpPost("/ntlm-protected/form");
httppost.setEntity(new StringEntity("lots and lots of data"));
HttpResponse response2 = httpclient.execute(target, httppost, localContext);
HttpEntity entity2 = response2.getEntity();
if (entity2 != null) {
entity2.consumeContent();
}
第五章 HTTP客戶端服務

5.1 HttpClient門面

HttpClient接口代表了最重要的HTTP請求執行的契約。它沒有在請求執行處理上強加限制或特殊細節,而在連接管理,狀態管理,認證和處理重定向到具體實現上留下了細節。這應該使得很容易使用額外的功能,比如響應內容緩存來裝飾接口。

DefaultHttpClient是HttpClient接口的默認實現。這個類扮演了很多特殊用戶程序或策略接口實現負責處理特定HTTP協議方面,比如重定向到處理認證或做出關於連接持久化和保持活動的持續時間決定的門面。這使得用戶可以選擇使用定製,具體程序等來替換某些方面默認實現。

DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response,
HttpContext context) {
long keepAlive = super.getKeepAliveDuration(response, context);
if (keepAlive == -1) {
// 如果keep-alive值沒有由服務器明確設置,那麼保持連接持續5秒。
keepAlive = 5000;
}
return keepAlive;
}
});

DefaultHttpClient也維護一組協議攔截器,意在處理即將離開的請求和即將到達的響應,而且提供管理那些攔截器的方法。新的協議攔截器可以被引入到協議處理器鏈中,或在需要時從中移除。內部的協議攔截器存儲在一個簡單的java.util.ArrayList中。它們以被加入到list中的自然順序來執行。

DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.removeRequestInterceptorByClass(RequestUserAgent.class);
httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
public void process(
HttpRequest request, HttpContext context)
throws HttpException, IOException {
request.setHeader(HTTP.USER_AGENT, "My-own-client");
}
});

DefaultHttpClient是線程安全的。建議相同的這個類的實例被重用於多個請求的執行。當一個DefaultHttpClient實例不再需要而且要脫離範圍時,和它關聯的連接管理器必須調用ClientConnectionManager#shutdown()方法關閉。

HttpClient httpclient = new DefaultHttpClient();
// 做些有用的事
httpclient.getConnectionManager().shutdown();

5.2 HttpClient參數

這些是可以用於定製默認HttpClient實現行爲的參數:
  • 'http.protocol.handle-redirects':定義了重定向是否應該自動處理。這個參數期望得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,HttpClient將會自動處理重定向。
  • 'http.protocol.reject-relative-redirect':定義了是否相對的重定向應該被拒絕。HTTP規範需要位置值是一個絕對URI。這個參數期望得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,那麼就允許相對重定向。
  • 'http.protocol.max-redirects':定義了要遵循重定向的最大數量。這個重定向數字的限制意在防止由破碎的服務器端腳本引發的死循環。這個參數期望得到一個java.lang.Integer類型的值。如果這個參數沒有被設置,那麼只允許不多餘100次重定向。
  • 'http.protocol.allow-circular-redirects':定義環形重定向(重定向到相同路徑)是否被允許。HTTP規範在環形重定向沒有足夠清晰的允許表述,因此這作爲可選的是可以開啓的。這個參數期望得到一個java.lang.Boolean類型的值。如果這個參數沒有被設置,那麼環形重定向就不允許。
  • 'http.connection-manager.factory-class-name':定義了默認的ClientConnectionManager實現的類型。這個參數期望得到一個java.lang.String類型的值。如果這個參數沒有被設置,對於每個默認的將使用SingleClientConnManager。
  • 'http.virtual-host':定義了在頭部信息Host中使用的虛擬主機名稱,來代替物理主機名稱。這個參數期望得到一個HttpHost類型的值。如果這個參數沒有被設置,那麼將會使用目標主機的名稱或IP地址。
  • 'http.default-headers':定義了每次請求默認發送的頭部信息。這個參數期望得到一個包含Header對象的java.util.Collection類型值。
  • 'http.default-host':定義了默認主機。如果目標主機沒有在請求URI(相對URI)中明確指定,那麼就使用默認值。這個參數期望得到一個HttpHost類型的值。

5.3 自動重定向處理

HttpClient處理所有類型的自動重定向,除了那些由HTTP規範明令禁止的,比如需要用戶干預的。參考其它(狀態碼303)POST和PUT請求重定向轉換爲由HTTP規範需要的GET請求。

5.4 HTTP客戶端和執行上下文

DefaultHttpClient將HTTP請求視爲不變的對象,也從來不會假定在請求執行期間改變。相反,它創建了一個原請求對象私有的可變副本,副本的屬性可以基於執行上下文來更新。因此,如目標主鍵和請求URI的final類型的請求參數可以在請求執行之後,由檢查本地HTTP上下文來決定。

DefaultHttpClient httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
HttpGet httpget = new HttpGet("http://localhost:8080/");
HttpResponse response = httpclient.execute(httpget, localContext);
HttpHost target = (HttpHost) localContext.getAttribute(
ExecutionContext.HTTP_TARGET_HOST);
HttpUriRequest req = (HttpUriRequest) localContext.getAttribute(
ExecutionContext.HTTP_REQUEST);
System.out.println("Target host: " + target);
System.out.println("Final request URI: " + req.getURI());
System.out.println("Final request method: " + req.getMethod());
第六章 高級主題

6.1 自定義客戶端連接

在特定條件下,也許需要來定製HTTP報文通過線路傳遞,越過了可能使用的HTTP參數來處理非標準不兼容行爲的方式。比如,對於Web爬蟲,它可能需要強制HttpClient接受格式錯誤的響應頭部信息,來搶救報文的內容。

通常插入一個自定義的報文解析器的過程或定製連接實現需要幾個步驟:

提供一個自定義LineParser/LineFormatter接口實現。如果需要,實現報文解析/格式化邏輯。

class MyLineParser extends BasicLineParser {
@Override
public Header parseHeader(
final CharArrayBuffer buffer) throws ParseException {
try {
return super.parseHeader(buffer);
} catch (ParseException ex) {
// 壓制ParseException異常
return new BasicHeader("invalid", buffer.toString());
}
}
}

提過一個自定義的OperatedClientConnection實現。替換需要自定義的默認請求/響應解析器,請求/響應格式化器。如果需要,實現不同的報文寫入/讀取代碼。

class MyClientConnection extends DefaultClientConnection {
@Override
protected HttpMessageParser createResponseParser(
final SessionInputBuffer buffer,
final HttpResponseFactory responseFactory,
final HttpParams params) {
return new DefaultResponseParser(buffer,
new MyLineParser(),responseFactory,params);
}
}

爲了創建新類的連接,提供一個自定義的ClientConnectionOperator接口實現。如果需要,實現不同的套接字初始化代碼。

class MyClientConnectionOperator extends
DefaultClientConnectionOperator {
public MyClientConnectionOperator(
final SchemeRegistry sr) {
super(sr);
}
@Override
public OperatedClientConnection createConnection() {
return new MyClientConnection();
}
}

爲了創建新類的連接操作,提供自定義的ClientConnectionManager接口實現。

class MyClientConnManager extends SingleClientConnManager {
public MyClientConnManager(
final HttpParams params,
final SchemeRegistry sr) {
super(params, sr);
}
@Override
protected ClientConnectionOperator createConnectionOperator(
final SchemeRegistry sr) {
return new MyClientConnectionOperator(sr);
}
}

6.2 有狀態的HTTP連接

HTTP規範假設session狀態信息通常是以HTTP cookie格式嵌入在HTTP報文中的,因此HTTP連接通常是無狀態的,這個假設在現實生活中通常是不對的。也有一些情況,當HTTP連接使用特定的用戶標識或特定的安全上下文來創建時,因此不能和其它用戶共享,只能由該用戶重用。這樣的有狀態的HTTP連接的示例就是NTLM認證連接和使用客戶端證書認證的SSL連接。

6.2.1 用戶令牌處理器

HttpClient依賴UserTokenHandler接口來決定給定的執行上下文是否是用戶指定的。如果這個上下文是用戶指定的或者如果上下文沒有包含任何資源或關於當前用戶指定詳情而是null,令牌對象由這個處理器返回,期望唯一地標識當前的用戶。用戶令牌將被用來保證用戶指定資源不會和其它用戶來共享或重用。

如果它可以從給定的執行上下文中來獲得,UserTokenHandler接口的默認實現是使用主類的一個實例來代表HTTP連接的狀態對象。UserTokenHandler將會使用基於如NTLM或開啓的客戶端認證SSL會話認證模式的用戶的主連接。如果二者都不可用,那麼就不會返回令牌。

如果默認的不能滿足它們的需要,用戶可以提供一個自定義的實現:
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.setUserTokenHandler(new UserTokenHandler() {
public Object getUserToken(HttpContext context) {
return context.getAttribute("my-token");
}
});

6.2.2 用戶令牌和執行上下文

在HTTP請求執行的過程中,HttpClient添加了下列和用戶標識相關的對象到執行上下文中:

'http.user-token':對象實例代表真實的用戶標識,通常期望Principle接口的實例。

我們可以在請求被執行後,通過檢查本地HTTP上下文的內容,發現是否用於執行請求的連接是有狀態的。
DefaultHttpClient httpclient = new DefaultHttpClient();
HttpContext localContext = new BasicHttpContext();
HttpGet httpget = new HttpGet("http://localhost:8080/");
HttpResponse response = httpclient.execute(httpget, localContext);
HttpEntity entity = response.getEntity();
if (entity != null) {
entity.consumeContent();
}
Object userToken = localContext.getAttribute(ClientContext.USER_TOKEN);
System.out.println(userToken);
6.2.2.1 持久化有狀態的連接
請注意帶有狀態對象的持久化連接僅當請求被執行時,相同狀態對象被綁定到執行上下文時可以被重用。所以,保證相同上下文重用於執行隨後的相同用戶,或用戶令牌綁定到之前請求執行上下文的HTTP請求是很重要的。
DefaultHttpClient httpclient = new DefaultHttpClient();
HttpContext localContext1 = new BasicHttpContext();
HttpGet httpget1 = new HttpGet("http://localhost:8080/");
HttpResponse response1 = httpclient.execute(httpget1, localContext1);
HttpEntity entity1 = response1.getEntity();
if (entity1 != null) {
entity1.consumeContent();
}
Principal principal = (Principal) localContext1.getAttribute(
ClientContext.USER_TOKEN);
HttpContext localContext2 = new BasicHttpContext();
localContext2.setAttribute(ClientContext.USER_TOKEN, principal);
HttpGet httpget2 = new HttpGet("http://localhost:8080/");
HttpResponse response2 = httpclient.execute(httpget2, localContext2);
HttpEntity entity2 = response2.getEntity();
if (entity2 != null) {
entity2.consumeContent();
}
轉自:http://www.cnblogs.com/loveyakamoz/archive/2011/07/21/2113251.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章