HttpClient4.5教程-基礎 1.1

1.1 Request 的執行

HttpClient最必不可少的功能就是執行HTTP的方法,執行HTTP方法會涉及到一個或者多個HTTP request/HTTP response交換,而這些過程通常會在HttpClient內部完成。使用者提交一個request的對象去執行,HttpClient會發送這個request到目標服務器並且獲得一個對應的response對象,如果不成功的話則拋出一個異常。

自然而然,滿足了上面描述的HttpClient接口就是HttpClient API的主要入口。

下面是一個最簡單的request執行過程:

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    <...>
} finally {
    response.close();
}

1.1.1 HTTP request

所有的HTTP request都有一個請求線,包含了請求的方法名稱,請求的URI和HTTP協議版本。

HttpClient開箱即用的支持所有HTTP/1.1中定義的HTTP方法,包括GET,HEAD,POST,PUT,DELETE,TRACE 和 OPTIONS,這些HTTP方法類型都有一個與之對應的類:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace 和 HttpOptions。

Request-URI是一個統一資源定位符,它指明瞭用於處理該request的資源的位置。HTTP request URIs包含了一個協議類型,主機名,可選端口,資源路徑,可選查詢參數,可選片段。

HttpGet httpget = new HttpGet(
     "http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient提供URIBuilder實用類用於簡化request 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 response

HTTP response是服務器在接收並且處理完request消息之後返回給客戶端的消息,消息的第一行包含了協議版本,status code和與之關聯的文本。

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消息包含多個header,這些header描述了消息的屬性比如content length,content type等等,HttpClient提供了取出,添加,刪除和遍歷header的方法。

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

獲取所有指定headers的最高效的方式是使用HeaderIterator接口。

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");

HeaderIterator it = response.headerIterator("Set-Cookie");

while (it.hasNext()) {
    System.out.println(it.next());
}

輸出:

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"

該接口同時也提供HTTP消息轉換到獨立header元素的便利方法。

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 entity

HTTP消息可以攜帶跟request和response相關聯的content entity。由於這些Entity是可選的,所以他們在某些request和response中可以查找到,有些則查不到。使用了entity的request被稱爲entity封裝請求,HTTP定義了兩種entity封裝請求方法:POST 和 PUT。Response通常會封裝一個content entity。不同的方法會有對應的異常,比如對於HEAD來說就有204 No Content,304 Not Modified, 205 Reset Content異常。

HttpClient根據內容來源將Entities區分爲3種,根據內容來源區分爲:

streamed: 內容來自於流或者實時產生。特別的是,這個分類包含了從HTTP responses接收到的entities,Streamed entities通常不可重複。

self-contained: 內容來自於內存或者通過獨立於connection或者其他entity的手段獲得,self-contained entities通常可以重複,其也是封裝了entity的HTTP request最常用的Entities類型。

wrapping : 內容來自於其他的entity。

這種區分對於HTTP response輸出內容時的連接管理是非常重要的。對於request entites來說,由於其是被應用創建並且通過HttpClient發送,streamed和self-contained有什麼不同就沒有那麼重要。

由此,建議將不可重複的entites視爲streamed,可以重複的entities視爲self-contained。


1.1.4.1 可重複的entities

一個entity能夠重複,表明它的內容可以被讀取多次,這種情況只會在self contained entities中發生(比如ByteArrayEntity 或者 StringEntity)


1.1.4.2 使用HTTP entities

Entity可以同時代表二進制和文字內容,支持對字符進行編碼,比如對character content進行編碼。

Entity產生於request(封裝內容 )執行時,或者request成功請求並且response body用於發送結果數據到客戶端時。

從Entity中讀取內容,可以通過HttpEntity#getContent()獲取input stream,也可以向HttpEntity#writeTo(OutputStream)提供一個output stream,該方法會將內容一次性全部寫回給指派的stream。

當消息到達並且Entity已經被接收時,可以用 HttpEntity#getContentType() 和 HttpEntity#getContentLength() 方法讀取常用的元數據,如Content-Type 和 Content-Length 頭信息,Content-Type頭信息包含文本的mime-type編碼信息如 text/plain 或者 text/html,該信息可以通過 HttpEntity#getContentType() 獲取到,如果Header裏面不包含這些信息,那麼 HttpEntity#getContentLength()  會返回-1 並且  HttpEntity#getContentType()  返回 NULL,如果Header包含這些信息,那麼 HttpEntity#getContentType() 會返回 Header 對象。

當創建一個出站消息的entity時,必須同時指定其相關的參數。

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 確保釋放低級資源

爲了保證恰當的釋放系統資源,使用完畢後必須關閉entity相關的stream和response。

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

關閉內容流和關閉response的不同之處在於,關閉內容流會嘗試通過消費entity的content來保持底層連接,而關閉response則是直接關閉和丟棄掉這個連接。

注意 HttpEntity#writeTo(OutputStream)  方法也需要在entity被寫出到流之後恰當的釋放系統資源。如果通過HttpEntity#getContent()獲取了java.io.InputStream流的實例,那麼在finally階段也需要關閉該流。

當處理streaming entites時,可以使用 EntityUtils#consume(HttpEntity) 方法來確保entity內容已經被完全消費並且底層stream已經被關閉。

有另一種情況是,當我們只需要從response content裏面獲取一小部分數據,但是消費剩餘數據和保持連接複用的性能損失又太高時,我們可以直接關閉response。

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 消費entity內容

消費Entity內容的推薦方式是使用 HttpEntity#getContent() 或者 HttpEntity#writeTo(OuptputStream) 方法,也可以使用EntityUtils類,該類提供了一些靜態方法來簡化讀取Entity內容或者其他信息。通過使用該類的某些方法,你可以以String /byte[] 方式來獲取整個content body,從而替代直接讀取 InputStream 的方式。但是,除非你知道response entity來自於可信HTTP server 並且其長度有限,否則不推薦使用EntityUtils。

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

某些情況下你可能讀取entity content不止一次,這時就需要將內容通過內存或者磁盤的方式緩存起來,最簡單的實現方式就是使用BufferedHttpEntity包裝源Entity,該類會將源 Entity 的內容讀取到內存中。如果通過其他方式來實現,那麼就必須要保存一個源entity了。

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

1.1.7 生產 entity content

HttpClient提供多個可以通過HTTP連接高效的輸出內容的類,這些類的實例可以跟POST和PUT等request相關聯並且爲request封裝entity內容,HttpClient提供了最常用的數據容器如string, byte array , input stream 和文件 : 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的實現(self-contained)來替代普通的InputStreamEntity,FileEntity是一個不錯的起始點。


1.1.7.1 HTML表單

許多應用程序需要模擬表單提交的過程,比如登錄web應用或者提交input 數據,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 encoding來編碼參數並且產生出如下內容:

param1=value1¶m2=value2

1.1.7.2 內容分塊

通常來說,建議讓HttpClient選擇最合適的傳輸編碼,HttpClient選擇的傳輸編碼會基於被傳輸的HTTP消息的屬性而定。你可以通過設置HttpEntity#setChunked()爲true來通知HttpClient需要進行chunk編碼。注意HttpClient只是把這一標識當做一個提示使用,該值在不支持chunk編碼的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

最簡單和方便處理response的方式是使用ResponseHandler,該類包含了handleResponse(HttpResponse response)方法。該方法使得用戶完全不需要去操心連接管理的事情。當使用ResponseHandler時,HttpClient會自動確保將連接釋放回連接管理器,無論request是否執行成功。

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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章