徹底掌握網路通訊(二)Apache的HttpClient基礎知識
網路通訊系列文章序
接上面的基礎知識,這篇主要介紹下Apache中的HttpClient,通過這篇你可以對DefaultHttpClient用到的各種知識有一個更好的瞭解
1)先解釋下何為HttpClient
HttpClient 是Apache Jakarta Common 下的子專案,他是一個第三方庫,開發者可以使用這個庫提供的API方便進行HTTP請求,HttpClient是基於HttpCore來實現的一個庫;
HttpClient主頁
2)何為HttpCore
HttpCore是一組Http傳輸元件,可以說HttpClient進行的Http請求都是由HttpCore來進行的;
3)HttpClient詳解
3.0)特徵
- 基於java,實現了http 1.0和1.1
- 實現了所有的HTTP方法
- 支援HTTPS
- 支援COOKIE處理
- 支援HTTP1.1的響應快取
3.1)範圍
- 基於httpcore而實現的第三方庫
- 基於阻塞I/0
3.2)部分樣例程式碼
- 返回訊息對頭資訊的處理,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");
Header h2 = response.getLastHeader("Set-Cookie");
Header[] hs = response.getHeaders("Set-Cookie" );
最有效的獲取全部Header的方法是使用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());
}
在使用HeaderIterator介面的時候,我們也可以通過HeaderElement獲取Header的key value值
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_OK, "OK");
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]);
}
}
-終止請求只要呼叫
HttpUriRequest#abort()
即可,該方法是執行緒安全的,呼叫之後會丟擲InterruptedIOException
3.3)Http Entity實體
Http Entity即可以表示請求裡面的請求實體,也可以是響應中的響應實體,Http Entity可以分為如下幾類
1)代表底層流的基本實體:通常是在http報文中獲取的實體。他只有一個空參的構造方法。剛建立時沒有內容,長度為負值。需要通過兩個方法,把值賦進去。
2)自我包含的,可重複獲得使用的實體
3)流式不可以重複的實體
其中代表底層流的基本實體的類有
1.1)BasicHttpEntity
InputStream is = null;
//BasicHttpEntity這類就是一個輸入流的內容包裝類,包裝內容的相關的編碼格式,長度等
BasicHttpEntity entity = new BasicHttpEntity();
//設定內容
entity.setContent(is);
//設定長度
entity.setContentLength(is.available());
其中代表自我包含的,可重複獲得使用的實體類有
2.1)ByteArrayEntity:從指定的位元組陣列中取出內容的實體
ByteArrayEntity entity = new ByteArrayEntity("內容".getBytes());
ByteArrayInputStream is = (ByteArrayInputStream) entity.getContent();
2.2)StringEntity:通過String建立的實體
String content = "測試StringEntity";
StringEntity entity = new StringEntity(content);
//建立帶字元創引數和字元編碼的
StringEntity entity2 = new StringEntity(content, "UTF-8");
2.3)FileEntity:通過檔案構建的Entity,引數傳入檔案和檔案型別
FileEntity entity = new FileEntity(new File(""), "application/java-achive");
其中代表是流式不可以重複的實體類有
3.1)InputreamEntity:構造方法是InputStream 和內容長度,內容長度是輸入流的長度
InputStream is = null;
//InputStreamEntity嚴格是對內容和長度相匹配的。用法和BasicHttpEntity類似
InputStreamEntity entity = new InputStreamEntity(is, is.available());
綜述:各種Entity,比如FileEntity,StringEntity等,這些都是實現了HttpEntity介面,同時實現自己的一些方法罷了;上面的實體的型別,如代表底層流的實體他們只是在構造上不同罷了,不同的建立方式決定這個Entity是不是可重用,是不是一塊一塊的等等;還有一些類如HttpEntityWrapper,BufferedHttpEntity,這些都是對各種Entity的包裝;下圖列明瞭各種Entity的含義
在我們使用實體之後,我們還需要對實體進行管理,在httpclient中,有一個EntityUtils的類,他是一個靜態的物件用來我們處理Entity
這個類在使用的時候,我們應該注意下,在Android使用的HttpClient中他沒有consume方法,因此在使用完entity的時候,需要注意的entity的釋放
-釋放資源
正確釋放系統資源,我們應該做到釋放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();
}
而有的時候,我們可能需要不止一次閱讀實體內容。 在這種情況下,實體內容必須以某種方式進行緩衝,無論是在記憶體中還是在磁碟上。 最簡單的方法是用BufferedHttpEntity類包裝原始實體。 這會導致原始實體的內容被讀入記憶體緩衝區。
CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}
3.4)HttpClient的執行緒安全
建議使用同一個HttpClient的例項來確保執行緒安全
3.5)HttpContext
這是一個Http請求上下文類,如果是同一個上下文,則兩次請求間可以共享這個上線文儲存的資訊。有的同學會問,通一個httpclient本身就具備維護cookies的功能,為什麼要用HttpContext?其實HttpContext的妙用就是在於多個httpclient例項之間可以共享httpcontext。
HttpContext和Map很相似,通過key值來完成多個會話之間的資料共享;通過HttpContext封裝資訊之後,如果代表一個邏輯相關的會話中的多個請求序列被同一個HttpContext例項執行,則請求之間會話上下文和狀態資訊可以自動傳輸。
/*演示使用同一個httpcontext,設定請求引數,多個請求之間共享這個引數設定,即httpget1和httpget2共享了requestConfig,因為此處他們使用的是同一個context*/
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();
}
這裡我們配置了請求引數,同理你也可以配置其他引數,如下
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("username", "**"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params,"UTF-8");
HttpPost httpPost = new HttpPost("url");
httpPost.setEntity(entity);
CloseableHttpResponse response = httpClient.execute(httpPost,context);
HttpEntity responseEntity = response.getEntity();
}
finally {
httpClient.close();
response.close();
}
CloseableHttpClient httpClient2 = HttpClients.createDefault();
try {
HttpGet httpGet = new HttpGet("url");
CloseableHttpResponse response = httpClient2.execute(httpGet,context);
HttpEntity entity = response.getEntity();
} finally {
httpClient2.close();
response.close();
}
3.6)HTTP 攔截器
這個攔截器可以設定在請求傳送之前,也可以設定在獲取到響應實體之後,在android中我們可以通過繼承AbstractHttpClient類,並實現addRequestInterceptor和addResponseInterceptor兩個方法實現目的
public class Client extends DefaultHttpClient {
private static ClientConnectionManager mClientConnectionManager;
public Client(Handler handler) {
super(makeClientConnectionManager(), new BasicHttpParams());
// gzip stream support,新增請求攔截
addRequestInterceptor(new HttpRequestInterceptor() {
public void process(final HttpRequest request, final HttpContext context)
throws HttpException, IOException {
if (!request.containsHeader("Accept-Encoding")) {
request.addHeader("Accept-Encoding", "gzip");
}
}
});
//響應攔截
addResponseInterceptor(new HttpResponseInterceptor() {
public void process(final HttpResponse response, final HttpContext context)
throws HttpException, IOException {
HttpEntity entity = response.getEntity();
if (entity != null) {
Header ceheader = entity.getContentEncoding();
if (ceheader != null) {
HeaderElement[] codecs = ceheader.getElements();
if (codecs != null) {
for (int i = 0; i < codecs.length; i++) {
if (codecs[i].getName().equalsIgnoreCase("gzip")) {
response.setEntity(new GzipDecompressingEntity(response.getEntity()));
return;
}
}
}
}
}
}
});
}
}
static final ClientConnectionManager makeClientConnectionManager() {
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
mClientConnectionManager = new ThreadSafeClientConnManager(new BasicHttpParams(), schemeRegistry);
return mClientConnectionManager;
}
3.7)HTTP的安全性和冪等性
安全性僅指該方法的多次呼叫不會產生副作用,不涉及傳統意義上的“安全”,這裡的副作用是指資源狀態。即,安全的方法不會修改資源狀態,儘管多次呼叫的返回值可能不一樣(被其他非安全方法修改過)。冪等性,是指該方法多次呼叫返回的效果(形式)一致,客戶端可以重複呼叫並且期望同樣的結果
3.8)Http的異常恢復機制
預設情況下,HttpClient會嘗試從I / O異常中自動恢復。 預設的自動恢復機制僅限於少數已知安全的例外。
3.8.1)HttpClient不會嘗試從任何邏輯或HTTP協議錯誤(從HttpException類派生的錯誤)中恢復。
3.8.2)HttpClient會自動重試那些被認為是冪等的方法。
3.8.3)當HTTP請求仍然被傳送到目標伺服器時(即請求沒有完全傳送到伺服器),HttpClient會自動重試那些傳送異常失敗的方法。
我們可以使用如下程式碼進行異常恢復
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();
同時我們可以使用StandardHttpRequestRetryHandler替代預設的HttpRequestRetryHandler,以便將那些被RFC-2616定義為冪等的請求方法視為安全自動重試:GET,HEAD,PUT,DELETE,OPTIONS和TRACE