前言
Http協議應該是互聯網中最重要的協議。持續增加的web服務、可聯網的家用電器等都在繼承並拓展着Http協議,向着瀏覽器以外的方向發展。javascript
雖然jdk中的java.net包中提供了一些基本的方法,經過http協議來訪問網絡資源,可是大多數場景下,它都不夠靈活和強大。HttpClient致力於填補這個空白,它能夠提供有效的、最新的、功能豐富的包來實現http客戶端。html
爲了拓展,HttpClient即支持基本的http協議,還支持http-aware客戶端程序,如web瀏覽器,Webservice客戶端,以及利用or拓展http協議的分佈式系統。java
一、HttpClient的範圍/特性web
- 是一個基於HttpCore的客戶端Http傳輸類庫
- 基於傳統的(阻塞)IO
- 內容無關
二、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就會拋出異樣。apache
下面是個很簡單的http請求執行的例子:json
- 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版本號。windows
HttpClient支持HTTP/1.1這個版本定義的全部Http方法:GET
,HEAD
,POST
,PUT
,DELETE
,TRACE和
OPTIONS。對於每一種http方法,HttpClient都定義了一個相應的類:
HttpGet,
HttpHead,
HttpPost,
HttpPut,
HttpDelete,
HttpTrace和
HttpOpquertions。api
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就全面支持NTLMv一、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鏈接
HTTP規範假設session狀態信息一般是以HTTP cookie格式嵌入在HTTP報文中的,所以HTTP鏈接一般是無狀態的,這個假設在現實生活中一般是不對的。也有一些狀況,當HTTP鏈接使用特定的用戶標識或特定的安全上下文來建立時,所以不能和其它用戶共享,只能由該用戶重用。這樣的有狀態的HTTP鏈接的示例就是NTLM認證鏈接和使用客戶端證書認證的SSL鏈接。
7.2.1 用戶令牌處理器
HttpClient依賴UserTokenHandler接口來決定給定的執行上下文是不是用戶指定的。若是這個上下文是用戶指定的或者若是上下文沒有包含任何資源或關於當前用戶指定詳情而是null,令牌對象由這個處理器返回,指望惟一地標識當前的用戶。用戶令牌將被用來保證用戶指定資源不會和其它用戶來共享或重用。
若是它能夠從給定的執行上下文中來得到,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 持久化有狀態的鏈接
請注意帶有狀態對象的持久化鏈接僅當請求被執行時,相同狀態對象被綁定到執行上下文時能夠被重用。因此,保證相同上下文重用於執行隨後的相同用戶,或用戶令牌綁定到以前請求執行上下文的HTTP請求是很重要的。
- <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,你能夠調度HTTP調用以及把response看成一個Future。這是很是有用的,好比當屢次調用一個Web服務。使用FutureRequestExecutionService的優點在於您可使用多個線程來調度請求同時,對任務設置超時或取消,當response再也不須要的時候。
FutureRequestExecutionService用HttpRequestFutureTask(繼承FutureTask)包裝request。這個類容許你取消Task以及保持跟蹤各項指標,如request duration。
7.3.1. 構造FutureRequestExecutionService
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>