閱讀675 返回首頁    go 阿裏雲 go 技術社區[雲棲]


java之httpclient的一些破事

本文偏重使用,簡單講述httpclient,其實在網絡編程中,基於java的實現幾乎都是包裝了socket的通信,然後來模擬各種各樣的協議;httpclient其實就是模擬瀏覽器發起想服務器端的請求,而這種更加類似於JS的請求或頁麵的POST、GET,不過這種數據的返回一般需要得到有意義的數據,才方便做其他的交互,否則得到一個頁麵結果,全是標簽了,畢竟不是瀏覽器,所以我們用httpclient更多使得係統的交互更加的簡單,本文從如何使用httpclient開始說明到性能的優化方法切入:

1、httpclient客戶端調用例子,以及服務器端需要做什麼。

2、安全性怎麼樣去控製。

3、httpclient在並發量較高的調用下問題如何去解決。


1、httpclient客戶端調用例子,以及服務器端需要做什麼。

首先說明服務器端需要做什麼,httpclient模擬的是一個瀏覽器,要服務器端進行數據交互,那麼就是和瀏覽器一樣發起請求,接受請求的操作,但是它和瀏覽器與服務器交互最大的區別是它沒有登錄動作,當然也可以通過模擬登錄來完成cookie的獲取,但是這個代碼寫起來就費勁了,而且這個賬號必須在你的代碼中寫明登錄賬號才能獲取到cookie,如果程序是單獨自己用還是可以的,如果很多人用就有點亂了,因為每個人的密碼你也得用某種方法傳遞到服務器端,但是沒有cookie很明顯會被服務器端攔截到某個直接的登錄界麵上去,從而得不到自己想要的數據,所以,我們先拋開安全性問題,那麼就是將部分URL開放出來,也就是不經過過濾器的URL,或將某些目錄單獨開放出來訪問,安全性的問題,第二章來討論,OK,如果服務器端開放了一個URL路徑後,客戶端訪問就像瀏覽器訪問一個URL一樣簡單,用httpclient如何去訪問呢?

在使用之前,需要先了解,httpclient是apache提供的,所以需要先引入相關的包,要使用它基本需要幾個包:

commons-logging、commons-httpclient、commons-codec具體的版本以及引入方式請自己根據項目和工程打包方法決定,目前來講maven引入是比較方便的方法,然後在代碼前麵引入:

import org.apache.commons.httpclient.*;

<順便說下 .* 這個說法,有人說用這個 * 是很慢的,對於現在的JVM來說隻能說它是在亂說,一個一個引入唯一的好處是可以很快知道這個類是那個包下麵來的,但是絕對不是提高性能,jvm在編譯時早就決定了哪些是需要的,哪些是不需要的,如果引入同一個包太多,即使每個單獨寫,jvm也會給改成*,JVM的內存結構也不會因為某個類多引入幾個*,就會修改他們之間的鏈接結構,初始化是由父子集成關係以及包裝關係決定的,而運行是優化器決定的>


首先來看一個Get請求的非常簡單的例子:

try {
    HttpClient client = new HttpClient();//定義client對象
    client.getHttpConnectionManager().getParams().setConnectionTimeout(2000);//設置連接超時時間為2秒(連接初始化時間)
    GetMethod method = new GetMethod("https://www.google.com.hk/");//訪問下穀歌的首頁
    int statusCode = client.executeMethod(method);//狀態,一般200為OK狀態,其他情況會拋出如404,500,403等錯誤
    if (statusCode != HttpStatus.SC_OK) {
       System.out.println("遠程訪問失敗。");
    }
    System.out.println(method.getResponseBodyAsString());//輸出反饋結果
    client.getHttpConnectionManager().closeIdleConnections(1);
}catch(....) {.....}


注意,上述反饋結果可能和你用一個socket去模擬一些係統沒有什麼區別,因為返回的內容沒有任何價值,都是頁麵標簽,當你和另一個係統交互時,它做response數據時,可以返回指定的json、xml等格式,用處就非常好用了,下麵還會提及到它的好處;注意,采用GET方法,參數放在URL上麵,要將非英文字符傳遞過去,需要對數據進行編碼,如:

String url = "https://www.xxx.xxx.com/xxx?name=" + URLEncoder.encode("謝宇" , "GBK") + "&otherName=" + URLEncoder.encode("謝宇" , "GBK") ;

其中URLEncoder使用apache或jdk自帶的均可。


順便再寫個POST的例子:

try {
    HttpClient client = new HttpClient();//定義client對象
    client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "GBK");//指定傳送字符集為GBK格式
    client.getHttpConnectionManager().getParams().setConnectionTimeout(2000);//設置連接超時時間為2秒(連接初始化時間)
    PostMethod method = new PostMethod("https://www.xxx.xxx.com/aaa/bbb.do");
    method.setRequestBody(new NameValuePair[] {
                  new NameValuePair("name" , "謝宇"), new NameValuePair("otherName" , "謝宇")
    });
    int statusCode = client.executeMethod(method);
    if (statusCode != HttpStatus.SC_OK) {
       System.out.println("遠程訪問失敗。");
    }
    System.out.println(method.getResponseBodyAsString());//輸出反饋結果
    client.getHttpConnectionManager().closeIdleConnections(1);
}catch(....) {.....}

可以看出,Post的例子和Get差不多,唯一的區別是傳入參數的方法post采用了單獨逐個寫參數的方法,而get是在URL上麵。

