| | 1 | [[PageOutline]] |
| | 2 | |
| | 3 | 一般的情況下我們都是使用IE或者Navigator瀏覽器來訪問一個WEB服務器,用來瀏覽頁面查看信息或者提交一些數據等等。所訪問的這些頁面有的僅僅是一些普通的頁面,有的需要用戶登錄後方可使用,或者需要認證以及是一些通過加密方式傳輸,例如HTTPS。目前我們使用的瀏覽器處理這些情況都不會構成問題。不過你可能在某些時候需要通過程序來訪問這樣的一些頁面,比如從別人的網頁中「偷」一些數據;利用某些站點提供的頁面來完成某種功能,例如說我們想知道某個手機號碼的歸屬地而我們自己又沒有這樣的數據,因此只好借助其他公司已有的網站來完成這個功能,這個時候我們需要向網頁提交手機號碼並從返回的頁面中解析出我們想要的數據來。如果對方僅僅是一個很簡單的頁面,那我們的程序會很簡單,本文也就沒有必要大張旗鼓的在這裡浪費口舌。 |
| | 4 | |
| | 5 | |
| | 6 | 但是考慮到一些服務授權的問題,很多公司提供的頁面往往並不是可以通過一個簡單的URL就可以訪問的,而必須經過註冊然後登錄後方可使用提供服務的頁面,這個時候就涉及到 COOKIE問題的處理。我們知道目前流行的動態網頁技術例如ASP、JSP無不是通過COOKIE來處理會話信息的。為了使我們的程序能使用別人所提供的服務頁面,就要求程序首先登錄後再訪問服務頁面,這過程就需要自行處理cookie,想想當你用java.net.HttpURLConnection 來完成這些功能時是多麼恐怖的事情啊!況且這僅僅是我們所說的頑固的WEB服務器中的一個很常見的「頑固」!再有如通過HTTP來上傳文件呢?不需要頭疼,這些問題有了「它」就很容易解決了! |
| | 7 | |
| | 8 | |
| | 9 | 我們不可能列舉所有可能的頑固,我們會針對幾種最常見的問題進行處理。當然了,正如前面說到的,如果我們自己使用java.net.HttpURLConnection來搞定這些問題是很恐怖的事情,因此在開始之前我們先要介紹一下一個開放源碼的項目,這個項目就是Apache開源組織中的httpclient,它隸屬於Jakarta的commons項目,目前的版本是2.0RC2。 commons下本來已經有一個net的子項目,但是又把httpclient單獨提出來,可見http服務器的訪問絕非易事。 |
| | 10 | |
| | 11 | |
| | 12 | Commons-httpclient項目就是專門設計來簡化HTTP客戶端與服務器進行各種通訊編程。通過它可以讓原來很頭疼的事情現在輕鬆的解決,例如你不再管是HTTP或者HTTPS的通訊方式,告訴它你想使用HTTPS方式,剩下的事情交給httpclient替你完成。本文會針對我們在編寫 HTTP客戶端程序時經常碰到的幾個問題進行分別介紹如何使用httpclient來解決它們,為了讓讀者更快的熟悉這個項目我們最開始先給出一個簡單的例子來讀取一個網頁的內容,然後循序漸進解決掉前進中的所有問題。 |
| | 13 | |
| | 14 | = 1. 讀取網頁(HTTP/HTTPS)內容 = |
| | 15 | |
| | 16 | 下面是我們給出的一個簡單的例子用來訪問某個頁面 |
| | 17 | |
| | 18 | {{{ |
| | 19 | #!java |
| | 20 | /* |
| | 21 | * Created on 2003-12-14 by Liudong |
| | 22 | */ |
| | 23 | package http.demo; |
| | 24 | import java.io.IOException; |
| | 25 | import org.apache.commons.httpclient.*; |
| | 26 | import org.apache.commons.httpclient.methods.*; |
| | 27 | /** |
| | 28 | * 最簡單的HTTP客戶端,用來演示通過GET或者POST方式訪問某個頁面 |
| | 29 | * @author Liudong |
| | 30 | */ |
| | 31 | public class SimpleClient { |
| | 32 | public static void main(String[] args) throws IOException |
| | 33 | { |
| | 34 | HttpClient client = new HttpClient(); |
| | 35 | //設置代理服務器地址和端口 |
| | 36 | //client.getHostConfiguration().setProxy("proxy_host_addr",proxy_port); |
| | 37 | //使用GET方法,如果服務器需要通過HTTPS連接,那只需要將下面URL中的http換成https |
| | 38 | HttpMethod method = new GetMethod("http://java.sun.com"); |
| | 39 | //使用POST方法 |
| | 40 | //HttpMethod method = new PostMethod("http://java.sun.com"); |
| | 41 | client.executeMethod(method); |
| | 42 | //打印服務器返回的狀態 |
| | 43 | System.out.println(method.getStatusLine()); |
| | 44 | //打印返回的信息 |
| | 45 | System.out.println(method.getResponseBodyAsString()); |
| | 46 | //釋放連接 |
| | 47 | method.releaseConnection(); |
| | 48 | } |
| | 49 | } |
| | 50 | }}} |
| | 51 | |
| | 52 | 在這個例子中首先創建一個HTTP客戶端(HttpClient)的實例,然後選擇提交的方法是GET或者POST,最後在HttpClient實例上執行提交的方法,最後從所選擇的提交方法中讀取服務器反饋回來的結果。這就是使用HttpClient的基本流程。其實用一行代碼也就可以搞定整個請求的過程,非常的簡單! |
| | 53 | |
| | 54 | |
| | 55 | = 2. 以GET或者POST方式向網頁提交參數 = |
| | 56 | |
| | 57 | |
| | 58 | 其實前面一個最簡單的示例中我們已經介紹了如何使用GET或者 POST方式來請求一個頁面,本小節與之不同的是多了提交時設定頁面所需的參數,我們知道如果是GET的請求方式,那麼所有參數都直接放到頁面的URL後面用問號與頁面地址隔開,每個參數用&隔開,例如:http://java.sun.com?name=liudong& mobile=123456,但是當使用POST方法時就會稍微有一點點麻煩。本小節的例子演示向如何查詢手機號碼所在的城市,代碼如下: |
| | 59 | |
| | 60 | {{{ |
| | 61 | #!java |
| | 62 | /* |
| | 63 | * Created on 2003-12-7 by Liudong |
| | 64 | */ |
| | 65 | package http.demo; |
| | 66 | import java.io.IOException; |
| | 67 | import org.apache.commons.httpclient.*; |
| | 68 | import org.apache.commons.httpclient.methods.*; |
| | 69 | /** |
| | 70 | * 提交參數演示 |
| | 71 | * 該程序連接到一個用於查詢手機號碼所屬地的頁面 |
| | 72 | * 以便查詢號碼段1330227所在的省份以及城市 |
| | 73 | * @author Liudong |
| | 74 | */ |
| | 75 | public class SimpleHttpClient { |
| | 76 | public static void main(String[] args) throws IOException |
| | 77 | { |
| | 78 | HttpClient client = new HttpClient(); |
| | 79 | client.getHostConfiguration().setHost("www.imobile.com.cn", 80, "http"); |
| | 80 | HttpMethod method = getPostMethod();//使用POST方式提交數據 |
| | 81 | client.executeMethod(method); |
| | 82 | //打印服務器返回的狀態 |
| | 83 | System.out.println(method.getStatusLine()); |
| | 84 | //打印結果頁面 |
| | 85 | String response = |
| | 86 | new String(method.getResponseBodyAsString().getBytes("8859_1")); |
| | 87 | //打印返回的信息 |
| | 88 | System.out.println(response); |
| | 89 | method.releaseConnection(); |
| | 90 | } |
| | 91 | /** |
| | 92 | * 使用GET方式提交數據 |
| | 93 | * @return |
| | 94 | */ |
| | 95 | private static HttpMethod getGetMethod(){ |
| | 96 | return new GetMethod("/simcard.php?simcard=1330227"); |
| | 97 | } |
| | 98 | /** |
| | 99 | * 使用POST方式提交數據 |
| | 100 | * @return |
| | 101 | */ |
| | 102 | private static HttpMethod getPostMethod(){ |
| | 103 | PostMethod post = new PostMethod("/simcard.php"); |
| | 104 | NameValuePair simcard = new NameValuePair("simcard","1330227"); |
| | 105 | post.setRequestBody(new NameValuePair[] { simcard}); |
| | 106 | return post; |
| | 107 | } |
| | 108 | } |
| | 109 | |
| | 110 | }}} |
| | 111 | |
| | 112 | 在上面的例子中頁面http://www.imobile.com.cn/simcard.php需要一個參數是simcard,這個參數值為手機號碼段,即手機號碼的前七位,服務器會返回提交的手機號碼對應的省份、城市以及其他詳細信息。GET的提交方法只需要在URL後加入參數信息,而POST則需要通過NameValuePair類來設置參數名稱和它所對應的值 |
| | 113 | |
| | 114 | = 3. 處理頁面重定向 = |
| | 115 | |
| | 116 | 在JSP/Servlet編程中 response.sendRedirect方法就是使用HTTP協議中的重定向機制。它與JSP中的<jsp:forward …>的區別在於後者是在服務器中實現頁面的跳轉,也就是說應用容器加載了所要跳轉的頁面的內容並返回給客戶端;而前者是返回一個狀態碼,這些狀態碼的可能值見下表,然後客戶端讀取需要跳轉到的頁面的URL並重新加載新的頁面。就是這樣一個過程,所以我們編程的時候就要通過 HttpMethod.getStatusCode()方法判斷返回值是否為下表中的某個值來判斷是否需要跳轉。如果已經確認需要進行頁面跳轉了,那麼可以通過讀取HTTP頭中的location屬性來獲取新的地址。 |
| | 117 | |
| | 118 | {{{ |
| | 119 | #!text |
| | 120 | 狀態碼 |
| | 121 | 對應HttpServletResponse的常量 |
| | 122 | 詳細描述 |
| | 123 | 301 |
| | 124 | SC_MOVED_PERMANENTLY |
| | 125 | 頁面已經永久移到另外一個新地址 |
| | 126 | 302 |
| | 127 | SC_MOVED_TEMPORARILY |
| | 128 | 頁面暫時移動到另外一個新的地址 |
| | 129 | 303 |
| | 130 | SC_SEE_OTHER |
| | 131 | 客戶端請求的地址必須通過另外的URL來訪問 |
| | 132 | 307 |
| | 133 | SC_TEMPORARY_REDIRECT |
| | 134 | 同SC_MOVED_TEMPORARILY |
| | 135 | }}} |
| | 136 | |
| | 137 | 下面的代碼片段演示如何處理頁面的重定向 |
| | 138 | |
| | 139 | {{{ |
| | 140 | #!java |
| | 141 | client.executeMethod(post); |
| | 142 | System.out.println(post.getStatusLine().toString()); |
| | 143 | post.releaseConnection(); |
| | 144 | |
| | 145 | //檢查是否重定向 |
| | 146 | int statuscode = post.getStatusCode(); |
| | 147 | if ((statuscode == HttpStatus.SC_MOVED_TEMPORARILY) || |
| | 148 | (statuscode == HttpStatus.SC_MOVED_PERMANENTLY) || |
| | 149 | (statuscode == HttpStatus.SC_SEE_OTHER) || |
| | 150 | (statuscode == HttpStatus.SC_TEMPORARY_REDIRECT)) { |
| | 151 | //讀取新的URL地址 |
| | 152 | Header header = post.getResponseHeader("location"); |
| | 153 | if (header != null) { |
| | 154 | String newuri = header.getValue(); |
| | 155 | if ((newuri == null) || (newuri.equals(""))) |
| | 156 | newuri = "/"; |
| | 157 | GetMethod redirect = new GetMethod(newuri); |
| | 158 | client.executeMethod(redirect); |
| | 159 | System.out.println("Redirect:"+ redirect.getStatusLine().toString()); |
| | 160 | redirect.releaseConnection(); |
| | 161 | } else |
| | 162 | System.out.println("Invalid redirect"); |
| | 163 | } |
| | 164 | }}} |
| | 165 | 我們可以自行編寫兩個JSP頁面,其中一個頁面用response.sendRedirect方法重定向到另外一個頁面用來測試上面的例子。 |
| | 166 | |
| | 167 | = 4. 模擬輸入用戶名和口令進行登錄 = |
| | 168 | |
| | 169 | 本小節應該說是HTTP客戶端編程中最常碰見的問題,很多網站的內容都只是對註冊用戶可見的,這種情況下就必須要求使用正確的用戶名和口令登錄成功後,方可瀏覽到想要的頁面。因為HTTP協議是無狀態的,也就是連接的有效期只限於當前請求,請求內容結束後連接就關閉了。在這種情況下為了保存用戶的登錄信息必須使用到Cookie機制。以JSP/Servlet為例,當瀏覽器請求一個JSP或者是Servlet的頁面時,應用服務器會返回一個參數,名為 jsessionid(因不同應用服務器而異),值是一個較長的唯一字符串的Cookie,這個字符串值也就是當前訪問該站點的會話標識。瀏覽器在每訪問該站點的其他頁面時候都要帶上jsessionid這樣的Cookie信息,應用服務器根據讀取這個會話標識來獲取對應的會話信息。 |
| | 170 | |
| | 171 | 對於需要用戶登錄的網站,一般在用戶登錄成功後會將用戶資料保存在服務器的會話中,這樣當訪問到其他的頁面時候,應用服務器根據瀏覽器送上的Cookie中讀取當前請求對應的會話標識以獲得對應的會話信息,然後就可以判斷用戶資料是否存在於會話信息中,如果存在則允許訪問頁面,否則跳轉到登錄頁面中要求用戶輸入帳號和口令進行登錄。這就是一般使用JSP開發網站在處理用戶登錄的比較通用的方法。 |
| | 172 | |
| | 173 | 這樣一來,對於HTTP的客戶端來講,如果要訪問一個受保護的頁面時就必須模擬瀏覽器所做的工作,首先就是請求登錄頁面,然後讀取Cookie值;再次請求登錄頁面並加入登錄頁所需的每個參數;最後就是請求最終所需的頁面。當然在除第一次請求外其他的請求都需要附帶上Cookie信息以便服務器能判斷當前請求是否已經通過驗證。說了這麼多,可是如果你使用 httpclient的話,你甚至連一行代碼都無需增加,你只需要先傳遞登錄信息執行登錄過程,然後直接訪問想要的頁面,跟訪問一個普通的頁面沒有任何區別,因為類HttpClient已經幫你做了所有該做的事情了,太棒了!下面的例子實現了這樣一個訪問的過程。 |
| | 174 | |
| | 175 | |
| | 176 | {{{ |
| | 177 | #!java |
| | 178 | /* |
| | 179 | * Created on 2003-12-7 by Liudong |
| | 180 | */ |
| | 181 | package http.demo; |
| | 182 | import org.apache.commons.httpclient.*; |
| | 183 | import org.apache.commons.httpclient.cookie.*; |
| | 184 | import org.apache.commons.httpclient.methods.*; |
| | 185 | /** |
| | 186 | * 用來演示登錄表單的示例 |
| | 187 | * @author Liudong |
| | 188 | */ |
| | 189 | public class FormLoginDemo { |
| | 190 | static final String LOGON_SITE = "localhost"; |
| | 191 | static final int LOGON_PORT = 8080; |
| | 192 | |
| | 193 | public static void main(String[] args) throws Exception{ |
| | 194 | HttpClient client = new HttpClient(); |
| | 195 | client.getHostConfiguration().setHost(LOGON_SITE, LOGON_PORT); |
| | 196 | |
| | 197 | //模擬登錄頁面login.jsp->main.jsp |
| | 198 | PostMethod post = new PostMethod("/main.jsp"); |
| | 199 | NameValuePair name = new NameValuePair("name", "ld"); |
| | 200 | NameValuePair pass = new NameValuePair("password", "ld"); |
| | 201 | post.setRequestBody(new NameValuePair[]{name,pass}); |
| | 202 | int status = client.executeMethod(post); |
| | 203 | System.out.println(post.getResponseBodyAsString()); |
| | 204 | post.releaseConnection(); |
| | 205 | |
| | 206 | //查看cookie信息 |
| | 207 | CookieSpec cookiespec = CookiePolicy.getDefaultSpec(); |
| | 208 | Cookie[] cookies = cookiespec.match(LOGON_SITE, LOGON_PORT, "/", false, client.getState().getCookies()); |
| | 209 | if (cookies.length == 0) { |
| | 210 | System.out.println("None"); |
| | 211 | } else { |
| | 212 | for (int i = 0; i < cookies.length; i++) { |
| | 213 | System.out.println(cookies[i].toString()); |
| | 214 | } |
| | 215 | } |
| | 216 | //訪問所需的頁面main2.jsp |
| | 217 | GetMethod get = new GetMethod("/main2.jsp"); |
| | 218 | client.executeMethod(get); |
| | 219 | System.out.println(get.getResponseBodyAsString()); |
| | 220 | get.releaseConnection(); |
| | 221 | } |
| | 222 | } |
| | 223 | }}} |
| | 224 | |
| | 225 | = 5. 提交XML格式參數 = |
| | 226 | |
| | 227 | 提交XML格式的參數很簡單,僅僅是一個提交時候的ContentType問題,下面的例子演示從文件文件中讀取XML信息並提交給服務器的過程,該過程可以用來測試Web服務。 |
| | 228 | |
| | 229 | {{{ |
| | 230 | #!java |
| | 231 | import java.io.File; |
| | 232 | import java.io.FileInputStream; |
| | 233 | import org.apache.commons.httpclient.HttpClient; |
| | 234 | import org.apache.commons.httpclient.methods.EntityEnclosingMethod; |
| | 235 | import org.apache.commons.httpclient.methods.PostMethod; |
| | 236 | /** |
| | 237 | * 用來演示提交XML格式數據的例子 |
| | 238 | */ |
| | 239 | public class PostXMLClient { |
| | 240 | public static void main(String[] args) throws Exception { |
| | 241 | File input = new File(「test.xml」); |
| | 242 | PostMethod post = new PostMethod(「http://localhost:8080/httpclient/xml.jsp」); |
| | 243 | // 設置請求的內容直接從文件中讀取 |
| | 244 | post.setRequestBody(new FileInputStream(input)); |
| | 245 | |
| | 246 | if (input.length() < Integer.MAX_VALUE) |
| | 247 | post.setRequestContentLength(input.length()); |
| | 248 | else post.setRequestContentLength(EntityEnclosingMethod.CONTENT_LENGTH_CHUNKED); |
| | 249 | |
| | 250 | // 指定請求內容的類型 |
| | 251 | post.setRequestHeader("Content-type", "text/xml; charset=GBK"); |
| | 252 | |
| | 253 | HttpClient httpclient = new HttpClient(); |
| | 254 | int result = httpclient.executeMethod(post); |
| | 255 | System.out.println("Response status code: " + result); |
| | 256 | System.out.println("Response body: "); |
| | 257 | System.out.println(post.getResponseBodyAsString()); |
| | 258 | post.releaseConnection(); |
| | 259 | } |
| | 260 | } |
| | 261 | }}} |
| | 262 | |
| | 263 | = 6. 通過HTTP上傳文件 = |
| | 264 | |
| | 265 | |
| | 266 | httpclient使用了單獨的一個HttpMethod子類來處理文件的上傳,這個類就是MultipartPostMethod,該類已經封裝了文件上傳的細節,我們要做的僅僅是告訴它我們要上傳文件的全路徑即可,下面的代碼片段演示如何使用這個類。 |
| | 267 | |
| | 268 | {{{ |
| | 269 | #!java |
| | 270 | MultipartPostMethod filePost = new MultipartPostMethod(targetURL); |
| | 271 | filePost.addParameter("fileName", targetFilePath); |
| | 272 | HttpClient client = new HttpClient(); |
| | 273 | //由於要上傳的文件可能比較大,因此在此設置最大的連接超時時間 |
| | 274 | client.getHttpConnectionManager().getParams().setConnectionTimeout(5000); |
| | 275 | int status = client.executeMethod(filePost); |
| | 276 | }}} |
| | 277 | 上面代碼中,targetFilePath即為要上傳的文件所在的路徑。 |
| | 278 | |
| | 279 | |
| | 280 | = 7. 訪問啟用認證的頁面 = |
| | 281 | |
| | 282 | |
| | 283 | 我們經常會碰到這樣的頁面,當訪問它的時候會彈出一個瀏覽器的對話框要求輸入用戶名和密碼後方可,這種用戶認證的方式不同於我們在前面介紹的基於表單的用戶身份驗證。這是HTTP的認證策略,httpclient支持三種認證方式包括:基本、摘要以及NTLM認證。其中基本認證最簡單、通用但也最不安全;摘要認證是在HTTP 1.1中加入的認證方式,而NTLM則是微軟公司定義的而不是通用的規範,最新版本的NTLM是比摘要認證還要安全的一種方式。 |
| | 284 | |
| | 285 | 下面例子是從httpclient的CVS服務器中下載的,它簡單演示如何訪問一個認證保護的頁面: |
| | 286 | |
| | 287 | {{{ |
| | 288 | #!java |
| | 289 | import org.apache.commons.httpclient.HttpClient; |
| | 290 | import org.apache.commons.httpclient.UsernamePasswordCredentials; |
| | 291 | import org.apache.commons.httpclient.methods.GetMethod; |
| | 292 | public class BasicAuthenticationExample { |
| | 293 | public BasicAuthenticationExample() { |
| | 294 | } |
| | 295 | public static void main(String[] args) throws Exception { |
| | 296 | HttpClient client = new HttpClient(); |
| | 297 | client.getState().setCredentials( |
| | 298 | "www.verisign.com", |
| | 299 | "realm", |
| | 300 | new UsernamePasswordCredentials("username", "password") |
| | 301 | ); |
| | 302 | GetMethod get = new GetMethod("https://www.verisign.com/products/index.html"); |
| | 303 | get.setDoAuthentication( true ); |
| | 304 | int status = client.executeMethod( get ); |
| | 305 | System.out.println(status+"\n"+ get.getResponseBodyAsString()); |
| | 306 | get.releaseConnection(); |
| | 307 | } |
| | 308 | } |
| | 309 | }}} |
| | 310 | |
| | 311 | = 8. 多線程模式下使用httpclient = |
| | 312 | |
| | 313 | 多線程同時訪問httpclient,例如同時從一個站點上下載多個文件。對於同一個HttpConnection同一個時間只能有一個線程訪問,為了保證多線程工作環境下不產生衝突,httpclient使用了一個多線程連接管理器的類:MultiThreadedHttpConnectionManager,要使用這個類很簡單,只需要在構造HttpClient實例的時候傳入即可,代碼如下: |
| | 314 | |
| | 315 | |
| | 316 | {{{ |
| | 317 | #!java |
| | 318 | MultiThreadedHttpConnectionManager connectionManager = |
| | 319 | new MultiThreadedHttpConnectionManager(); |
| | 320 | HttpClient client = new HttpClient(connectionManager); |
| | 321 | }}} |
| | 322 | 以後儘管訪問client實例即可。 |