HttpComponents入門解析

 

1 簡介

    超文本傳輸協議(http)是目前互聯網上極其普遍的傳輸協議,它爲構建功能豐富,絢麗多彩的網頁提供了強大的支持。構建一個網站,通常無需直接操作http協議,目前流行的WEB框架已經透明的將這些底層功能封裝的很好了,如常見的J2EE, .NET, php等框架或語言。

除了作爲網站系統的底層支撐,http同樣可以在其它的一些場景中使用,如遊戲服務器和客戶端的傳輸協議、web service、 網絡爬蟲、HTTP代理、網站後臺數據接口等。

Http Components 對HTTP底層協議進行了很好的封裝,如果你是一個J2EE、.net或php程序員,對下面涉及的概念可能不會陌生。

 

2 httpComponents組件結構

HttpComponents Core

    簡稱HttpCore, 是一組底層Http傳輸協議組件,支持兩種I/O模型,阻塞I/O模型和和非阻塞I/O模型。上層組件(HttpComponents Client, HttpComponents AsyncClient)依賴此組件實現數據傳輸。

阻塞I/O模型基於基本的JAVA I/O實現,非阻塞模型基於JAVA NIO實現。

HttpComponents Client

建立在HttpCore之上的Http客戶端管理組件。底層基於HttpCore 阻塞I/O。從Commons HttpClient 3.x 繼承而來,Commons HttpClient原來是apache commons組建的一部分,現在被HttpComponents Client所替代了。

    原始的Commons HttpClient 3.x可以在http://hc.apache.org/httpclient-legacy/index.html找到。

 

HttpComponents AsyncClient

    建立在HttpCore NIO模型之上的Http客戶端,與基於阻塞I/O的HttpComponents Client形成互補,由於底層使用的NIO非阻塞模型,所以適用於高性能的應用場景。

 

開始使用HttpComponents組件

    首先打開http://hc.apache.org/,點擊左側的Download鏈接,進入下載頁面,下載最新版本的HttpComponents。在編寫本文時最新版本是4.1.2。解壓縮下載到的壓縮包,lib目錄下是HttpComponents和它依賴的類庫,將它們放到你的工程classpath中,如果依賴文件已經存在了,不要放置多份,以免類庫之間的衝突。

然後需要檢查一下工程的classpath中是否存在commons http包。Commons http與HttpComponents是完全兩個東西,HttpComponents中的Client是從Commons http繼承而來的,所以很多類名是相同的。爲了避免出現莫名奇妙的問題,應將Commons http從工程中刪除(當然,如果你認爲自己足夠聰明,也可以在引用java包時小心區分)。

Commons http類庫的包是org.apache.commons.httpclient

HttpComonents類庫的包是org.apache.http

3 Get請求

    Get、Post是最常見的獲取網頁內容的請求形式,當然,返回內容並非必須是html代碼,任何的xml、json或文字字符串都可以作爲返回內容。

下面是用Get請求獲取一個html網頁內容的代碼

 

 

// (1) 創建HttpGet實例
HttpGet get = new HttpGet("http://www.126.com");

// (2) 使用HttpClient發送get請求,獲得返回結果HttpResponse
HttpClient http = new DefaultHttpClient();
HttpResponse response = http.execute(get);

// (3) 讀取返回結果
if (response.getStatusLine().getStatusCode() == 200) {
    HttpEntity entity = response.getEntity();

    InputStream in = entity.getContent();
    readResponse(in);
}

  

(1)HttpGet的實例就是一個get請求,構造函數只有一個字符串參數,即要獲取的網頁地址。另外一種構造形式是使用URI實例作爲HttpGet的參數。HttpComponents提供了URIUtils類,它的createURI()返回一個URI實例,將請求地址拆分構造不失爲一種更加清晰的方式。

    URI uri = URIUtils.createURI("http", "www.126.com", 80, "/", "", null);
    HttpGet get = new HttpGet(uri);

 

(2)請求最後被HttpClient發送出去,new DefaultHttpClient()創建一個基本的HttpClient實例。由於底層是基於阻塞的JAVA I/O模型,執行execute()的時間與具體請求的遠程服務器和網絡速度有關,在實際運行場景中應特別注意此問題。如果是在tomcat等環境中執行可能會造成線程等待,浪費服務器資源,或拒絕其它的連接。

 

(3)請求返回後就可以讀取返回內容了,但有一個前提是此次請求是否真的成功了?服務器地址錯誤,或請求的頁面不存在等問題都會讓請求失敗。爲了確保得到了正確的響應首先應判斷返回碼是否正確。調用response.getStatusLine()返回一個StatusLine的實例,此實例描述了一次請求的響應信息。一個成功響應的StatusLine實例本身包含如下信息:

 

    HTTP/1.0 200 OK

 

HTTP/1.0:是請求協議和版本號

200:是響應碼

 

StatusLine的下面2個方法分別用於獲取響應信息的各部分內容

getProtocolVersion(): 得到請求協議和協議版本號,如HTTP/1.0

