第一章 基礎(chǔ)(1)

1.1 執(zhí)行請求

HttpClient最基本的功能是執(zhí)行HTTP方法。一個HTTP方法的執(zhí)行涉及到一個或多個HTTP請求和響應(yīng)的交換,這通常是在HttpClient內(nèi)部處理的。用戶需要提供一個請求對象,HttpClient負(fù)責(zé)傳輸這個請求到目標(biāo)服務(wù)器并返回相對應(yīng)的響應(yīng)的對象,如果執(zhí)行不成功,則拋出異常。

HttpClient API的入口就是HttpClient接口。

以下是最簡單的形式的執(zhí)行請求的例子:

    public void chapter1_1() throws IOException {

        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            // handle response
        } finally {
            response.close();
        }

    }

1.1.1 HTTP請求

所有的HTTP請求都有一個請求頭,其中包含方法名、請求URIHTTP協(xié)議版本號

HttpClient支持所有HTTP/1.1規(guī)格中定義的HTTP方法,包括:GET,HEAD, POST, PUT, DELETE, TRACEOPTIONS。每種方法都有與之對應(yīng)的類:HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTraceHttpOptions 。

請求URI是請求資源的統(tǒng)一資源描述符(Uniform Resource Identifier)。HTTP請求URI包含協(xié)議(protocol schema)、主機(jī)名(host name)、可選的端口(optional port)、資源路徑(resource path)、可選的查詢(optional query)和可選的分塊(optional fragment)。

HttpGet httpGet = new HttpGet(
                "http://www.google.com/search?h1=en&q=httpclient&btnG=Google+Search&aq=f&oq=");

HttpClient 提供 URIBuilder工具類來簡化創(chuàng)建和修改請求URI:

      URI uri = new URIBuilder()
                .setScheme("http")
                .setHost("www.google.com")
                .setPath("/search")
                .setParameter("h1", "en")
                .setParameter("q", "httpclient")
                .setParameter("btnG", "Google+Search")
                .setParameter("aq", "f")
                .setParameter("oq", "")
                .build();
        HttpGet httpGet1 = new HttpGet(uri);
        System.out.println(httpGet1.getURI());

1.1.2 HTTP響應(yīng)

HTTP響應(yīng)是服務(wù)器接收和處理完請求報文后所返回的報文。報文的第一行由協(xié)議版本、狀態(tài)碼和及其描述組成。

    public void chapter1_1_2() throws Exception {
        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報文包含一些用于描述報文的頭部信息,如內(nèi)容長度、內(nèi)容類型等等 。HttpClient提供方法去獲取、添加、移除和列舉這些頭部信息。

    public void chapter1_1_3() {
        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=\"/\"; 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="/"; domain="localhost"
2

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

    public void chapter1_1_3_2() {
        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=\"/\"; domain=\"localhost\"");

        BasicHeaderElementIterator 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 (NameValuePair param : params) {
                System.out.println(" " + param);
            }
        }
    }

輸出>

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

1.1.4 HTTP實(shí)體

HTTP報文可以攜帶與請求或響應(yīng)相關(guān)聯(lián)的內(nèi)容實(shí)體。因?yàn)閷?shí)體是可選的,并不是所有的請求和響應(yīng)中都包含實(shí)體。使用實(shí)體的請求被稱為包含實(shí)體的請求(entity enclosing request)。HTTP規(guī)格中定義了2種包含實(shí)體的請求的方法:POSTPUT。
響應(yīng)通常會包含一個內(nèi)容實(shí)體。但也是例外,如HEAD方法的響應(yīng)和 204 No Content, 304 Not Modified, 205 Reset Content 響應(yīng)。

HttpClient區(qū)分三種實(shí)體類型,取決于它們內(nèi)容的來源:

  • streamed: 內(nèi)容是從流(stream)中獲得,或聯(lián)機(jī)生成的。這里包含從HTTP響應(yīng)中的實(shí)體。流式實(shí)體(streamed entities)通常是不能重復(fù)的。
  • self-contained: 內(nèi)容是在內(nèi)存里。自包含實(shí)體(self-contained entities)通常是可重復(fù)的。這種類型的實(shí)體最多用于包含實(shí)體的請求(entity enclosing request)。
  • wrapping: 內(nèi)容從另一實(shí)體獲得。

