HttpClient-01基本概念


Http 協議應該是互聯網中最重要的協議。持續增長的 web 服務、可聯網的家用電器等都在繼承並拓 展着 Http 協議,向着瀏覽器之外的方向發展。

雖然 jdk 中的 java.net 包中提供了一些基本的方法,通過 http 協議來訪問網絡資源,但是大多數場 景下,它都不夠靈活和強大。HttpClient 致力於填補這個空白,它可以提供有效的、最新的、功能豐 富的包來實現 http 客戶端

以下是一個簡單的請求例子:

    @Test
     public void test01() throws Exception {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://www.baidu.com/");
        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            System.out.println(response);
        } catch (Exception e) {

        } 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 的創建和修改過程。 

@Test
     public void test02() throws Exception {
        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響應第一行是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提供了API來獲取、添加、修改、遍歷消息頭。

   HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
    response.addHeader("Set-Cookie", "c1=a; path=/; domain=yeetrack.com");
    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=yeetrack.com

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=yeetrack.com");
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=yeetrack.com
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=yeetrack.com");
response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"yeetrack.com\"");
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=yeetrack.com c2 = b path=/ c3 = c domain=yeetrack.com 

 

1.1.4. HTTP實體

Http消息可以攜帶http實體,這個http實體既可以是http請求,也可以是http響應的。Http實體,可以在某些http請求或者響應中發現,但不是必須的。Http規范中定義了兩種包含請求的方法:POST和PUT。HTTP響應一般會包含一個內容實體。當然這條規則也有異常情況,如Head方法的響應,204沒有內容,304沒有修改或者205內容資源重置。

HttpClient根據來源的不同,划分了三種不同的Http實體內容。

  • streamed: Http內容是通過流來接受或者generated on the fly。特別是,streamed這一類包含從http響應中獲取的實體內容。一般說來,streamed實體是不可重復的。
  • self-contained: The content is in memory or obtained by means that are independent from a connection or other entity。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-TypeContent-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://www.baidu.com/");
        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連接。

請注意HttpEntitywriteTo(OutputStream)方法,當Http實體被寫入到OutputStream后,也要確保釋放系統資源。如果這個方法內調用了HttpEntitygetContent()方法,那么它會有一個java.io.InpputStream的實例,我們需要在finally中關閉這個流。

但是也有這樣的情況,我們只需要獲取Http響應內容的一小部分,而獲取整個內容並、實現連接的可重復性代價太大,這時我們可以通過關閉響應的方式來關閉內容輸入、輸出流。

   CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://www.baidu.com/");
        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推薦使用HttpEntitygetConent()方法或者HttpEntitywriteTo(OutputStream)方法來消耗掉Http實體內容。HttpClient也提供了EntityUtils這個類,這個類提供一些靜態方法可以更容易地讀取Http實體的內容和信息。和以java.io.InputStream流讀取內容的方式相比,EntityUtils提供的方法可以以字符串或者字節數組的形式讀取Http實體。但是,強烈不推薦使用EntityUtils這個類,除非目標服務器發出的響應是可信任的,並且http響應實體的長度不會過大。

    CloseableHttpClient httpclient = HttpClients.createDefault();
    HttpGet httpget = new HttpGet("http://www.yeetrack.com/");
    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 provides several classes that can be used to efficiently stream out content though HTTP connections.感覺thought應該是throught)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://www.yeetrack.com/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://www.yeetrack.com/handler.do");
    httppost.setEntity(entity);

UrlEncodedFormEntity實例會使用所謂的Url編碼的方式對我們的參數進行編碼,產生的結果如下:

 param1=value1&param2=value2 

1.1.7.2. 內容分塊

一般來說,推薦讓HttpClient自己根據Http消息傳遞的特征來選擇最合適的傳輸編碼。當然,如果非要手動控制也是可以的,可以通過設置HttpEntitysetChunked()為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://www.yeetrack.com/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://www.yeetrack.com/json");

        ResponseHandler<JsonObject> rh = new ResponseHandler<JsonObject>() {

            @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, JsonObject.class);
            }
        };
        //設置responseHandler,當執行http方法時,就會返回MyJsonObject對象。
        JsonObject myjson = httpclient.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-alive這個參數,我們就把它設置成5秒
                    keepAlive = 5000;
                }
                return keepAlive;
        }

    };
    //定制我們自己的httpclient
    CloseableHttpClient httpclient = HttpClients.custom()
            .setKeepAliveStrategy(keepAliveStrat)
            .build();

1.2.1.HTTPCLIENT的線程安全性

HttpClient已經實現了線程安全。所以希望用戶在實例化HttpClient時,也要支持為多個請求使用。

1.2.2.HTTPCLIENT的內存分配

當一個CloseableHttpClient的實例不再被使用,並且它的作用范圍即將失效,和它相關的連接必須被關閉,關閉方法可以調用CloseableHttpClientclose()方法。

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://www.yeetrack.com/1");
    httpget1.setConfig(requestConfig);
    CloseableHttpResponse response1 = httpclient.execute(httpget1, context);
    try {
        HttpEntity entity1 = response1.getEntity();
    } finally {
        response1.close();
    }
    //httpget2被執行時,也會使用httpget1的上下文
    HttpGet httpget2 = new HttpGet("http://www.yeetrack.com/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方法的冪等性(To ensure HTTP transport layer safety the system must ensure the idempotency of HTTP methods on the application layer)。

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默認把非實體方法gethead方法看做冪等方法,把實體方法postput方法看做非冪等方法。

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) {
                // 如果已經重試了5次,就放棄
                return false;
            }
            if (exception instanceof InterruptedIOException) {
                // 超時
                return false;
            }
            if (exception instanceof UnknownHostException) {
                // 目標服務器不可達
                return false;
            }
            if (exception instanceof ConnectTimeoutException) {
                // 連接被拒絕
                return false;
            }
            if (exception instanceof SSLException) {
                // ssl握手異常
                return false;
            }
            HttpClientContext clientContext = HttpClientContext.adapt(context);
            HttpRequest request = clientContext.getRequest();
            boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
            if (idempotent) {
                // 如果請求是冪等的,就再次嘗試
                return true;
            }
            return false;
        }

    };  
    CloseableHttpClient httpclient = HttpClients.custom()
            .setRetryHandler(myRetryHandler)
            .build();

1.5.終止請求

有時候由於目標服務器負載過高或者客戶端目前有太多請求積壓,http請求不能在指定時間內執行完畢。這時候終止這個請求,釋放阻塞I/O的進程,就顯得很必要。通過HttpClient執行的Http請求,在任何狀態下都能通過調用HttpUriRequestabort()方法來終止。這個方法是線程安全的,並且能在任何線程中調用。當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是個線程安全的整型類
                    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://www.yeetrack.com/");
    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可以自動重定向所有的HEAD,GET,POST請求,解除了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://www.yeetrack.com: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());
        // 一般會取得一個絕對路徑的uri
    } finally {
        response.close();
    }

 

  

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM