HttpClient 教程

前言 超文本傳輸協議(HTTP)也許是當今互聯網上使用的最重要的協議了。Web服務,有網絡功能的設備和網絡計算的發展,都持續擴展了HTTP協議的角色,超越了用戶使用的Web瀏覽器範疇,同時,也增長了須要HTTP協議支持的應用程序的數量。 儘管java.net包提供了基本經過HTTP訪問資源的功能,但它沒有提供全面的靈活性和其它不少應用程序須要的功能。HttpClient就是尋求彌補這項空白的組件,經過提供一個有效的,保持更新的,功能豐富的軟件包來實現客戶端最新的HTTP標準和建議。 爲擴展而設計,同時爲基本的HTTP協議提供強大的支持,HttpClient組件也許就是構建HTTP客戶端應用程序,好比web瀏覽器,web服務端,利用或擴展HTTP協議進行分佈式通訊的系統的開發人員的關注點。javascript

  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(); }html

第五章 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(); }java

相關文章
相關標籤/搜索