當(dāng)從HTTP響應(yīng)中獲取數(shù)據(jù)流時,這些區(qū)分對于連接管理來說是重要的。對于應(yīng)用創(chuàng)建的請求實(shí)體,且僅使用HttpClient來發(fā)送,streamed 還是 self-contained 的區(qū)別就不重要了。這種情況下,建議把不可重復(fù)的實(shí)體歸為streamed類型,可重復(fù)的為self-contained類型。

1.1.4.1 可重復(fù)實(shí)體

可重復(fù)實(shí)體是指它的內(nèi)容能夠被重復(fù)讀取。只有自包含實(shí)體(self-contained entities)才是可重復(fù)的(如ByteArrayEntityStringEntity)。

1.1.4.2 使用HTTP實(shí)體

因?yàn)閷?shí)體可表示二進(jìn)制和字符內(nèi)容,所以它是支持字符編碼的。
實(shí)體被創(chuàng)建的時機(jī)有 a) 執(zhí)行包含內(nèi)容的請求; b) 請求成功后,響應(yīng)體使用實(shí)體將結(jié)果返回。

為了從輸入報文的實(shí)體中讀取內(nèi)容,我們可以通過HttpEntity#getContent()方法獲取輸入流java.io.InputStream, 或者我們可以通過HttpEntity#writeTo(OutpusStream)方法將其寫到另一個給定的輸出流中。

當(dāng)從響應(yīng)報文中接收到實(shí)體后,HttpEntity#getContentTypeHttpEntity#getContentLength方法可以用來讀取通用的元數(shù)據(jù),如Content-TypeContent-Length頭部信息(如果有的話)。Content-Type頭部對于文本類型的多媒體類型(如text/plaintext/html)來說可能包含字符編碼的信息,HttpEntity#getContentEncoding()方法可能用來讀取該信息。如果頭部不可用的話,HttpEntity#getContentLength返回-1,HttpEntity#getContentType返回NULL。如果Content-Type可用的話,Header對象將會被返回。

當(dāng)給輸出報文創(chuàng)建實(shí)體,元數(shù)據(jù)必須使用實(shí)體的創(chuàng)建者方法來創(chuàng)建:

    public void chapter1_1_4() throws IOException {
        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 確保釋放底層資源

為了確保正確地的釋放系統(tǒng)資源,我們必須關(guān)閉實(shí)體關(guān)聯(lián)的內(nèi)容流(stream)或者響應(yīng)(response)本身。

    public void chapter1_1_5() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            HttpEntity entity = response.getEntity();
            if (Objects.nonNull(entity)) {
                InputStream inputStream = entity.getContent();
                try {
                    // do something with inputStream
                } finally {
                    inputStream.close();
                }
            }
        } finally {
            response.close();
        }
    }

關(guān)閉內(nèi)容流和關(guān)閉響應(yīng)的區(qū)別在于,前者通過消費(fèi)實(shí)體內(nèi)容來試圖保持底層連接,后者會立即關(guān)閉并且丟棄該連接。

請注意HttpEntity#writeTo(OUtputStream)方法也需要確保正確釋放系統(tǒng)資源。如果這個方法通過調(diào)用HttpEntity#getContent方法來獲取的java.io.InputStream實(shí)例,這也需要在一個finally子句中將其關(guān)閉。

我們也可以使用EntityUtils#consume(HttpEntity)方法來確認(rèn)實(shí)體內(nèi)容被完全消費(fèi)并且底層流被關(guān)閉。

有一種情況,如果僅需要讀取響應(yīng)中的一部分內(nèi)容,并且報文剩余內(nèi)容的性能代價和保持連接的代價太高的話,我們可以通過關(guān)閉響應(yīng)來結(jié)束內(nèi)容流。

    public void chapter1_1_5_1() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            HttpEntity entity = response.getEntity();
            if (Objects.nonNull(entity)) {
                InputStream inputStream = entity.getContent();
                int byteOne = inputStream.read();
                int byteTwo = inputStream.read();
                // Do not need the rest
            }
        } finally {
            response.close();
        }
    }

連接將不會被重用,而且被該連接持有的所有資源將會被正確地釋放。

1.1.6 消費(fèi)實(shí)體內(nèi)容