如果上麵兩個例子,在你的機器上跑通了,那麼好,我們開始來討論它的安全性問題:


2、安全性怎麼樣去控製

其實接口既然是人定義的,也就是協議是自己控製的,我們基於了一種輕量級的編程模式,讓服務器端和客戶端編碼都變得十分簡單,簡單了,安全問題來了,誰都可以調用,如果你的接口是十分open的,那麼這不是問題,隻是控製好並發就可以,但是如果存在安全隱患的話,那麼就有問題了,這個需要雙方定義好一個加密方法,加密的粒度可以根據實際情況來決定,這種傳送最好不要使用簡單加密,一般有兩種方法:

其一:使用不可逆加密算法(如:MD5,注意md5在對中文數據加密時,采用不同字符集轉碼加密出來結果也不一樣),不可逆加密算法,就必須要雙方都約定一個協議,在傳遞的參數上增加一個加密後的token值,其餘的參數照樣傳遞,加密過程為使用某種key與數據本身進行組合,並且存在一些動態變化性,將加密數據作為一個參數傳遞到接收方,接收方使用相同的方法得到一個密文,兩個密文進行對比,若對比一致,則認證成功,若對比不一致,則認證失敗;這樣做算是比較簡單的方法,有些還用了可逆和不可逆同時來用,先將數據按照可逆算法加密,然後再計算token,不過比較複雜些了。

其二:可逆加密算法,但是這種可逆,需要有一個密匙,最好的是非對稱密匙,而且最好是雙方的密匙可以隨著某個值而變化,而不是固定的密匙。非對稱密匙比較複雜,如果要用的話,這個可能會比較麻煩,安全級別極高的可以考慮,如果你的密匙本身可以隨著某種方式得到一個變化,使用對稱也基本夠用,如:Blowfish就還算是不錯的,但是沒有密匙的就別用了,類似Base64就太簡單了;當然你也可以像上麵說的,可逆和不可逆混用來提高安全級別。


3、httpclient在並發量較高的調用下問題如何去解決

前麵有提及到httpclient模擬係統之間的交互,如果係統之間的交互不高,是非常輕鬆的動作,不過httpclient是作為WEB容器的web請求存在,在http協議下,都是無狀態的協議,也就是連接-請求-反饋-斷開幾個基本動作,好在現在WEB容器有了keep-alive的功能,包括很多負載均衡設備:如:LB、LVS、nginx、apache、jboss、tomcat等等都是支持的,雖然支持,但是看看上麵的代碼,就發現,每次請求都會重新建立連接,如何讓他們不要重複創建連接呢?或者說在服務器端沒有斷開前不要重複創建連接,一個連接可以被使用多次請求,不至於一次請求就被斷開一次;建立一次連接需要三次握手過程,以及更多的網絡開銷,所以你懂的。

道理很簡單,其實和鏈接數據庫差不多,將上麵的請求的client對象以及method對象作為共享變量時,發起多次請求,平均效率會提升2倍左右,注意,這裏是循環測試,而不是多線程。

但是對於並發較高的,我們不可能將method隻用一個,因為它本身不能並發,於是我們就要用多個,在多個共享的對象中,如果控製好征用,有涉及到連接池的問題,不過這個連接池相對數據庫的連接池要簡單很多,因為,重試等動作,apache已經為你包裝好了,你隻需要順序找和分配就可以了,如何降低競爭就是算法和策略的問題了。

但是,讓客戶端來編寫這麼一段代碼是不是有點過分,當然你願意寫也是可以的,其實apache又為我們提供了一個後麵就是異步httpclient(其實這裏所知的異步並非真正的異步IO模式),也就是將這部分包裝了,對於訪問者來說還是同步的,隻是在IO層麵是非阻塞的了,這個就配合了服務器端的keep-alive,就像服務器端同時向一個站點請求多個資源時,我們希望是一個連接,而不是多個鏈接,其實在很多瀏覽器(如chrome、FF)都可以監控到它同時請求的服務器端資源,那麼要用httpclient實現異步IO應該如何來做呢?其實也蠻簡單的,下麵是一個簡單例子:

首先你要增加一個關於異步IO需要的包:

1、async-http-client包,可以在這裏下載:https://oss.sonatype.org/content/repositories/releases/com/ning/async-http-client/1.6.2/

2、log4j的包,這個不用我說了,都知道在哪裏

3、slf4j-spi 的包,目前用1.5以上的版本比較多。

4、slf4j-log4j 的包,可以看出,slf4j是在log4j基礎上包裝的。

OK,就這幾個了,弄好後再看看下麵這段代碼,通過使用它,性能可以得到明顯改善:

AsyncHttpClient client = new AsyncHttpClient();
try {
   Future<Response> f = client.prepareGet("https://www.google.com.hk/").execute();
   System.out.println(f.get().getResponseBody("Big5"));//穀歌的輸出編碼集為Big5,反向解析結果的時候使用
}catch(...) {....}


這段代碼是不是超級簡單,可以通過上麵描述的三種方式:

1、直接調用

2、將GetMethod或PostMethod對象作為共享對象反複使用。

3、使用AsyncHttpClient

這三種方法,非別使用一次調用、循環多次調用、並發調用來測試性能,後麵兩者的性能比第一種方法的性能要高很多。

最後更新:2017-04-02 06:52:21

  上一篇:go 自動裝箱與拆箱引發的享元設計模式
  下一篇:go 查詢數據表裏所有重複裏的單條記錄