getStatusCode():得到響應碼,如200

 

HttpEntity entity = response.getEntity()返回一個HttpEntity實例,進而調用getContent()就得到了一個輸入流。後面的事情應該很明確了。readResponse()是一個自己寫的讀取輸入流中字符串的方法,代碼如下:

public static void readResponse(InputStream in) throws Exception{

    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

 

4 Post請求

    Post請求在代碼上與Get請求的主要區別是將HttpGet換成了HttpPost,其餘部分代碼基本一致。請看代碼:

// (1) 創建HttpGet實例
HttpPost post = new HttpPost("http://www.126.com");

// (2) 使用HttpClient發送get請求,獲得返回結果HttpResponse
HttpClient http = new DefaultHttpClient();
HttpResponse response = http.execute(post);

// (3) 讀取返回結果
if (response.getStatusLine().getStatusCode() == 200) {
    HttpEntity entity = response.getEntity();

    InputStream in = entity.getContent();
    readResponse(in);
}

  

與Get請求不同的代碼被標註爲紅色。可見Post請求與Get請求在代碼上的區別並不大,互相切換也是比較容易的。在下面可以看到的令一個不同之處是傳遞的查詢字符串,即請求參數。

 

5 參數傳遞

    Get與Post在傳遞參數時有一些區別,Get請求的參數作爲查詢字符串出傳遞,而Post請求的參數則作爲實體傳遞。在開發WEB項目時經常遇到亂碼的問題,使用HttpComponents也會涉及到這個問題,所以在使用時應特別注意。服務器端的處理方法與WEB項目相同, HttpComponents只要注意字符編碼就可以了。

 

Get請求傳遞參數方法一:將查詢字符串作爲請求地址的一部分

這是一種最簡單的傳參方式,將查詢參數用(&)連接,然後放在請求地址?的後面,如下面這個請求地址

 

http://localhost:8080/servlet1?name=ahopedog&work=programer

 

請看代碼:

 

// (1) 創建HttpGet實例
HttpGet get = new HttpGet("http://localhost:8080/jsx/servlet?id=007");

 

這裏只對請求地址稍作了些修改,請求地址是http://localhost:8080/jsx/servlet

查詢參數是id=007

請求地址與查詢參數之間用?連接

 

Get請求傳遞參數方法二:使用URI攜帶查詢字符串

還記得上面提到的HttpGet有一種用URI構造的方法嗎?這第二種傳遞的方式就是藉助了這個機制,只是HttpComponents提供了一種創建查詢參數比較清晰的方式NameValuePair

// (1)創建查詢參數
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("name", "ahopedog"));
params.add(new BasicNameValuePair("work", "程序員"));
String queryString = URLEncodedUtils.format(params, "utf-8");

// (2) 創建Get實例
URI uri = URIUtils.createURI("http", "localhost", 8080, "/jsx/servlet", queryString, null);
HttpGet get = new HttpGet(uri);

  

(1)NameValuePair用一對鍵、值表示一個查詢參數,將多個NameValuePair放在一個List中,就形成了一組查詢參數。但是List<NameValuePair>並不能直接被HttpGet使用,所以需要用URLEncodedUtils.format()方法將其編碼成字符串。URLEncodedUtils是HttpComponents提供的一個編譯查詢字符串的工具類。

(2)使用編譯好的查詢字符串構造URI對象,這樣查詢參數就一起被髮送到了服務器上。

其實,這裏的查詢字符串完全可以手工的方式拼湊出來,只是,從代碼的清晰性和維護性方面考慮,NameValuePair和URLEncodedUtils的方式更加可取。值得一提的是,在開發J2EE項目時,經常遇到一些查詢條件或請求條件衆多的情況,有的是將多個值放在一個Map中管理,有的則創建一個固定結構的Java Bean類。在這方面不同人可能會有不同的看法。Map方式固然省事,而且也很靈活,但是如果缺少了文檔和註釋時,會很難知道這個Map中放的到底是什麼。而創建成Java Bean的話,代碼本身就是一個很好的說明,讓人一目瞭然,缺點是導致Java Bean的急劇增加,以致混亂和難以管理。

本人想不到什麼很完美的解決辦法,但是,任何極端的方式都是不可取,我想在這時折中或許稍好些,什麼方法由場景決定。

Post請求傳遞參數:

// (1) Post請求
HttpPost post = new HttpPost("http://localhost:8080/jsx/servlet");

//添加參數
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("name", "ahopedog"));
params.add(new BasicNameValuePair("work", "程序員"));
post.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));

// (3) 發送請求
HttpClient http = new DefaultHttpClient();
HttpResponse response = http.execute(post);

  

也是用到了List<NameValuePair>組織參數,這樣就不用費更多心思研究新的方式了。將請求參數加入查詢是上面代碼中紅色文字的一行。很簡單,只要別把你的字符編碼搞錯就行了。

6 響應

直接操作響應中的輸入流是最直接也是最有效的方式,不過需要注意的一點是,輸入流讀取完以後一定要將其關閉。

