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