HttpClient 完整教程
前言
Http協議應該是網際網路中最重要的協議。持續增長的web服務、可聯網的家用電器等都在繼承並擴充著Http協議,向著瀏覽器之外的方向發展。
雖然jdk中的java.net包中提供了一些基本的方法,通過http協議來訪問網路資源,但是大多數場景下,它都不夠靈活和強大。HttpClient致力於填補這個空白,它可以提供有效的、最新的、功能豐富的包來實現http客戶端。
為了擴充,HttpClient即支援基本的http協議,還支援http-aware客戶端程式,如web瀏覽器,Webservice客戶端,以及利用or擴充http協議的分散式系統。
1、HttpClient的範圍/特性
- 是一個基於HttpCore的客戶端Http傳輸類庫
- 基於傳統的(阻塞)IO
- 內容無關
2、HttpClient不能做的事情
- HttpClient不是瀏覽器,它是一個客戶端http協議傳輸類庫。HttpClient被用來傳送和接受Http訊息。HttpClient不會處理http訊息的內容,不會進行javascript解析,不會關心content type,如果沒有明確設定,httpclient也不會對請求進行格式化、重定向url,或者其他任何和http訊息傳輸相關的功能。
第一章 基本概念
1.1. 請求執行
HttpClient最基本的功能就是執行Http方法。一個Http方法的執行涉及到一個或者多個Http請求/Http響應的互動,通常這個過程都會自動被HttpClient處理,對使用者透明。使用者只需要提供Http請求物件,HttpClient就會將http請求傳送給目標伺服器,並且接收伺服器的響應,如果http請求執行不成功,httpclient就會丟擲異樣。
下面是個很簡單的http請求執行的例子:
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpGet httpget = new HttpGet("http://localhost/");
- CloseableHttpResponse response = httpclient.execute(httpget);
- try {
- <...>
- } finally {
- response.close();
- }
1.1.1. HTTP請求
所有的Http請求都有一個請求行(request line),包括方法名、請求的URI和Http版本號。
HttpClient支援HTTP/1.1這個版本定義的所有Http方法:GET
,HEAD
,POST
,PUT
,DELETE
,TRACE和
OPTIONS。對於每一種http方法,HttpClient都定義了一個相應的類:
HttpGet,
HttpHead,
HttpPost,
HttpPut,
HttpDelete,
HttpTrace和
HttpOpquertions。
Request-URI即統一資源定位符,用來標明Http請求中的資源。Http request URIs包含協議名、主機名、主機埠(可選)、資源路徑、query(可選)和片段資訊(可選)。
- HttpGet httpget = new HttpGet(
- "http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient提供URIBuilder
工具類來簡化URIs的建立和修改過程。
- URI uri = new URIBuilder()
- .setScheme("http")
- .setHost("www.google.com")
- .setPath("/search")
- .setParameter("q", "httpclient")
- .setParameter("btnG", "Google Search")
- .setParameter("aq", "f")
- .setParameter("oq", "")
- .build();
- 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請求後,就會對其進行解析,然後把響應發給客戶端,這個響應就是HTTP response.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訊息可以包含一系列的訊息頭,用來對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"
- 2
最有效的獲取指定型別的訊息頭的方法還是使用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"
HeaderIterator也提供非常便捷的方式,將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實體,這個http實體既可以是http請求,也可以是http響應的。Http實體,可以在某些http請求或者響應中發現,但不是必須的。Http規範中定義了兩種包含請求的方法:POST和PUT。HTTP響應一般會包含一個內容實體。當然這條規則也有異常情況,如Head方法的響應,204沒有內容,304沒有修改或者205內容資源重置。
HttpClient根據來源的不同,劃分了三種不同的Http實體內容。
- streamed流式: 內容是通過流來接受或者在執行中產生。特別是,streamed這一類包含從http響應中獲取的實體內容。一般說來,streamed實體是不可重複的。
- self-contained自我包含式:內容在記憶體中或通過獨立的連線或其它實體中獲得。self-contained型別的實體內容通常是可重複的。這種型別的實體通常用於關閉http請求。
- wrapping包裝式: 這種型別的內容是從另外的http實體中獲取的。
當從Http響應中讀取內容時,上面的三種區分對於連線管理器來說是非常重要的。對於由應用程式建立而且只使用HttpClient傳送的請求實體,streamed和self-contained兩種型別的不同就不那麼重要了。這種情況下,建議考慮如streamed流式這種不能重複的實體,和可以重複的self-contained自我包含式實體。
1.1.4.1. 可重複的實體
一個實體是可重複的,也就是說它的包含的內容可以被多次讀取。這種多次讀取只有self contained(自包含)的實體能做到(比如ByteArrayEntity
或者StringEntity
)。
1.1.4.2. 使用Http實體
由於一個Http實體既可以表示二進位制內容,又可以表示文字內容,所以Http實體要支援字元編碼(為了支援後者,即文字內容)。
當需要執行一個完整內容的Http請求或者Http請求已經成功,伺服器要傳送響應到客戶端時,Http實體就會被建立。
如果要從Http實體中讀取內容,我們可以利用HttpEntity
類的getContent
方法來獲取實體的輸入流(java.io.InputStream
),或者利用HttpEntity
類的writeTo(OutputStream)
方法來獲取輸出流,這個方法會把所有的內容寫入到給定的流中。
當實體類已經被接受後,我們可以利用HttpEntity
類的getContentType()
和getContentLength()
方法來讀取Content-Type
和Content-Length
兩個頭訊息(如果有的話)。由於Content-Type
包含mime-types的字元編碼,比如text/plain或者text/html,HttpEntity
類的getContentEncoding()
方法就是讀取這個編碼的。如果頭資訊不存在,getContentLength()
會返回-1,getContentType()
會返回NULL。如果Content-Type
資訊存在,就會返回一個Header
類。
當為傳送訊息建立Http實體時,需要同時附加meta資訊。
- StringEntity myEntity = new StringEntity("important message",
- ContentType.create("text/plain", "UTF-8"));
- System.out.println(myEntity.getContentType());
- System.out.println(myEntity.getContentLength());
- System.out.println(EntityUtils.toString(myEntity));
- System.out.println(EntityUtils.toByteArray(myEntity).length);
上述程式碼會在控制檯輸出:
- Content-Type: text/plain; charset=utf-8
- 17
- important message
- 17
1.1.5. 確保底層的資源連線被釋放
為了確保系統資源被正確地釋放,我們要麼管理Http實體的內容流、要麼關閉Http響應。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpGet httpget = new HttpGet("http://localhost/");
- CloseableHttpResponse response = httpclient.execute(httpget);
- try {
- HttpEntity entity = response.getEntity();
- if (entity != null) {
- InputStream instream = entity.getContent();
- try {
- // do something useful
- } finally {
- instream.close();
- }
- }
- } finally {
- response.close();
- }
關閉Http實體內容流和關閉Http響應的區別在於,前者通過消耗掉Http實體內容來保持相關的http連線,然後後者會立即關閉、丟棄http連線。
請注意HttpEntity
的writeTo(OutputStream)
方法,當Http實體被寫入到OutputStream後,也要確保釋放系統資源。如果這個方法內呼叫了HttpEntity
的getContent()
方法,那麼它會有一個java.io.InpputStream
的例項,我們需要在finally中關閉這個流。
但是也有這樣的情況,我們只需要獲取Http響應內容的一小部分,而獲取整個內容並、實現連線的可重複性代價太大,這時我們可以通過關閉響應的方式來關閉內容輸入、輸出流。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpGet httpget = new HttpGet("http://localhost/");
- CloseableHttpResponse response = httpclient.execute(httpget);
- try {
- HttpEntity entity = response.getEntity();
- if (entity != null) {
- InputStream instream = entity.getContent();
- int byteOne = instream.read();
- int byteTwo = instream.read();
- // Do not need the rest
- }
- } finally {
- response.close();
- }
上面的程式碼執行後,連線變得不可用,所有的資源都將被釋放。
1.1.6. 消耗HTTP實體內容
HttpClient推薦使用HttpEntity
的getConent()
方法或者HttpEntity
的writeTo(OutputStream)
方法來消耗掉Http實體內容。HttpClient也提供了EntityUtils
這個類,這個類提供一些靜態方法可以更容易地讀取Http實體的內容和資訊。和以java.io.InputStream
流讀取內容的方式相比,EntityUtils提供的方法可以以字串或者位元組陣列的形式讀取Http實體。但是,強烈不推薦使用EntityUtils
這個類,除非目標伺服器發出的響應是可信任的,並且http響應實體的長度不會過大。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpGet httpget = new HttpGet("http://localhost/");
- CloseableHttpResponse response = httpclient.execute(httpget);
- try {
- 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
- }
- }
- } finally {
- response.close();
- }
有些情況下,我們希望可以重複讀取Http實體的內容。這就需要把Http實體內容快取在記憶體或者磁碟上。最簡單的方法就是把Http Entity轉化成BufferedHttpEntity
,這樣就把原Http實體的內容緩衝到了記憶體中。後面我們就可以重複讀取BufferedHttpEntity中的內容。
- CloseableHttpResponse response = <...>
- HttpEntity entity = response.getEntity();
- if (entity != null) {
- entity = new BufferedHttpEntity(entity);
- }
1.1.7. 建立HTTP實體內容
HttpClient提供了一些類,這些類可以通過http連線高效地輸出Http實體內容。HttpClient提供的這幾個類涵蓋的常見的資料型別,如String,byte陣列,輸入流,和檔案型別:StringEntity
,ByteArrayEntity
,InputStreamEntity
,FileEntity
。
- File file = new File("somefile.txt");
- FileEntity entity = new FileEntity(file,
- ContentType.create("text/plain", "UTF-8"));
- HttpPost httppost = new HttpPost("http://localhost/action.do");
- httppost.setEntity(entity);
InputStreamEntity
只能從下層的資料流中讀取一次,所以它是不能重複的。推薦,通過繼承HttpEntity
這個自包含的類來自定義HttpEntity類,而不是直接使用InputStreamEntity
這個類。FileEntity
就是一個很好的起點(FileEntity就是繼承的HttpEntity)。
1.7.1.1. HTML表單
很多應用程式需要模擬提交Html表單的過程,舉個例子,登陸一個網站或者將輸入內容提交給伺服器。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, Consts.UTF_8);
- HttpPost httppost = new HttpPost("http://localhost/handler.do");
- httppost.setEntity(entity);
UrlEncodedFormEntity
例項會使用所謂的Url編碼的方式對我們的引數進行編碼,產生的結果如下:
- param1=value1&m2=value2
1.1.7.2. 內容分塊
一般來說,推薦讓HttpClient自己根據Http訊息傳遞的特徵來選擇最合適的傳輸編碼。當然,如果非要手動控制也是可以的,可以通過設定HttpEntity
的setChunked()
為true。請注意:HttpClient僅會將這個引數看成是一個建議。如果Http的版本(如http 1.0)不支援內容分塊,那麼這個引數就會被忽略。
- StringEntity entity = new StringEntity("important message",
- ContentType.create("plain/text", Consts.UTF_8));
- entity.setChunked(true);
- HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
- httppost.setEntity(entity);
1.1.8.RESPONSE HANDLERS
最簡單也是最方便的處理http響應的方法就是使用ResponseHandler
介面,這個介面中有handleResponse(HttpResponse response)
方法。使用這個方法,使用者完全不用關心http連線管理器。當使用ResponseHandler
時,HttpClient會自動地將Http連線釋放給Http管理器,即使http請求失敗了或者丟擲了異常。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpGet httpget = new HttpGet("http://localhost/json");
- ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() {
- @Override
- public JsonObject handleResponse(
- final HttpResponse response) throws IOException {
- StatusLine statusLine = response.getStatusLine();
- HttpEntity entity = response.getEntity();
- if (statusLine.getStatusCode() >= 300) {
- throw new HttpResponseException(
- statusLine.getStatusCode(),
- statusLine.getReasonPhrase());
- }
- if (entity == null) {
- throw new ClientProtocolException("Response contains no content");
- }
- Gson gson = new GsonBuilder().create();
- ContentType contentType = ContentType.getOrDefault(entity);
- Charset charset = contentType.getCharset();
- Reader reader = new InputStreamReader(entity.getContent(), charset);
- return gson.fromJson(reader, MyJsonObject.class);
- }
- };
- MyJsonObject myjson = client.execute(httpget, rh);
1.2. HttpClient介面
對於Http請求執行過程來說,HttpClient
的介面有著必不可少的作用。HttpClient
介面沒有對Http請求的過程做特別的限制和詳細的規定,連線管理、狀態管理、授權資訊和重定向處理這些功能都單獨實現。這樣使用者就可以更簡單地擴充介面的功能(比如快取響應內容)。
一般說來,HttpClient實際上就是一系列特殊的handler或者說策略介面的實現,這些handler(測試介面)負責著處理Http協議的某一方面,比如重定向、認證處理、有關連線永續性和keep alive持續時間的決策。這樣就允許使用者使用自定義的引數來代替預設配置,實現個性化的功能。
- ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
- @Override
- public long getKeepAliveDuration(
- HttpResponse response,
- HttpContext context) {
- long keepAlive = super.getKeepAliveDuration(response, context);
- if (keepAlive == -1) {
- // Keep connections alive 5 seconds if a keep-alive value
- // has not be explicitly set by the server
- keepAlive = 5000;
- }
- return keepAlive;
- }
- };
- CloseableHttpClient httpclient = HttpClients.custom()
- .setKeepAliveStrategy(keepAliveStrat)
- .build();
1.2.1.HTTPCLIENT的執行緒安全性
HttpClient
已經實現了執行緒安全。所以希望使用者在例項化HttpClient時,也要支援為多個請求使用。
1.2.2.HTTPCLIENT的記憶體分配
當一個CloseableHttpClient
的例項不再被使用,並且它的作用範圍即將失效,和它相關的連線必須被關閉,關閉方法可以呼叫CloseableHttpClient
的close()
方法。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- try {
- <...>
- } finally {
- httpclient.close();
- }
1.3.Http執行上下文
最初,Http被設計成一種無狀態的、面向請求-響應的協議。然而,在實際使用中,我們希望能夠在一些邏輯相關的請求-響應中,保持狀態資訊。為了使應用程式可以保持Http的持續狀態,HttpClient允許http連線在特定的Http上下文中執行。如果在持續的http請求中使用了同樣的上下文,那麼這些請求就可以被分配到一個邏輯會話中。HTTP上下文就和一個java.util.Map<String, Object>
功能類似。它實際上就是一個任意命名的值的集合。應用程式可以在Http請求執行前填充上下文的值,也可以在請求執行完畢後檢查上下文。
HttpContext
可以包含任意型別的物件,因此如果在多執行緒中共享上下文會不安全。推薦每個執行緒都只包含自己的http上下文。
在Http請求執行的過程中,HttpClient會自動新增下面的屬性到Http上下文中:
HttpConnection
的例項,表示客戶端與伺服器之間的連線HttpHost
的例項,表示要連線的目標伺服器HttpRoute
的例項,表示全部的連線路由HttpRequest
的例項,表示Http請求。在執行上下文中,最終的HttpRequest物件會代表http訊息的狀態。Http/1.0和Http/1.1都預設使用相對的uri。但是如果使用了非隧道模式的代理伺服器,就會使用絕對路徑的uri。HttpResponse
的例項,表示Http響應java.lang.Boolean
物件,表示是否請求被成功的傳送給目標伺服器RequestConfig
物件,表示http request的配置資訊java.util.List<Uri>
物件,表示Http響應中的所有重定向地址
我們可以使用HttpClientContext
這個介面卡來簡化和上下文互動的過程。
- HttpContext context = <...>
- HttpClientContext clientContext = HttpClientContext.adapt(context);
- HttpHost target = clientContext.getTargetHost();
- HttpRequest request = clientContext.getRequest();
- HttpResponse response = clientContext.getResponse();
- RequestConfig config = clientContext.getRequestConfig();
同一個邏輯會話中的多個Http請求,應該使用相同的Http上下文來執行,這樣就可以自動地在http請求中傳遞會話上下文和狀態資訊。
在下面的例子中,我們在開頭設定的引數,會被儲存在上下文中,並且會應用到後續的http請求中。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- RequestConfig requestConfig = RequestConfig.custom()
- .setSocketTimeout(1000)
- .setConnectTimeout(1000)
- .build();
- HttpGet httpget1 = new HttpGet("http://localhost/1");
- httpget1.setConfig(requestConfig);
- CloseableHttpResponse response1 = httpclient.execute(httpget1, context);
- try {
- HttpEntity entity1 = response1.getEntity();
- } finally {
- response1.close();
- }
- HttpGet httpget2 = new HttpGet("http://localhost/2");
- CloseableHttpResponse response2 = httpclient.execute(httpget2, context);
- try {
- HttpEntity entity2 = response2.getEntity();
- } finally {
- response2.close();
- }
1.4. 異常處理
HttpClient會被丟擲兩種型別的異常,一種是java.io.IOException
,當遇到I/O異常時丟擲(socket超時,或者socket被重置);另一種是HttpException
,表示Http失敗,如Http協議使用不正確。通常認為,I/O錯誤時不致命、可修復的,而Http協議錯誤是致命了,不能自動修復的錯誤。
1.4.1.HTTP傳輸安全
Http協議不能滿足所有型別的應用場景,我們需要知道這點。Http是個簡單的面向協議的請求/響應的協議,當初它被設計用來支援靜態或者動態生成的內容檢索,之前從來沒有人想過讓它支援事務性操作。例如,Http伺服器成功接收、處理請求後,生成響應訊息,並且把狀態碼傳送給客戶端,這個過程是Http協議應該保證的。但是,如果客戶端由於讀取超時、取消請求或者系統崩潰導致接收響應失敗,伺服器不會回滾這一事務。如果客戶端重新傳送這個請求,伺服器就會重複的解析、執行這個事務。在一些情況下,這會導致應用程式的資料損壞和應用程式的狀態不一致。
即使Http當初設計是不支援事務操作,但是它仍舊可以作為傳輸協議為某些關鍵程式提供服務。為了保證Http傳輸層的安全性,系統必須保證應用層上的http方法的冪等性。
1.4.2.方法的冪等性
HTTP/1.1規範中是這樣定義冪等方法的,Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request。用其他話來說,應用程式需要正確地處理同一方法多次執行造成的影響。新增一個具有唯一性的id就能避免重複執行同一個邏輯請求,問題解決。
請知曉,這個問題不只是HttpClient才會有,基於瀏覽器的應用程式也會遇到Http方法不冪等的問題。
HttpClient預設把非實體方法get
、head
方法看做冪等方法,把實體方法post
、put
方法看做非冪等方法。
1.4.3.異常自動修復
預設情況下,HttpClient會嘗試自動修復I/O異常。這種自動修復僅限於修復幾個公認安全的異常。
- HttpClient不會嘗試修復任何邏輯或者http協議錯誤(即從HttpException衍生出來的異常)。
- HttpClient會自動再次傳送冪等的方法(如果首次執行失敗)。
- HttpClient會自動再次傳送遇到transport異常的方法,前提是Http請求仍舊保持著連線(例如http請求沒有全部傳送給目標伺服器,HttpClient會再次嘗試傳送)。
1.4.4.請求重試HANDLER
如果要自定義異常處理機制,我們需要實現HttpRequestRetryHandler
介面。
- HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
- public boolean retryRequest(
- IOException exception,
- int executionCount,
- HttpContext context) {
- if (executionCount >= 5) {
- // Do not retry if over max retry count
- return false;
- }
- if (exception instanceof InterruptedIOException) {
- // Timeout
- return false;
- }
- if (exception instanceof UnknownHostException) {
- // Unknown host
- return false;
- }
- if (exception instanceof ConnectTimeoutException) {
- // Connection refused
- return false;
- }
- if (exception instanceof SSLException) {
- // SSL handshake exception
- return false;
- }
- HttpClientContext clientContext = HttpClientContext.adapt(context);
- HttpRequest request = clientContext.getRequest();
- boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
- if (idempotent) {
- // Retry if the request is considered idempotent
- return true;
- }
- return false;
- }
- };
- CloseableHttpClient httpclient = HttpClients.custom()
- .setRetryHandler(myRetryHandler)
- .build();
1.5.終止請求
有時候由於目標伺服器負載過高或者客戶端目前有太多請求積壓,http請求不能在指定時間內執行完畢。這時候終止這個請求,釋放阻塞I/O的程式,就顯得很必要。通過HttpClient執行的Http請求,在任何狀態下都能通過呼叫HttpUriRequest
的abort()
方法來終止。這個方法是執行緒安全的,並且能在任何執行緒中呼叫。當Http請求被終止了,本執行緒(即使現在正在阻塞I/O)也會通過丟擲一個InterruptedIOException
異常,來釋放資源。
1.6. Http協議攔截器
HTTP協議攔截器是一種實現一個特定的方面的HTTP協議的程式碼程式。通常情況下,協議攔截器會將一個或多個頭訊息加入到接受或者傳送的訊息中。協議攔截器也可以操作訊息的內容實體—訊息內容的壓縮/解壓縮就是個很好的例子。通常,這是通過使用“裝飾”開發模式,一個包裝實體類用於裝飾原來的實體來實現。一個攔截器可以合併,形成一個邏輯單元。
協議攔截器可以通過共享資訊協作——比如處理狀態——通過HTTP執行上下文。協議攔截器可以使用Http上下文儲存一個或者多個連續請求的處理狀態。
通常,只要攔截器不依賴於一個特定狀態的http上下文,那麼攔截執行的順序就無所謂。如果協議攔截器有相互依賴關係,必須以特定的順序執行,那麼它們應該按照特定的順序加入到協議處理器中。
協議處理器必須是執行緒安全的。類似於servlets,協議攔截器不應該使用變數實體,除非訪問這些變數是同步的(執行緒安全的)。
下面是個例子,講述了本地的上下文時如何在連續請求中記錄處理狀態的:
- CloseableHttpClient httpclient = HttpClients.custom()
- .addInterceptorLast(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()));
- }
- })
- .build();
- AtomicInteger count = new AtomicInteger(1);
- HttpClientContext localContext = HttpClientContext.create();
- localContext.setAttribute("count", count);
- HttpGet httpget = new HttpGet("http://localhost/");
- for (int i = 0; i < 10; i++) {
- CloseableHttpResponse response = httpclient.execute(httpget, localContext);
- try {
- HttpEntity entity = response.getEntity();
- } finally {
- response.close();
- }
- }
上面程式碼在傳送http請求時,會自動新增Count這個header,可以使用wireshark抓包檢視。
1.7.1. 重定向處理
HttpClient會自動處理所有型別的重定向,除了那些Http規範明確禁止的重定向。See Other (status code 303) redirects on POST and PUT requests are converted to GET requests as required by the HTTP specification. 我們可以使用自定義的重定向策略來放鬆Http規範對Post方法重定向的限制。
- LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
- CloseableHttpClient httpclient = HttpClients.custom()
- .setRedirectStrategy(redirectStrategy)
- .build();
HttpClient在請求執行過程中,經常需要重寫請求的訊息。 HTTP/1.0和HTTP/1.1都預設使用相對的uri路徑。同樣,原始的請求可能會被一次或者多次的重定向。最終結對路徑的解釋可以使用最初的請求和上下文。URIUtils
類的resolve
方法可以用於將攔截的絕對路徑構建成最終的請求。這個方法包含了最後一個分片識別符號或者原始請求。
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpClientContext context = HttpClientContext.create();
- HttpGet httpget = new HttpGet("http://localhost:8080/");
- CloseableHttpResponse response = httpclient.execute(httpget, context);
- try {
- HttpHost target = context.getTargetHost();
- List<URI> redirectLocations = context.getRedirectLocations();
- URI location = URIUtils.resolve(httpget.getURI(), target, redirectLocations);
- System.out.println("Final HTTP location: " + location.toASCIIString());
- // Expected to be an absolute URI
- } finally {
- response.close();
- }
第二章 連線管理
2.1.持久連線
兩個主機建立連線的過程是很複雜的一個過程,涉及到多個資料包的交換,並且也很耗時間。Http連線需要的三次握手開銷很大,這一開銷對於比較小的http訊息來說更大。但是如果我們直接使用已經建立好的http連線,這樣花費就比較小,吞吐率更大。
HTTP/1.1預設就支援Http連線複用。相容HTTP/1.0的終端也可以通過宣告來保持連線,實現連線複用。HTTP代理也可以在一定時間內保持連線不釋放,方便後續向這個主機傳送http請求。這種保持連線不釋放的情況實際上是建立的持久連線。HttpClient也支援持久連線。
2.2.HTTP連線路由
HttpClient既可以直接、又可以通過多箇中轉路由(hops)和目標伺服器建立連線。HttpClient把路由分為三種plain(明文 ),tunneled(隧道)和layered(分層)。隧道連線中使用的多箇中間代理被稱作代理鏈。
客戶端直接連線到目標主機或者只通過了一箇中間代理,這種就是Plain路由。客戶端通過第一個代理建立連線,通過代理鏈tunnelling,這種情況就是Tunneled路由。不通過中間代理的路由不可能時tunneled路由。客戶端在一個已經存在的連線上進行協議分層,這樣建立起來的路由就是layered路由。協議只能在隧道—>目標主機,或者直接連線(沒有代理),這兩種鏈路上進行分層。
2.2.1.路由計算
RouteInfo
介面包含了資料包傳送到目標主機過程中,經過的路由資訊。HttpRoute
類繼承了RouteInfo
介面,是RouteInfo
的具體實現,這個類是不允許修改的。HttpTracker
類也實現了RouteInfo
介面,它是可變的,HttpClient會在內部使用這個類來探測到目標主機的剩餘路由。HttpRouteDirector
是個輔助類,可以幫助計算資料包的下一步路由資訊。這個類也是在HttpClient內部使用的。
HttpRoutePlanner
介面可以用來表示基於http上下文情況下,客戶端到伺服器的路由計算策略。HttpClient有兩個HttpRoutePlanner
的實現類。SystemDefaultRoutePlanner
這個類基於java.net.ProxySelector
,它預設使用jvm的代理配置資訊,這個配置資訊一般來自系統配置或者瀏覽器配置。DefaultProxyRoutePlanner
這個類既不使用java本身的配置,也不使用系統或者瀏覽器的配置。它通常通過預設代理來計算路由資訊。
2.2.2. 安全的HTTP連線
為了防止通過Http訊息傳遞的資訊不被未授權的第三方獲取、截獲,Http可以使用SSL/TLS協議來保證http傳輸安全,這個協議是當前使用最廣的。當然也可以使用其他的加密技術。但是通常情況下,Http資訊會在加密的SSL/TLS連線上進行傳輸。
2.3. HTTP連線管理器
2.3.1. 管理連線和連線管理器
Http連線是複雜,有狀態的,執行緒不安全的物件,所以它必須被妥善管理。一個Http連線在同一時間只能被一個執行緒訪問。HttpClient使用一個叫做Http連線管理器的特殊實體類來管理Http連線,這個實體類要實現HttpClientConnectionManager
介面。Http連線管理器在新建http連線時,作為工廠類;管理持久http連線的生命週期;同步持久連線(確保執行緒安全,即一個http連線同一時間只能被一個執行緒訪問)。Http連線管理器和ManagedHttpClientConnection
的例項類一起發揮作用,ManagedHttpClientConnection
實體類可以看做http連線的一個代理伺服器,管理著I/O操作。如果一個Http連線被釋放或者被它的消費者明確表示要關閉,那麼底層的連線就會和它的代理進行分離,並且該連線會被交還給連線管理器。這是,即使服務消費者仍然持有代理的引用,它也不能再執行I/O操作,或者更改Http連線的狀態。
下面的程式碼展示瞭如何從連線管理器中取得一個http連線:
- HttpClientContext context = HttpClientContext.create();
- HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
- HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
- // 獲取新的連線. 這裡可能耗費很多時間
- ConnectionRequest connRequest = connMrg.requestConnection(route, null);
- // 10秒超時
- HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
- try {
- // 如果建立連線失敗
- if (!conn.isOpen()) {
- // establish connection based on its route info
- connMrg.connect(conn, route, 1000, context);
- // and mark it as route complete
- connMrg.routeComplete(conn, route, context);
- }
- // 進行自己的操作.
- } finally {
- connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
- }
如果要終止連線,可以呼叫ConnectionRequest
的cancel()
方法。這個方法會解鎖被ConnectionRequest
類get()
方法阻塞的執行緒。
2.3.2.簡單連線管理器
BasicHttpClientConnectionManager
是個簡單的連線管理器,它一次只能管理一個連線。儘管這個類是執行緒安全的,它在同一時間也只能被一個執行緒使用。BasicHttpClientConnectionManager
會盡量重用舊的連線來傳送後續的請求,並且使用相同的路由。如果後續請求的路由和舊連線中的路由不匹配,BasicHttpClientConnectionManager
就會關閉當前連線,使用請求中的路由重新建立連線。如果當前的連線正在被佔用,會丟擲java.lang.IllegalStateException
異常。
2.3.3.連線池管理器
相對BasicHttpClientConnectionManager
來說,PoolingHttpClientConnectionManager
是個更復雜的類,它管理著連線池,可以同時為很多執行緒提供http連線請求。Connections are pooled on a per route basis.當請求一個新的連線時,如果連線池有有可用的持久連線,連線管理器就會使用其中的一個,而不是再建立一個新的連線。
PoolingHttpClientConnectionManager
維護的連線數在每個路由基礎和總數上都有限制。預設,每個路由基礎上的連線不超過2個,總連線數不能超過20。在實際應用中,這個限制可能會太小了,尤其是當伺服器也使用Http協議時。
下面的例子演示瞭如果調整連線池的引數:
- PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
- // Increase max total connection to 200
- cm.setMaxTotal(200);
- // Increase default max connection per route to 20
- cm.setDefaultMaxPerRoute(20);
- // Increase max connections for localhost:80 to 50
- HttpHost localhost = new HttpHost("locahost", 80);
- cm.setMaxPerRoute(new HttpRoute(localhost), 50);
- CloseableHttpClient httpClient = HttpClients.custom()
- .setConnectionManager(cm)
- .build();
2.3.4.關閉連線管理器
當一個HttpClient的例項不在使用,或者已經脫離它的作用範圍,我們需要關掉它的連線管理器,來關閉掉所有的連線,釋放掉這些連線佔用的系統資源。
- CloseableHttpClient httpClient = <...>
- httpClient.close();
2.4.多執行緒請求執行
當使用了請求連線池管理器(比如PoolingClientConnectionManager
)後,HttpClient就可以同時執行多個執行緒的請求了。
PoolingClientConnectionManager
會根據它的配置來分配請求連線。如果連線池中的所有連線都被佔用了,那麼後續的請求就會被阻塞,直到有連線被釋放回連線池中。為了防止永遠阻塞的情況發生,我們可以把http.conn-manager.timeout
的值設定成一個整數。如果在超時時間內,沒有可用連線,就會丟擲ConnectionPoolTimeoutException
異常。
- PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
- CloseableHttpClient httpClient = HttpClients.custom()
- .setConnectionManager(cm)
- .build();
- // URIs to perform GETs on
- String[] urisToGet = {
- "http://www.domain1.com/",
- "http://www.domain2.com/",
- "http://www.domain3.com/",
- "http://www.domain4.com/"
- };
- // create a thread for each 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);
- }
- // start the threads
- for (int j = 0; j < threads.length; j++) {
- threads[j].start();
- }
- // join the threads
- for (int j = 0; j < threads.length; j++) {
- threads[j].join();
- }
即使HttpClient的例項是執行緒安全的,可以被多個執行緒共享訪問,但是仍舊推薦每個執行緒都要有自己專用例項的HttpContext。
下面是GetThread類的定義:
- static class GetThread extends Thread {
- private final CloseableHttpClient httpClient;
- private final HttpContext context;
- private final HttpGet httpget;
- public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
- this.httpClient = httpClient;
- this.context = HttpClientContext.create();
- this.httpget = httpget;
- }
- @Override
- public void run() {
- try {
- CloseableHttpResponse response = httpClient.execute(
- httpget, context);
- try {
- HttpEntity entity = response.getEntity();
- } finally {
- response.close();
- }
- } catch (ClientProtocolException ex) {
- // Handle protocol errors
- } catch (IOException ex) {
- // Handle I/O errors
- }
- }
- }
2.5. 連線回收策略
經典阻塞I/O模型的一個主要缺點就是隻有當組側I/O時,socket才能對I/O事件做出反應。當連線被管理器收回後,這個連線仍然存活,但是卻無法監控socket的狀態,也無法對I/O事件做出反饋。如果連線被伺服器端關閉了,客戶端監測不到連線的狀態變化(也就無法根據連線狀態的變化,關閉本地的socket)。
HttpClient為了緩解這一問題造成的影響,會在使用某個連線前,監測這個連線是否已經過時,如果伺服器端關閉了連線,那麼連線就會失效。這種過時檢查並不是100%有效,並且會給每個請求增加10到30毫秒額外開銷。唯一一個可行的,且does not involve a one thread per socket model for idle connections的解決辦法,是建立一個監控執行緒,來專門回收由於長時間不活動而被判定為失效的連線。這個監控執行緒可以週期性的呼叫ClientConnectionManager
類的closeExpiredConnections()
方法來關閉過期的連線,回收連線池中被關閉的連線。它也可以選擇性的呼叫ClientConnectionManager
類的closeIdleConnections()
方法來關閉一段時間內不活動的連線。
- public static class IdleConnectionMonitorThread extends Thread {
- private final HttpClientConnectionManager connMgr;
- private volatile boolean shutdown;
- public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
- super();
- this.connMgr = connMgr;
- }
- @Override
- public void run() {
- try {
- while (!shutdown) {
- synchronized (this) {
- wait(5000);
- // Close expired connections
- connMgr.closeExpiredConnections();
- // Optionally, close connections
- // that have been idle longer than 30 sec
- connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
- }
- }
- } catch (InterruptedException ex) {
- // terminate
- }
- }
- public void shutdown() {
- shutdown = true;
- synchronized (this) {
- notifyAll();
- }
- }
- }
2.6. 連線存活策略
Http規範沒有規定一個持久連線應該保持存活多久。有些Http伺服器使用非標準的Keep-Alive
頭訊息和客戶端進行互動,伺服器端會保持數秒時間內保持連線。HttpClient也會利用這個頭訊息。如果伺服器返回的響應中沒有包含Keep-Alive
頭訊息,HttpClient會認為這個連線可以永遠保持。然而,很多伺服器都會在不通知客戶端的情況下,關閉一定時間內不活動的連線,來節省伺服器資源。在某些情況下預設的策略顯得太樂觀,我們可能需要自定義連線存活策略。
- ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
- public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
- // Honor 'keep-alive' header
- HeaderElementIterator it = new BasicHeaderElementIterator(
- response.headerIterator(HTTP.CONN_KEEP_ALIVE));
- while (it.hasNext()) {
- HeaderElement he = it.nextElement();
- String param = he.getName();
- String value = he.getValue();
- if (value != null && param.equalsIgnoreCase("timeout")) {
- try {
- return Long.parseLong(value) * 1000;
- } catch(NumberFormatException ignore) {
- }
- }
- }
- HttpHost target = (HttpHost) context.getAttribute(
- HttpClientContext.HTTP_TARGET_HOST);
- if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
- // Keep alive for 5 seconds only
- return 5 * 1000;
- } else {
- // otherwise keep alive for 30 seconds
- return 30 * 1000;
- }
- }
- };
- CloseableHttpClient client = HttpClients.custom()
- .setKeepAliveStrategy(myStrategy)
- .build();
2.7.socket連線工廠
Http連線使用java.net.Socket
類來傳輸資料。這依賴於ConnectionSocketFactory
介面來建立、初始化和連線socket。這樣也就允許HttpClient的使用者在程式碼執行時,指定socket初始化的程式碼。PlainConnectionSocketFactory
是預設的建立、初始化明文socket(不加密)的工廠類。
建立socket和使用socket連線到目標主機這兩個過程是分離的,所以我們可以在連線發生阻塞時,關閉socket連線。
- HttpClientContext clientContext = HttpClientContext.create();
- PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();
- Socket socket = sf.createSocket(clientContext);
- int timeout = 1000; //ms
- HttpHost target = new HttpHost("localhost");
- InetSocketAddress remoteAddress = new InetSocketAddress(
- InetAddress.getByAddress(new byte[] {127,0,0,1}), 80);
- sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);
2.7.1. 安全SOCKET分層
LayeredConnectionSocketFactory
是ConnectionSocketFactory
的擴充介面。分層socket工廠類可以在明文socket的基礎上建立socket連線。分層socket主要用於在代理伺服器之間建立安全socket。HttpClient使用SSLSocketFactory
這個類實現安全socket,SSLSocketFactory
實現了SSL/TLS分層。請知曉,HttpClient沒有自定義任何加密演算法。它完全依賴於Java加密標準(JCE)和安全套接字(JSEE)擴充。
2.7.2. 整合連線管理器
自定義的socket工廠類可以和指定的協議(Http、Https)聯絡起來,用來建立自定義的連線管理器。
- ConnectionSocketFactory plainsf = <...>
- LayeredConnectionSocketFactory sslsf = <...>
- Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
- .register("http", plainsf)
- .register("https", sslsf)
- .build();
- HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);
- HttpClients.custom()
- .setConnectionManager(cm)
- .build();
2.7.3. SSL/TLS定製
HttpClient使用SSLSocketFactory
來建立ssl連線。SSLSocketFactory
允許使用者高度定製。它可以接受javax.net.ssl.SSLContext
這個類的例項作為引數,來建立自定義的ssl連線。
- HttpClientContext clientContext = HttpClientContext.create();
- KeyStore myTrustStore = <...>
- SSLContext sslContext = SSLContexts.custom()
- .useTLS()
- .loadTrustMaterial(myTrustStore)
- .build();
- SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
2.7.4. 域名驗證
除了信任驗證和在ssl/tls協議層上進行客戶端認證,HttpClient一旦建立起連線,就可以選擇性驗證目標域名和儲存在X.509證照中的域名是否一致。這種驗證可以為伺服器信任提供額外的保障。X509HostnameVerifier
介面代表主機名驗證的策略。在HttpClient中,X509HostnameVerifier
有三個實現類。重要提示:主機名有效性驗證不應該和ssl信任驗證混為一談。
StrictHostnameVerifier
: 嚴格的主機名驗證方法和java 1.4,1.5,1.6驗證方法相同。和IE6的方式也大致相同。這種驗證方式符合RFC 2818萬用字元。The hostname must match either the first CN, or any of the subject-alts. A wildcard can occur in the CN, and in any of the subject-alts.BrowserCompatHostnameVerifier
: 這種驗證主機名的方法,和Curl及firefox一致。The hostname must match either the first CN, or any of the subject-alts. A wildcard can occur in the CN, and in any of the subject-alts.StrictHostnameVerifier
和BrowserCompatHostnameVerifier
方式唯一不同的地方就是,帶有萬用字元的域名(比如*.yeetrack.com),BrowserCompatHostnameVerifier
方式在匹配時會匹配所有的的子域名,包括 a.b.yeetrack.com .AllowAllHostnameVerifier
: 這種方式不對主機名進行驗證,驗證功能被關閉,是個空操作,所以它不會丟擲javax.net.ssl.SSLException
異常。HttpClient預設使用BrowserCompatHostnameVerifier
的驗證方式。如果需要,我們可以手動執行驗證方式。- SSLContext sslContext = SSLContexts.createSystemDefault();
- SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
- sslContext,
- SSLConnectionSocketFactory.STRICT_HOSTNAME_VERIFIER);
2.8. HttpClient代理伺服器配置
儘管,HttpClient支援複雜的路由方案和代理鏈,它同樣也支援直接連線或者只通過一跳的連線。
使用代理伺服器最簡單的方式就是,指定一個預設的proxy引數。
- HttpHost proxy = new HttpHost("someproxy", 8080);
- DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
- CloseableHttpClient httpclient = HttpClients.custom()
- .setRoutePlanner(routePlanner)
- .build();
我們也可以讓HttpClient去使用jre的代理伺服器。
- SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(
- ProxySelector.getDefault());
- CloseableHttpClient httpclient = HttpClients.custom()
- .setRoutePlanner(routePlanner)
- .build();
又或者,我們也可以手動配置RoutePlanner
,這樣就可以完全控制Http路由的過程。
- HttpRoutePlanner routePlanner = 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()));
- }
- };
- CloseableHttpClient httpclient = HttpClients.custom()
- .setRoutePlanner(routePlanner)
- .build();
- }
- }
第三章 Http狀態管理
最初,Http被設計成一個無狀態的,面向請求/響應的協議,所以它不能在邏輯相關的http請求/響應中保持狀態會話。由於越來越多的系統使用http協議,其中包括http從來沒有想支援的系統,比如電子商務系統。因此,http支援狀態管理就很必要了。
當時的web客戶端和伺服器軟體領先者,網景(netscape)公司,最先在他們的產品中支援http狀態管理,並且制定了一些專有規範。後來,網景通過發規範草案,規範了這一機制。這些努力促成 RFC standard track制定了標準的規範。但是,現在多數的應用的狀態管理機制都在使用網景公司的規範,而網景的規範和官方規定是不相容的。因此所有的瀏覽器開發這都被迫相容這兩種協議,從而導致協議的不統一。
3.1. Http cookies
所謂的Http cookie就是一個token或者很短的報文資訊,http代理和伺服器可以通過cookie來維持會話狀態。網景的工程師把它們稱作“magic cookie”。
HttpClient使用Cookie
介面來代表cookie。簡單說來,cookie就是一個鍵值對。一般,cookie也會包含版本號、域名、路徑和cookie有效期。
SetCookie
介面可以代表伺服器發給http代理的一個set-cookie響應頭,在瀏覽器中,這個set-cookie響應頭可以寫入cookie,以便保持會話狀態。SetCookie2
介面對SetCookie
介面進行了擴充,新增了Set-Cookie2
方法。
ClientCookie
介面繼承了Cookie
介面,並進行了功能擴充,比如它可以取出伺服器傳送過來的原始cookie的值。生成頭訊息是很重要的,因為只有當cookie被指定為Set-Cookie
或者Set-Cookie2
時,它才需要包括一些特定的屬性。
3.1.1 COOKIES版本
相容網景的規範,但是不相容官方規範的cookie,是版本0. 相容官方規範的版本,將會是版本1。版本1中的Cookie可能和版本0工作機制有差異。
下面的程式碼,建立了網景版本的Cookie:
- BasicClientCookie netscapeCookie = new BasicClientCookie("name", "value");
- netscapeCookie.setVersion(0);
- netscapeCookie.setDomain(".mycompany.com");
- netscapeCookie.setPath("/");
下面的程式碼,建立標準版本的Cookie。注意,標準版本的Cookie必須保留伺服器傳送過來的Cookie所有屬性。
- BasicClientCookie stdCookie = new BasicClientCookie("name", "value");
- stdCookie.setVersion(1);
- stdCookie.setDomain(".mycompany.com");
- stdCookie.setPath("/");
- stdCookie.setSecure(true);
- // Set attributes EXACTLY as sent by the server
- stdCookie.setAttribute(ClientCookie.VERSION_ATTR, "1");
- stdCookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");
下面的程式碼,建立了Set-Cookie2
相容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);
- // Set attributes EXACTLY as sent by the server
- 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
規範:
- Netscape draft: 這種符合網景公司指定的規範。但是儘量不要使用,除非一定要保證相容很舊的程式碼。
- Standard: RFC 2965 HTTP狀態管理規範
- Browser compatibility: 這種方式,儘量模仿常用的瀏覽器,如IE和firefox
- Best match: ‘Meta’ cookie specification that picks up a cookie policy based on the format of cookies sent with the HTTP response.它基本上將上面的幾種規範積聚到一個類中。
++ Ignore cookies: 忽略所有Cookie
強烈推薦使用Best Match匹配規則,讓HttpClient根據執行時環境自己選擇合適的規範。
3.3. 選擇Cookie策略
我們可以在建立Http client的時候指定Cookie測試,如果需要,也可以在執行http請求的時候,進行覆蓋指定。
- RequestConfig globalConfig = RequestConfig.custom()
- .setCookieSpec(CookieSpecs.BEST_MATCH)
- .build();
- CloseableHttpClient httpclient = HttpClients.custom()
- .setDefaultRequestConfig(globalConfig)
- .build();
- RequestConfig localConfig = RequestConfig.copy(globalConfig)
- .setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY)
- .build();
- HttpGet httpGet = new HttpGet("/");
- httpGet.setConfig(localConfig);
3.4. 自定義Cookie策略
如果我們要自定義Cookie測試,就要自己實現CookieSpec
介面,然後建立一個CookieSpecProvider
介面來新建、初始化自定義CookieSpec
介面,最後把CookieSpecProvider
註冊到HttpClient中。一旦我們註冊了自定義策略,就可以像其他標準策略一樣使用了。
- CookieSpecProvider easySpecProvider = new CookieSpecProvider() {
- public CookieSpec create(HttpContext context) {
- return new BrowserCompatSpec() {
- @Override
- public void validate(Cookie cookie, CookieOrigin origin)
- throws MalformedCookieException {
- // Oh, I am easy
- }
- };
- }
- };
- Registry<CookieSpecProvider> r = RegistryBuilder.<CookieSpecProvider>create()
- .register(CookieSpecs.BEST_MATCH,
- new BestMatchSpecFactory())
- .register(CookieSpecs.BROWSER_COMPATIBILITY,
- new BrowserCompatSpecFactory())
- .register("easy", easySpecProvider)
- .build();
- RequestConfig requestConfig = RequestConfig.custom()
- .setCookieSpec("easy")
- .build();
- CloseableHttpClient httpclient = HttpClients.custom()
- .setDefaultCookieSpecRegistry(r)
- .setDefaultRequestConfig(requestConfig)
- .build();
3.5. Cookie持久化
HttpClient可以使用任何儲存方式的cookie store,只要這個cookie store實現了CookieStore
介面。預設的CookieStore通過java.util.ArrayList
簡單實現了BasicCookieStore
。存在在BasicCookieStore
中的Cookie,當載體物件被當做垃圾回收掉後,就會丟失。如果必要,使用者可以自己實現更為複雜的方式。
- // Create a local instance of cookie store
- CookieStore cookieStore = new BasicCookieStore();
- // Populate cookies if needed
- BasicClientCookie cookie = new BasicClientCookie("name", "value");
- cookie.setVersion(0);
- cookie.setDomain(".mycompany.com");
- cookie.setPath("/");
- cookieStore.addCookie(cookie);
- // Set the store
- CloseableHttpClient httpclient = HttpClients.custom()
- .setDefaultCookieStore(cookieStore)
- .build();
3.6.HTTP狀態管理和執行上下文
在Http請求執行過程中,HttpClient會自動向執行上下文中新增下面的狀態管理物件:
Lookup
物件 代表實際的cookie規範registry。在當前上下文中的這個值優先於預設值。CookieSpec
物件 代表實際的Cookie規範。CookieOrigin
物件 代表實際的origin server的詳細資訊。CookieStore
物件 表示Cookie store。這個屬性集中的值會取代預設值。
本地的HttpContext
物件可以用來在Http請求執行前,自定義Http狀態管理上下文;或者測試http請求執行完畢後上下文的狀態。我們也可以在不同的執行緒中使用不同的執行上下文。我們在http請求層指定的cookie規範集和cookie store會覆蓋在http Client層級的預設值。
- CloseableHttpClient httpclient = <...>
- Lookup<CookieSpecProvider> cookieSpecReg = <...>
- CookieStore cookieStore = <...>
- HttpClientContext context = HttpClientContext.create();
- context.setCookieSpecRegistry(cookieSpecReg);
- context.setCookieStore(cookieStore);
- HttpGet httpget = new HttpGet("http://somehost/");
- CloseableHttpResponse response1 = httpclient.execute(httpget, context);
- <...>
- // Cookie origin details
- CookieOrigin cookieOrigin = context.getCookieOrigin();
- // Cookie spec used
- CookieSpec cookieSpec = context.getCookieSpec();
第四章 HTTP認證
HttpClient既支援HTTP標準規範定義的認證模式,又支援一些廣泛使用的非標準認證模式,比如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系統使用的一種憑據,包含username、password,還包括一系列其他的屬性,比如使用者所在的域名。在Microsoft 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. 認證方案
AutoScheme
介面表示一個抽象的面向挑戰/響應的認證方案。一個認證方案要支援下面的功能:
- 客戶端請求伺服器受保護的資源,伺服器會傳送過來一個chanllenge(挑戰),認證方案(Authentication scheme)需要解析、處理這個挑戰
- 為processed challenge提供一些屬性值:認證方案的型別,和此方案需要的一些引數,這種方案適用的範圍
- 使用給定的授權資訊生成授權字串;生成http請求,用來響應伺服器傳送來過的授權challenge
請注意:一個認證方案可能是有狀態的,因為它可能涉及到一系列的挑戰/響應。
HttpClient實現了下面幾種AutoScheme
:
- Basic: Basic認證方案是在RFC2617號文件中定義的。這種授權方案用明文來傳輸憑證資訊,所以它是不安全的。雖然Basic認證方案本身是不安全的,但是它一旦和TLS/SSL加密技術結合起來使用,就完全足夠了。
- Digest: Digest(摘要)認證方案是在RFC2617號文件中定義的。Digest認證方案比Basic方案安全多了,對於那些受不了Basic+TLS/SSL傳輸開銷的系統,digest方案是個不錯的選擇。
- NTLM: NTLM認證方案是個專有的認證方案,由微軟開發,並且針對windows平臺做了優化。NTLM被認為比Digest更安全。
- SPNEGO: SPNEGO(Simple and Protected GSSAPI Negotiation Mechanism)是GSSAPI的一個“偽機制”,它用來協商真正的認證機制。SPNEGO最明顯的用途是在微軟的HTTP協商認證機制擴充上。可協商的子機制包括NTLM、Kerberos。目前,HttpCLient只支援Kerberos機制。(原文:The negotiable sub-mechanisms include NTLM and Kerberos supported by Active Directory. At present HttpClient only supports the Kerberos sub-mechanism.)
4.3. 憑證 provider
憑證providers旨在維護一套使用者的憑證,當需要某種特定的憑證時,providers就應該能產生這種憑證。認證的具體內容包括主機名、埠號、realm name和認證方案名。當使用憑據provider的時候,我們可以很模糊的指定主機名、埠號、realm和認證方案,不用寫的很精確。因為,憑據provider會根據我們指定的內容,篩選出一個最匹配的方案。
只要我們自定義的憑據provider實現了CredentialsProvider
這個介面,就可以在HttpClient中使用。預設的憑據provider叫做BasicCredentialsProvider
,它使用java.util.HashMap
對CredentialsProvider
進行了簡單的實現。
- 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.4.HTTP授權和執行上下文
HttpClient依賴AuthState
類去跟蹤認證過程中的狀態的詳細資訊。在Http請求過程中,HttpClient建立兩個AuthState
例項:一個用於目標伺服器認證,一個用於代理伺服器認證。如果伺服器或者代理伺服器需要使用者的授權資訊,AuthScope
、AutoScheme
和認證資訊就會被填充到兩個AuthScope
例項中。通過對AutoState
的檢測,我們可以確定請求的授權型別,確定是否有匹配的AuthScheme
,確定憑據provider根據指定的授權型別是否成功生成了使用者的授權資訊。
在Http請求執行過程中,HttpClient會向執行上下文中新增下面的授權物件:
Lookup
物件,表示使用的認證方案。這個物件的值可以在本地上下文中進行設定,來覆蓋預設值。CredentialsProvider
物件,表示認證方案provider,這個物件的值可以在本地上下文中進行設定,來覆蓋預設值。AuthState
物件,表示目標伺服器的認證狀態,這個物件的值可以在本地上下文中進行設定,來覆蓋預設值。AuthState
物件,表示代理伺服器的認證狀態,這個物件的值可以在本地上下文中進行設定,來覆蓋預設值。AuthCache
物件,表示認證資料的快取,這個物件的值可以在本地上下文中進行設定,來覆蓋預設值。
我們可以在請求執行前,自定義本地HttpContext
物件來設定需要的http認證上下文;也可以在請求執行後,再檢測HttpContext
的狀態,來檢視授權是否成功。
- CloseableHttpClient httpclient = <...>
- CredentialsProvider credsProvider = <...>
- Lookup<AuthSchemeProvider> authRegistry = <...>
- AuthCache authCache = <...>
- HttpClientContext context = HttpClientContext.create();
- context.setCredentialsProvider(credsProvider);
- context.setAuthSchemeRegistry(authRegistry);
- context.setAuthCache(authCache);
- HttpGet httpget = new HttpGet("http://somehost/");
- CloseableHttpResponse response1 = httpclient.execute(httpget, context);
- <...>
- AuthState proxyAuthState = context.getProxyAuthState();
- System.out.println("Proxy auth state: " + proxyAuthState.getState());
- System.out.println("Proxy auth scheme: " + proxyAuthState.getAuthScheme());
- System.out.println("Proxy auth credentials: " + proxyAuthState.getCredentials());
- AuthState targetAuthState = context.getTargetAuthState();
- System.out.println("Target auth state: " + targetAuthState.getState());
- System.out.println("Target auth scheme: " + targetAuthState.getAuthScheme());
- System.out.println("Target auth credentials: " + targetAuthState.getCredentials());
4.5. 快取認證資料
從版本4.1開始,HttpClient就會自動快取驗證通過的認證資訊。但是為了使用這個快取的認證資訊,我們必須在同一個上下文中執行邏輯相關的請求。一旦超出該上下文的作用範圍,快取的認證資訊就會失效。
4.6. 搶先認證
HttpClient預設不支援搶先認證,因為一旦搶先認證被誤用或者錯用,會導致一系列的安全問題,比如會把使用者的認證資訊以明文的方式傳送給未授權的第三方伺服器。因此,需要使用者自己根據自己應用的具體環境來評估搶先認證帶來的好處和帶來的風險。
即使如此,HttpClient還是允許我們通過配置來啟用搶先認證,方法是提前填充認證資訊快取到上下文中,這樣,以這個上下文執行的方法,就會使用搶先認證。
- CloseableHttpClient httpclient = <...>
- HttpHost targetHost = new HttpHost("localhost", 80, "http");
- CredentialsProvider credsProvider = new BasicCredentialsProvider();
- credsProvider.setCredentials(
- new AuthScope(targetHost.getHostName(), targetHost.getPort()),
- new UsernamePasswordCredentials("username", "password"));
- // Create AuthCache instance
- AuthCache authCache = new BasicAuthCache();
- // Generate BASIC scheme object and add it to the local auth cache
- BasicScheme basicAuth = new BasicScheme();
- authCache.put(targetHost, basicAuth);
- // Add AuthCache to the execution context
- HttpClientContext context = HttpClientContext.create();
- context.setCredentialsProvider(credsProvider);
- context.setAuthCache(authCache);
- HttpGet httpget = new HttpGet("/");
- for (int i = 0; i < 3; i++) {
- CloseableHttpResponse response = httpclient.execute(
- targetHost, httpget, context);
- try {
- HttpEntity entity = response.getEntity();
- } finally {
- response.close();
- }
- }
4.7. NTLM認證
從版本4.1開始,HttpClient就全面支援NTLMv1、NTLMv2和NTLM2認證。當人我們可以仍舊使用外部的NTLM引擎(比如Samba開發的JCIFS庫)作為與Windows互操作性程式的一部分。
4.7.1. NTLM連線永續性
相比Basic
和Digest
認證,NTLM認證要明顯需要更多的計算開銷,效能影響也比較大。這也可能是微軟把NTLM協議設計成有狀態連線的主要原因之一。也就是說,NTLM連線一旦建立,使用者的身份就會在其整個生命週期和它相關聯。NTLM連線的狀態性使得連線永續性更加複雜,The stateful nature of NTLM connections makes connection persistence more complex, as for
the obvious reason persistent NTLM connections may not be re-used by users with a different user identity. HttpClient中標準的連線管理器就可以管理有狀態的連線。但是,同一會話中邏輯相關的請求,必須使用相同的執行上下文,這樣才能使用使用者的身份資訊。否則,HttpClient就會結束舊的連線,為了獲取被NTLM協議保護的資源,而為每個HTTP請求,建立一個新的Http連線。更新關於Http狀態連線的資訊,點選此處。
由於NTLM連線是有狀態的,一般推薦使用比較輕量級的方法來處罰NTLM認證(如GET、Head方法),然後使用這個已經建立的連線在執行相對重量級的方法,尤其是需要附件請求實體的請求(如POST、PUT請求)。
- CloseableHttpClient httpclient = <...>
- CredentialsProvider credsProvider = new BasicCredentialsProvider();
- credsProvider.setCredentials(AuthScope.ANY,
- new NTCredentials("user", "pwd", "myworkstation", "microsoft.com"));
- HttpHost target = new HttpHost("www.microsoft.com", 80, "http");
- // Make sure the same context is used to execute logically related requests
- HttpClientContext context = HttpClientContext.create();
- context.setCredentialsProvider(credsProvider);
- // Execute a cheap method first. This will trigger NTLM authentication
- HttpGet httpget = new HttpGet("/ntlm-protected/info");
- CloseableHttpResponse response1 = httpclient.execute(target, httpget, context);
- try {
- HttpEntity entity1 = response1.getEntity();
- } finally {
- response1.close();
- }
- // Execute an expensive method next reusing the same context (and connection)
- HttpPost httppost = new HttpPost("/ntlm-protected/form");
- httppost.setEntity(new StringEntity("lots and lots of data"));
- CloseableHttpResponse response2 = httpclient.execute(target, httppost, context);
- try {
- HttpEntity entity2 = response2.getEntity();
- } finally {
- response2.close();
- }
4.8. SPNEGO/Kerberos認證
SPNEGO(Simple and Protected GSSAPI Megotiation Mechanism),當雙方均不知道對方能使用/提供什麼協議的情況下,可以使用SP認證協議。這種協議在Kerberos認證方案中經常使用。It can wrap other mechanisms, however the current version in HttpClient is designed solely with Kerberos in mind.
4.8.1. 在HTTPCIENT中使用SPNEGO
SPNEGO認證方案相容Sun java 1.5及以上版本。但是強烈推薦jdk1.6以上。Sun的JRE提供的類就已經幾乎完全可以處理Kerberos和SPNEGO token。這就意味著,需要設定很多的GSS類。SpnegoScheme
是個很簡單的類,可以用它來handle marshalling the tokens and 讀寫正確的頭訊息。
最好的開始方法就是從示例程式中找到KerberosHttpClient.java
這個檔案,嘗試讓它執行起來。執行過程有可能會出現很多問題,但是如果人品比較高可能會順利一點。這個檔案會提供一些輸出,來幫我們除錯。
在Windows系統中,應該預設使用使用者的登陸憑據;當然我們也可以使用kinit
來覆蓋這個憑據,比如$JAVA_HOME\bin\kinit testuser@AD.EXAMPLE.NET
,這在我們測試和除錯的時候就顯得很有用了。如果想用回Windows預設的登陸憑據,刪除kinit建立的快取檔案即可。
確保在krb5.conf檔案中列出domain_realms
。這能解決很多不必要的問題。
4.8.2. 使用GSS/JAVA KERBEROS
下面的這份文件是針對Windows系統的,但是很多資訊同樣適合Unix。
org.ietf.jgss
這個類有很多的配置引數,這些引數大部分都在krb5.conf/krb5.ini
檔案中配置。更多的資訊,參考此處。
login.conf檔案
下面是一個基本的login.conf檔案,使用於Windows平臺的IIS和JBoss Negotiation模組。
系統配置檔案java.security.auth.login.config
可以指定login.conf
檔案的路徑。
login.conf
的內容可能會是下面的樣子:
- com.sun.security.jgss.login {
- com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
- };
- com.sun.security.jgss.initiate {
- com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
- };
- com.sun.security.jgss.accept {
- com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
- };
4.8.4. KRB5.CONF / KRB5.INI 檔案
如果沒有手動指定,系統會使用預設配置。如果要手動指定,可以在java.security.krb5.conf
中設定系統變數,指定krb5.conf
的路徑。krb5.conf
的內容可能是下面的樣子:
- [libdefaults]
- default_realm = AD.EXAMPLE.NET
- udp_preference_limit = 1
- [realms]
- AD.EXAMPLE.NET = {
- kdc = KDC.AD.EXAMPLE.NET
- }
- [domain_realms]
- .ad.example.net=AD.EXAMPLE.NET
- ad.example.net=AD.EXAMPLE.NET
4.8.5. WINDOWS詳細的配置
為了允許Windows使用當前使用者的tickets,javax.security.auth.useSubjectCredsOnly
這個系統變數應該設定成false
,並且需要在Windows登錄檔中新增allowtgtsessionkey
這個項,而且要allow session keys to be sent in the Kerberos Ticket-Granting Ticket.
Windows Server 2003和Windows 2000 SP4,配置如下:
- HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters
- Value Name: allowtgtsessionkey
- Value Type: REG_DWORD
- Value: 0x01
Windows XP SP2 配置如下:
- HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\
- Value Name: allowtgtsessionkey
- Value Type: REG_DWORD
- Value: 0x01
第五章 快速API
5.1. Easy to use facade API
HttpClient從4.2開始支援快速api。快速api僅僅實現了HttpClient的基本功能,它只要用於一些不需要靈活性的簡單場景。例如,快速api不需要使用者處理連線管理和資源釋放。
下面是幾個使用快速api的例子:
- // Execute a GET with timeout settings and return response content as String.
- Request.Get("http://somehost/")
- .connectTimeout(1000)
- .socketTimeout(1000)
- .execute().returnContent().asString();
- // Execute a POST with the 'expect-continue' handshake, using HTTP/1.1,
- // containing a request body as String and return response content as byte array.
- Request.Post("http://somehost/do-stuff")
- .useExpectContinue()
- .version(HttpVersion.HTTP_1_1)
- .bodyString("Important stuff", ContentType.DEFAULT_TEXT)
- .execute().returnContent().asBytes();
- // Execute a POST with a custom header through the proxy containing a request body
- // as an HTML form and save the result to the file
- Request.Post("http://somehost/some-form")
- .addHeader("X-Custom-header", "stuff")
- .viaProxy(new HttpHost("myproxy", 8080))
- .bodyForm(Form.form().add("username", "vip").add("password", "secret").build())
- .execute().saveContent(new File("result.dump"));
如果需要在指定的安全上下文中執行某些請求,我們也可以直接使用Exector,這時候使用者的認證資訊就會被快取起來,以便後續的請求使用。
- Executor executor = Executor.newInstance()
- .auth(new HttpHost("somehost"), "username", "password")
- .auth(new HttpHost("myproxy", 8080), "username", "password")
- .authPreemptive(new HttpHost("myproxy", 8080));
- executor.execute(Request.Get("http://somehost/"))
- .returnContent().asString();
- executor.execute(Request.Post("http://somehost/do-stuff")
- .useExpectContinue()
- .bodyString("Important stuff", ContentType.DEFAULT_TEXT))
- .returnContent().asString();
5.1.1. 響應處理
一般情況下,HttpClient的快速api不用使用者處理連線管理和資源釋放。但是,這樣的話,就必須在記憶體中快取這些響應訊息。為了避免這一情況,建議使用使用ResponseHandler來處理Http響應。
- Document result = Request.Get("http://somehost/content")
- .execute().handleResponse(new ResponseHandler<Document>() {
- public Document handleResponse(final HttpResponse response) throws IOException {
- StatusLine statusLine = response.getStatusLine();
- HttpEntity entity = response.getEntity();
- if (statusLine.getStatusCode() >= 300) {
- throw new HttpResponseException(
- statusLine.getStatusCode(),
- statusLine.getReasonPhrase());
- }
- if (entity == null) {
- throw new ClientProtocolException("Response contains no content");
- }
- DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
- try {
- DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
- ContentType contentType = ContentType.getOrDefault(entity);
- if (!contentType.equals(ContentType.APPLICATION_XML)) {
- throw new ClientProtocolException("Unexpected content type:" +
- contentType);
- }
- String charset = contentType.getCharset();
- if (charset == null) {
- charset = HTTP.DEFAULT_CONTENT_CHARSET;
- }
- return docBuilder.parse(entity.getContent(), charset);
- } catch (ParserConfigurationException ex) {
- throw new IllegalStateException(ex);
- } catch (SAXException ex) {
- throw new ClientProtocolException("Malformed XML document", ex);
- }
- }
- });
第六章 HTTP快取
6.1. 基本概念
HttpClient的快取機制提供一個與HTTP/1.1標準相容的快取層 – 相當於Java的瀏覽器快取。HttpClient快取機制的實現遵循責任鏈(Chain of Responsibility)設計原則,預設的HttpClient是沒有快取的,有快取機制的HttpClient可以用來臨時替代預設的HttpClient,如果開啟了快取,我們的請求結果就會從快取中獲取,而不是從目標伺服器中獲取。如果在Get請求頭中設定了If-Modified-Since
或者If-None-Match
引數,那麼HttpClient會自動向伺服器校驗快取是否過期。
HTTP/1.1版本的快取是語義透明的,意思是無論怎樣,快取都不應該修改客戶端與伺服器之間傳輸的請求/響應資料包。因此,在existing compliant client-server relationship中使用帶有快取的HttpClient也應該是安全的。雖然快取是客戶端的一部分,但是從Http協議的角度來看,快取機制是為了相容透明的快取代理。
最後,HttpClient快取也支援RFC 5861規定的Cache-Control擴充(stale-if-error'和
stale-while-revalidate`)。
當開啟快取的HttpClient執行一個Http請求時,會經過下面的步驟:
- 檢查http請求是否符合HTTP 1.1的基本要求,如果不符合就嘗試修正錯誤。
- 重新整理該請求無效的快取項。(Flush any cache entries which would be invalidated by this request.)
- 檢測該請求是否可以從快取中獲取。如果不能,直接將請求傳送給目標伺服器,獲取響應並加入快取。
- 如果該請求可以從快取中獲取,HttpClient就嘗試讀取快取中的資料。如果讀取失敗,就會傳送請求到目標伺服器,如果可能的話,就把響應快取起來。
- 如果HttpClient快取的響應可以直接返回給請求,HttpClient就會構建一個包含
ByteArrayEntity
的BasicHttpResponse
物件,並將它返回給http請求。否則,HttpClient會向伺服器重新校驗快取。 - 如果HttpClient快取的響應,向伺服器校驗失敗,就會向伺服器重新請求資料,並將其快取起來(如果合適的話)。
當開啟快取的HttpClient收到伺服器的響應時,會經過下面的步驟: - 檢查收到的響應是否符合協議相容性
- 確定收到的響應是否可以快取
- 如果響應是可以快取的,HttpClient就會盡量從響應訊息中讀取資料(大小可以在配置檔案進行配置),並且快取起來。
- 如果響應資料太大,快取或者重構消耗的響應空間不夠,就會直接返回響應,不進行快取。
需要注意的是,帶有快取的HttpClient不是HttpClient的另一種實現,而是通過向http請求執行管道中插入附加處理元件來實現的。
6.2. RFC-2616 Compliance
HttpClient的快取機制和RFC-2626文件規定是無條件相容的。也就是說,只要指定了MUST
,MUST NOT
,SHOULD
或者SHOULD NOT
這些Http快取規範,HttpClient的快取層就會按照指定的方式進行快取。即當我們使用HttpClient的快取機制時,HttpClient的快取模組不會產生異常動作。
6.3. 使用範例
下面的例子講述瞭如何建立一個基本的開啟快取的HttpClient。並且配置了最大快取1000個Object物件,每個物件最大佔用8192位元組資料。程式碼中出現的資料,只是為了做演示,而過不是推薦使用的配置。
- CacheConfig cacheConfig = CacheConfig.custom()
- .setMaxCacheEntries(1000)
- .setMaxObjectSize(8192)
- .build();
- RequestConfig requestConfig = RequestConfig.custom()
- .setConnectTimeout(30000)
- .setSocketTimeout(30000)
- .build();
- CloseableHttpClient cachingClient = CachingHttpClients.custom()
- .setCacheConfig(cacheConfig)
- .setDefaultRequestConfig(requestConfig)
- .build();
- HttpCacheContext context = HttpCacheContext.create();
- HttpGet httpget = new HttpGet("http://www.mydomain.com/content/");
- CloseableHttpResponse response = cachingClient.execute(httpget, context);
- try {
- CacheResponseStatus responseStatus = context.getCacheResponseStatus();
- switch (responseStatus) {
- case CACHE_HIT:
- System.out.println("A response was generated from the cache with " +
- "no requests sent upstream");
- break;
- case CACHE_MODULE_RESPONSE:
- System.out.println("The response was generated directly by the " +
- "caching module");
- break;
- case CACHE_MISS:
- System.out.println("The response came from an upstream server");
- break;
- case VALIDATED:
- System.out.println("The response was generated from the cache " +
- "after validating the entry with the origin server");
- break;
- }
- } finally {
- response.close();
- }
6.4. 配置
有快取的HttpClient繼承了非快取HttpClient的所有配置項和引數(包括超時時間,連線池大小等配置項)。如果需要對快取進行具體配置,可以初始化一個CacheConfig
物件來自定義下面的引數:
Cache size
(快取大小). 如果後臺儲存支援,我們可以指定快取的最大條數,和每個快取中儲存的response的最大size。Public/private cacheing
(公用/私有 快取). 預設情況下,快取模組會把快取當做公用的快取,所以快取機制不會快取帶有授權頭訊息或者指定Cache-Control:private
的響應。但是如果快取只會被一個邏輯上的使用者使用(和瀏覽器餓快取類似),我們可能希望關閉快取共享機制。Heuristic caching
(啟發式快取)。即使伺服器沒有明確設定快取控制headers資訊,每個RFC2616快取也會儲存一定數目的快取。這個特徵在HttpClient中預設是關閉的,如果伺服器不設定控制快取的header資訊,但是我們仍然希望對響應進行快取,就需要在HttpClient中開啟這個功能。啟用啟發式快取,然後使用預設的重新整理時間或者自定義重新整理時間。更多啟發式快取的資訊,可以參考Http/1.1 RFC文件的13.2.2小節,13.2.4小節。Background validation
(後臺校驗)。HttpClient的快取機制支援RFC5861的stale-while-revalidate
指令,它允許一定數目的快取在後臺校驗是否過期。我們可能需要調整可以在後臺工作的最大和最小的執行緒數,以及設定執行緒在回收前最大的空閒時間。當沒有足夠執行緒來校驗快取是否過期時,我們可以指定排隊佇列的大小。
6.5.儲存介質
預設,HttpClient快取機制將快取條目和快取的response放在本地程式的jvm記憶體中。這樣雖然提供高效能,但是當我們的程式記憶體有大小限制的時候,這就會變得不太合理。因為快取的生命中期很短,如果程式重啟,快取就會失效。當前版本的HttpClient使用EhCache和memchached來儲存快取,這樣就支援將快取放到本地磁碟或者其他儲存介質上。如果記憶體、本地磁碟、外地磁碟,都不適合你的應用程式,HttpClient也支援自定義儲存介質,只需要實現HttpCacheStorage
介面,然後在建立HttpClient時,使用這個介面的配置。這種情況,快取會儲存在自定義的介質中,但是you
will get to reuse all of the logic surrounding HTTP/1.1 compliance and cache handling. 一般來說,可以建立出支援任何鍵值對指定儲存(類似Java Map介面)的HttpCacheStorage
,用於進行原子更新。
最後,通過一些額外的工作,還可以建立起多層次的快取結構;磁碟中的快取,遠端memcached中的快取,虛擬記憶體中的快取,L1/L2處理器中的快取等。
第七章 高階主題
7.1 自定義客戶端連線
在特定條件下,也許需要來定製HTTP報文通過線路傳遞,越過了可能使用的HTTP引數來處理非標準不相容行為的方式。比如,對於Web爬蟲,它可能需要強制HttpClient接受格式錯誤的響應頭部資訊,來搶救報文的內容。
通常插入一個自定義的報文解析器的過程或定製連線實現需要幾個步驟:
提供一個自定義LineParser/LineFormatter介面實現。如果需要,實現報文解析/格式化邏輯。
- <span style="font-family:SimSun;">class MyLineParser extends BasicLineParser {
- @Override
- public Header parseHeader(
- CharArrayBuffer buffer) throws ParseException {
- try {
- return super.parseHeader(buffer);
- } catch (ParseException ex) {
- // Suppress ParseException exception
- return new BasicHeader(buffer.toString(), null);
- }
- }
- }</span>
提過一個自定義的 HttpConnectionFactory 實現。替換需要自定義的預設請求/響應解析器,請求/響應格式化器。如果需要,實現不同的報文寫入/讀取程式碼。
- <span style="font-family:SimSun;">HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory =
- new ManagedHttpClientConnectionFactory(
- new DefaultHttpRequestWriterFactory(),
- new DefaultHttpResponseParserFactory(
- new MyLineParser(), new DefaultHttpResponseFactory()));</span>
為了建立新類的連線,提供一個自定義的ClientConnectionOperator介面實現。如果需要,實現不同的套接字初始化程式碼。
- <span style="font-family:SimSun;">PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
- connFactory);
- CloseableHttpClient httpclient = HttpClients.custom()
- .setConnectionManager(cm)
- .build();</span>
7.2 有狀態的HTTP連線
7.2.1 使用者令牌處理器
如果它可以從給定的執行上下文中來獲得,UserTokenHandler介面的預設實現是使用主類的一個例項來代表HTTP連線的狀態物件。UserTokenHandler將會使用基於如NTLM或開啟的客戶端認證SSL會話認證模式的使用者的主連線。如果二者都不可用,那麼就不會返回令牌。
- <span style="font-family:SimSun;">CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpClientContext context = HttpClientContext.create();
- HttpGet httpget = new HttpGet("http://localhost:8080/");
- CloseableHttpResponse response = httpclient.execute(httpget, context);
- try {
- Principal principal = context.getUserToken(Principal.class);
- System.out.println(principal);
- } finally {
- response.close();
- }</span>
- <span style="font-family:SimSun;">UserTokenHandler userTokenHandler = new UserTokenHandler() {
- public Object getUserToken(HttpContext context) {
- return context.getAttribute("my-token");
- }
- };
- CloseableHttpClient httpclient = HttpClients.custom()
- .setUserTokenHandler(userTokenHandler)
- .build();</span>
7.2.2 持久化有狀態的連線
- <span style="font-family:SimSun;">CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpClientContext context1 = HttpClientContext.create();
- HttpGet httpget1 = new HttpGet("http://localhost:8080/");
- CloseableHttpResponse response1 = httpclient.execute(httpget1, context1);
- try {
- HttpEntity entity1 = response1.getEntity();
- } finally {
- response1.close();
- }
- Principal principal = context1.getUserToken(Principal.class);
- HttpClientContext context2 = HttpClientContext.create();
- context2.setUserToken(principal);
- HttpGet httpget2 = new HttpGet("http://localhost:8080/");
- CloseableHttpResponse response2 = httpclient.execute(httpget2, context2);
- try {
- HttpEntity entity2 = response2.getEntity();
- } finally {
- response2.close();
- }</span>
7.3. 使用FutureRequestExecutionService
FutureRequestExecutionService用HttpRequestFutureTask(繼承FutureTask)包裝request。這個類允許你取消Task以及保持跟蹤各項指標,如request duration。
futureRequestExecutionService的構造方法包括兩個引數:httpClient例項和ExecutorService例項。當配置兩個引數的時候,您要使用的執行緒數等於最大連線數是很重要的。當執行緒比連線多的時候,連線可能會開始超時,因為沒有可用的連線。當連線多於執行緒時,futureRequestExecutionService不會使用所有的連線。
- <span style="font-family:SimSun;">HttpClient httpClient = HttpClientBuilder.create().setMaxConnPerRoute(5).build();
- ExecutorService executorService = Executors.newFixedThreadPool(5);
- FutureRequestExecutionService futureRequestExecutionService =
- new FutureRequestExecutionService(httpClient, executorService);</span>
7.3.2. 安排requests
要安排一個請求,只需提供一個HttpUriRequest,HttpContext和ResponseHandler。因為request是由executor service處理的,而ResponseHandler的是強制性的。
- <span style="font-family:SimSun;">private final class OkidokiHandler implements ResponseHandler<Boolean> {
- public Boolean handleResponse(
- final HttpResponse response) throws ClientProtocolException, IOException {
- return response.getStatusLine().getStatusCode() == 200;
- }
- }
- HttpRequestFutureTask<Boolean> task = futureRequestExecutionService.execute(
- new HttpGet("http://www.google.com"), HttpClientContext.create(),
- new OkidokiHandler());
- // blocks until the request complete and then returns true if you can connect to Google
- boolean ok=task.get();</span>
7.3.3. 取消tasks
預定的任務可能會被取消。如果任務尚未執行,但僅僅是排隊等待執行,它根本就不會執行。如果任務在執行中且mayInterruptIfRunning引數被設定為true,請求中的abort()函式將被呼叫;否則response會簡單地忽略,但該請求將被允許正常完成。任何後續呼叫task.get()會產生一個IllegalStateException。應當注意到,取消任務僅可以釋放客戶端的資源。該請求可能實際上是在伺服器端正常處理。
- <span style="font-family:SimSun;">task.cancel(true)
- task.get() // throws an Exception</span>
7.3.4. 回撥
不用手動呼叫task.get(),您也可以在請求完成時使用FutureCallback例項獲取回撥。這裡採用的是和HttpAsyncClient相同的介面
- <span style="font-family:SimSun;">private final class MyCallback implements FutureCallback<Boolean> {
- public void failed(final Exception ex) {
- // do something
- }
- public void completed(final Boolean result) {
- // do something
- }
- public void cancelled() {
- // do something
- }
- }
- HttpRequestFutureTask<Boolean> task = futureRequestExecutionService.execute(
- new HttpGet("http://www.google.com"), HttpClientContext.create(),
- new OkidokiHandler(), new MyCallback());</span>
7.3.5. 指標
FutureRequestExecutionService通常用於大量Web服務呼叫的應用程式之中。為了便於例如監視或配置調整,FutureRequestExecutionService跟蹤了幾個指標。
HttpRequestFutureTask會提供一些方法來獲得任務時間:從被安排,開始,直到結束。此外,請求和任務持續時間也是可用的。這些指標都聚集在FutureRequestExecutionService中的FutureRequestExecutionMetrics例項,可以通過FutureRequestExecutionService.metrics()獲取。
- <span style="font-family:SimSun;">task.scheduledTime() // returns the timestamp the task was scheduled
- task.startedTime() // returns the timestamp when the task was started
- task.endedTime() // returns the timestamp when the task was done executing
- task.requestDuration // returns the duration of the http request
- task.taskDuration // returns the duration of the task from the moment it was scheduled
- FutureRequestExecutionMetrics metrics = futureRequestExecutionService.metrics()
- metrics.getActiveConnectionCount() // currently active connections
- metrics.getScheduledConnectionCount(); // currently scheduled connections
- metrics.getSuccessfulConnectionCount(); // total number of successful requests
- metrics.getSuccessfulConnectionAverageDuration(); // average request duration
- metrics.getFailedConnectionCount(); // total number of failed tasks
- metrics.getFailedConnectionAverageDuration(); // average duration of failed tasks
- metrics.getTaskCount(); // total number of tasks scheduled
- metrics.getRequestCount(); // total number of requests
- metrics.getRequestAverageDuration(); // average request duration
- metrics.getTaskAverageDuration(); // average task duration</span>
參考:http://hc.apache.org/httpcomponents-client-ga/tutorial/html/index.html
相關文章
- HttpClient4.5中文教程HTTPclient
- HttpclientHTTPclient
- httpclient 4.5.3HTTPclient
- Python logging 庫的『完整教程』Python
- Apache之HttpClientApacheHTTPclient
- Swift 正規表示式完整教程Swift
- Prolog入門教程(完整版)
- 最完整的Markdown基礎教程
- tep完整教程幫你突破pytest
- 【HttpClient】httpclient之post 方法(引數為Map型別)HTTPclient型別
- 工具篇:apache-httpClient 和 jdk11-HttpClient的使用ApacheHTTPclientJDK
- Springboot整合MybatisPlus(超詳細)完整教程~Spring BootMyBatis
- PowerJob高階特效-容器部署完整教程特效
- 正規表示式完整教程(略長)
- Go - httpclient 常用操作GoHTTPclient
- Drools 業務規則引擎的完整教程
- 聊聊jdk httpclient的executorJDKHTTPclient
- HttpClient 下載檔案HTTPclient
- Httpclient 介面自動化HTTPclient
- HttpClient請求工具類HTTPclient
- .Netcore HttpClient原始碼探究NetCoreHTTPclient原始碼
- 《Django入門與實踐教程》完整版Django
- 最新Python教程全套合集專案實戰(完整)Python
- HttpClient 進行soap請求HTTPclient
- 為HttpClient開啟HTTP/2HTTPclient
- .NET Core HttpClient原始碼探究HTTPclient原始碼
- RestTemplate和 apache HttpClient 使用方式RESTApacheHTTPclient
- SecureCRT 註冊碼啟用資源附完整教程Securecrt
- PhpStorm配置Xdebug最完整最詳解教程,100%成功!PHPORM
- Mysql 5.7 免安裝版windows安裝完整教程MySqlWindows
- Java11 HttpClient小試牛刀JavaHTTPclient
- 聊聊jdk httpclient的retry引數JDKHTTPclient
- 小心 HttpClient 中的 FormUrlEncodeContent 的 bugHTTPclientORM
- Apache httpclient的execute方法除錯ApacheHTTPclient除錯
- 優雅通過HttpClientFactory使用HttpClientHTTPclient
- java httpclient傳送中文亂碼JavaHTTPclient
- C# httpclient上傳檔案C#HTTPclient
- 【傳輸協議】HttpClient基本使用協議HTTPclient
- Apache HttpClient使用和原始碼分析ApacheHTTPclient原始碼