消費(fèi)一個實(shí)體的內(nèi)容推薦的方法是使用HttpEntity#getContent()HttpEntity#writeTo(OutpusStream)方法。HttpClient也包含EntityUtils類,其他包含一些靜態(tài)方法可以理容易地讀取實(shí)體內(nèi)容或信息。這樣我們就可以使用這個類的方法來讀取整個字符或字節(jié)數(shù)據(jù)內(nèi)容,而不是直接操作java.io.InputStream。然而,EntityUtils的使用是非常不推薦的,除非響應(yīng)實(shí)體是來源于一個受信的HTTP服務(wù)器并且內(nèi)容的長度是有限的。

    public void chapter1_1_6() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            HttpEntity entity = response.getEntity();
            if (Objects.nonNull(entity)) {
                long len = entity.getContentLength();
                if (len != -1 && len < 2048) {
                    System.out.println(EntityUtils.toString(entity));
                } else {
                    // Stream content out
                    // content length is too large
                }
            }
        } finally {
            response.close();
        }
    }

在某些情況下,我們需要重復(fù)讀取實(shí)體內(nèi)容。此時,實(shí)體內(nèi)容必須用某種方式來緩沖,或者在內(nèi)容或者在磁盤中。完成緩存的最簡單方式就是把原始的實(shí)體用BufferedHttpEntity類來包裝。這能夠使原來的實(shí)體被讀進(jìn)內(nèi)存的緩存區(qū)中。

CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
  entity = new BufferedHttpEntity(entity);
}

1.1.7 生產(chǎn)實(shí)體內(nèi)容

HttpClient提供一些類,這些類用來高效地將實(shí)體內(nèi)容通過HTTP連接輸出到流。這些類的實(shí)例能夠與包含實(shí)體的請求關(guān)聯(lián), 如POSTPUT。HttpClient提供一些類來作為最常見的數(shù)據(jù)的容器,如字符串、字節(jié)數(shù)組、輸入流和文件:StringEntity、ByteArrayEntity 、InputStreamEntityFileEntity

    public void chapter1_1_7() throws Exception {
        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不是可重復(fù)的,因?yàn)樗荒軓牡讓訑?shù)據(jù)流中讀取一次。通常推薦去實(shí)現(xiàn)一個自定義的HttpEntity,使其成為self-contained類型的,而不是去使用InputStreamEntity。FileEntity就是一個很好的例子。

1.1.7.1 HTML表單

很多應(yīng)用需要去模擬提交HTML表單的過程,例如,為了登陸或提交輸入數(shù)據(jù)。HttpClient提供實(shí)體類UrlEncodedFormEntity來幫助這個提交過程。

    public void chapter1_1_7_1() throws Exception {
        List<NameValuePair> formParams = new ArrayList<>();
        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/handle.do");
        httpPost.setEntity(entity);
    }

UrlEncodedFormEntity會使用URL編碼來對參數(shù)進(jìn)行編碼,并且產(chǎn)生如下內(nèi)容:

param1=value1&param2=value2

1.1.7.2 內(nèi)容分塊(Content chunking)

通常推薦讓HttpClient基于傳輸?shù)腍TTP報文的屬性去選擇使用最合適的傳輸編碼。然而,通過設(shè)置HttpEntity#setChunked()true來通知HttpClient使用chunk編碼是可能的。當(dāng)然,這僅僅只是一個提示而已。如果使用不支持chunk編碼的HTTP協(xié)議,如HTTP/1.0,該值將會被忽略。

1.1.8 響應(yīng)處理器(Response handlers)

最簡單并且最方便的方式去處理響應(yīng)是使用ResponseHandler接口,該接口包含handleResponse(HttpResponse response)方法。該方法完全地把用戶從連接管理中解放出來。當(dāng)使用ResponseHandler,HttpClient會自動地的確保連接會被釋放回給連接管理器,不管執(zhí)行請求是否成功或異常。

    public void chapter1_1_8() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");

        ResponseHandler<MyJsonObject> rh = response -> {
            StatusLine statusLine = response.getStatusLine();
            HttpEntity entity = response.getEntity();
            if (statusLine.getStatusCode() >= 300) {
                throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
            }
            if (Objects.isNull(entity)) {
                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 myJsonObject = httpClient.execute(httpGet, rh);

    }
    
    static class MyJsonObject {
        String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容