其實在前面的代碼中已經涉及過了對響應的處理,這裏再加詳細的給予說明:

// (1) Post請求 
HttpPost post = new HttpPost("http://www.126.com"); 

// (2) 發送請求
HttpClient http = new DefaultHttpClient();
HttpResponse response = http.execute(post);

// (3) 處理響應結果
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();

// (4) 從輸入流讀取網頁字符串內容
System.out.println(entity.getContentType());
System.out.println(entity.getContentEncoding());
System.out.println(entity.getContentLength());

InputStream in = null;
try{
in = entity.getContent();

BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
} 

}finally{
//記得關閉輸入流
if(in != null)
in.close();
}

  


在本例中只需關心標記爲紅色的代碼,其它行的代碼上面已經介紹過了。

HttpEntity有3個獲取返回數據的描述信息(或叫做元數據)

getContentType():獲取響應體的類型

getContentEncoding():獲取響應體的字符編碼

getContentLength():獲取響應體的字節長度

元數據的內容由遠程服務器返回,實際上這些信息是包含在響應的頭部信息中的,HTTP請求的響應頭中還包含了其它有用的信息,HttpComponents將返回頭中的關鍵元數據封裝到了HttpEntity中,已便於使用。

entity.getContent()可以得到響應體的InputStream,有了這個流對象,基本上就可以"爲所欲爲"了。因爲InputStream是Java I/O中底層的基礎類,結合相對上層的輸入流對象或者對字節進行編碼等方法就可以獲得不同類型和形式的響應數據了。在本例中用BufferedReader將響應體以字符串形式讀取(返回的內容確實也是字符串的內容)。

 

7 headers

頭部信息在客戶端與服務器的HTTP傳輸過程中提供元數據,如服務器類型、處理時間、內容長度、內容類型等。

7.1 請求頭部信息

下圖是用Firefox訪問Google時獲取到的請求頭信息,在我們使用瀏覽器訪問一個網址時,瀏覽器都會默默的將一些與請求和客戶端相關的信息發送給服務器,讓服務器能更好的處理特定的客戶端請求。這裏面也包括了Cookie。

 

從上圖中可以看到,在發送給服務器的頭信息裏,Host是我們訪問的遠程服務器主機地址。User-Agent是瀏覽器標識,服務器程序可以通過這個字符串得知客戶端瀏覽器的類型和操作系統等信息。Accept-Charset則是客戶端可以接受的字符編碼類型。

其它各參數的含義在這裏就不再一一說明了,感興趣的讀者可以參考相關文章或在Google上搜索相關的資料。

我們是使用HttpComponents代替瀏覽器訪問服務器,默認情況並不包含瀏覽器所傳遞的頭信息。即使不傳遞這些信息,通常服務器也會正常返回你所要的網頁HTML內容的。但如果你真的很無聊,或者有一些特殊的操作,則完全可以模擬瀏覽給遠程服務器發送這些頭信息。

// (1) Post請求
HttpPost post = new HttpPost("http://www.126.com");

// (2) 添加請求頭信息
post.setHeader("User-Agent", "Ahopedog/5.0 (Linux NT 5.1; rv:5.0) Gecko/20100101 FireDog/5.0");
post.setHeader("Accept-Charset", "GB2312,utf-8;q=0.7,*;q=0.7");

// (3) 發送請求
HttpClient http = new DefaultHttpClient();
http.execute(post);

  

上面兩行紅色代碼向HttpPost中添加了User-Agent, Accept-Charset兩個頭信息(內容可以隨意設置),頭信息會隨着HttpClient的execute一起發送出去。

7.2 響應頭部信息

還是先在瀏覽器中都會得到什麼樣的響應頭

 

上圖內容是從FireBug返回頭的截圖,返回頭包含了服務器時間,緩存控制,返回內容編碼,服務器等信息。這裏很有意思的一處是Google的服務器是gws,而百度的服務器則是BWS/1.0,從沒見過的服務器,看來是自主研發的,不過名字也用不着太相近吧。

接下來,看看HttpComponents是如何解析這些信息的

// (1) Post請求 
HttpPost post = new HttpPost("http://www.126.com"); 

// (2) 發送請求
HttpClient http = new DefaultHttpClient();
HttpResponse response = http.execute(post);

// (3) 遍歷返回頭
Header[] headers = response.getAllHeaders(); 
for(Header h : headers){
System.out.println(h.getName() + " : " + h.getValue()); 
}
System.out.println("======================================");

// (4) 獲取Server頭信息,頭名字不區分大小寫
Header serverHeader = response.getFirstHeader("server");
System.out.println(serverHeader.getName() + " : " + serverHeader.getValue());

  

(3)response.getAllHeaders()得到響應頭數組,一個響應頭封裝成一個Header實例。Header的兩個關鍵方法是getName()和getValue(),得到頭名字和值。

(4)response.getFirstHeader()可以指定獲取一個特定的頭,需要指定頭的名字。多個頭名字是可以重名的,而getFirstHeader是得到同名頭中的第一個。

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章