1. 程式人生 > >徹底掌握網路通訊(二)Apache的HttpClient基礎知識

徹底掌握網路通訊(二)Apache的HttpClient基礎知識

網路通訊系列文章序

接上面的基礎知識,這篇主要介紹下Apache中的HttpClient,通過這篇你可以對DefaultHttpClient用到的各種知識有一個更好的瞭解

1)先解釋下何為HttpClient
HttpClient 是Apache Jakarta Common 下的子專案,他是一個第三方庫,開發者可以使用這個庫提供的API方便進行HTTP請求,HttpClient是基於HttpCore來實現的一個庫;
HttpClient主頁

2)何為HttpCore
HttpCore是一組Http傳輸元件,可以說HttpClient進行的Http請求都是由HttpCore來進行的;

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