17.2.3 使用URL和URLConnection URL(Uniform Resource Locator)對象代表統一資源定位器,它是指向互連網“資源”的指標。資源可以是簡單的檔案或目錄,也可以是對更為複雜的對象引用,例如對資料庫或搜 索引擎的查詢。通常情況而言,URL可以由協議名、主機、連接埠和資源群組成。即滿足如下格式:
protocol://host:port/resourceName |
例如如下的URL地址:
http://www.oneedu.cn/Index.htm |
JDK中還提供了一個URI(Uniform Resource Identifiers)類,其執行個體代表一個統一資源識別項,Java的URI不能用於定位任何資源,它的唯一作用就是解析。與此對應的是,URL則包含 一個可開啟到達該資源的輸入資料流,因此我們可以將URL理解成URI的特例。 URL類提供了多個構造器用於建立URL對象,一旦獲得了URL對象之後,可以調用如下方法來訪問該URL對應的資源: String getFile():擷取此URL的資源名。 String getHost():擷取此URL的主機名稱。 String getPath():擷取此URL的路徑部分。 int getPort():擷取此 URL 的連接埠號碼。 String getProtocol():擷取此 URL 的協議名稱。 String getQuery():擷取此 URL 的查詢字串部分。 URLConnection openConnection():返回一個URLConnection對象,它表示到URL所引用的遠程對象的串連。 InputStream openStream():開啟與此URL的串連,並返回一個用於讀取該URL資源的InputStream。 URL對象中前面幾個方法都非常容易理解,而該對象提供的openStream()可以讀取該URL資源的InputStream,通過該方法可以非常方便地讀取遠端資源——甚至實現多線程下載。如下程式所示: 程式清單:codes/17/17-2/MutilDown.java
//定義下載從start到end的內容的線程 class DownThread extends Thread { //定義位元組數組(取水的竹筒)的長度 private final int BUFF_LEN = 32; //定義下載的起始點 private long start; //定義下載的結束點 private long end; //下載資源對應的輸入資料流 private InputStream is; //將下載到的位元組輸出到raf中 private RandomAccessFile raf ; //構造器,傳入輸入資料流,輸出資料流和下載起始點、結束點 public DownThread(long start , long end , InputStream is , RandomAccessFile raf) { //輸出該線程負責下載的位元組位置 System.out.println(start + "---->" + end); this.start = start; this.end = end; this.is = is; this.raf = raf; } public void run() { try { is.skip(start); raf.seek(start); //定義讀取輸入資料流內容的緩衝數組(竹筒) byte[] buff = new byte[BUFF_LEN]; //本線程負責下載資源的大小 long contentLen = end - start; //定義最多需要讀取幾次就可以完成本線程的下載 long times = contentLen / BUFF_LEN + 4; //實際讀取的位元組數 int hasRead = 0; for (int i = 0; i < times ; i++) { hasRead = is.read(buff); //如果讀取的位元組數小於0,則退出迴圈! if (hasRead < 0) { break; } raf.write(buff , 0 , hasRead); } } catch (Exception ex) { ex.printStackTrace(); } //使用finally塊來關閉當前線程的輸入資料流、輸出資料流 finally { try { if (is != null) { is.close(); } if (raf != null) { raf.close(); } } catch (Exception ex) { ex.printStackTrace(); } } } } public class MutilDown { public static void main(String[] args) { final int DOWN_THREAD_NUM = 4; final String OUT_FILE_NAME = "down.jpg"; InputStream[] isArr = new InputStream[DOWN_THREAD_NUM]; RandomAccessFile[] outArr = new RandomAccessFile[DOWN_THREAD_NUM]; try { //建立一個URL對象 URL url = new URL("http://images.china-pub.com/ + "ebook35001-40000/35850/shupi.jpg"); //以此URL對象開啟第一個輸入資料流 isArr[0] = url.openStream(); long fileLen = getFileLength(url); System.out.println("網路資源的大小" + fileLen); //以輸出檔案名建立第一個RandomAccessFile輸出資料流 outArr[0] = new RandomAccessFile(OUT_FILE_NAME , "rw"); //建立一個與下載資源相同大小的空檔案 for (int i = 0 ; i < fileLen ; i++ ) { outArr[0].write(0); } //每線程應該下載的位元組數 long numPerThred = fileLen / DOWN_THREAD_NUM; //整個下載資源整除後剩下的餘數 long left = fileLen % DOWN_THREAD_NUM; for (int i = 0 ; i < DOWN_THREAD_NUM; i++) { //為每個線程開啟一個輸入資料流、一個RandomAccessFile對象, //讓每個線程分別負責下載資源的不同部分。 if (i != 0) { //以URL開啟多個輸入資料流 isArr[i] = url.openStream(); //以指定輸出檔案建立多個RandomAccessFile對象 outArr[i] = new RandomAccessFile(OUT_FILE_NAME , "rw"); } //分別啟動多個線程來下載網路資源 if (i == DOWN_THREAD_NUM - 1 ) { //最後一個線程下載指定numPerThred+left個位元組 new DownThread(i * numPerThred , (i + 1) * numPerThred + left , isArr[i] , outArr[i]).start(); } else { //每個線程負責下載一定的numPerThred個位元組 new DownThread(i * numPerThred , (i + 1) * numPerThred, isArr[i] , outArr[i]).start(); } } } catch (Exception ex) { ex.printStackTrace(); } } //定義擷取指定網路資源的長度的方法 //定義擷取指定網路資源的長度的方法 public static long getFileLength(URL url) throws Exception { long length = 0; //開啟該URL對應的URLConnection。 URLConnection con = url.openConnection(); //擷取串連URL資源的長度 long size = con.getContentLength(); length = size; return length; } } |
上面程式中定義了DownThread線程類,該線程從InputStream中讀取從start開始,到end結束的所有位元組資料,並寫入RandomAccessFile對象。這個DownThread線程類的run()就是一個簡單的輸入、輸出實現。 程式中MutilDown類中的main方法負責按如下步驟來實現多線程下載: 建立URL對象。 擷取指定URL對象所指向資源的大小(由getFileLength方法實現),此處用到了URLConnection類,該類代表Java應用程式和URL之間的通訊連結。下面還有關於URLConnection更詳細的介紹。 在本地磁碟上建立一個與網路資源相同大小的空檔案。 計算每條線程應該下載網路資源的哪個部分(從哪個位元組開始,到哪個位元組結束)。 依次建立、啟動多條線程來下載網路資源的指定部分。 上面程式已經實現了多線程下載的核心代碼,如果要實現斷點下載,則還需要額外增加一個設定檔(讀者可以發現所有斷點下載工具都會在下載開始產生兩 個檔案:一個是與網路資源相同大小的空檔案,一個是設定檔),該設定檔分別記錄每個線程已經下載到了哪個位元組,當網路斷開後再次開始下載時,每個線程 根據設定檔裡記錄的位置向後下載即可。 URL的openConnection()方法將返回一個URLConnection對象,該對象表示應用程式和 URL 之間的通訊連結。程式可以通過URLConnection執行個體向該URL發送請求、讀取URL引用的資源。 通常建立一個和 URL的串連,並發送請求、讀取此URL引用的資源需要如下幾個步驟: 通過調用URL對象openConnection()方法來建立URLConnection對象。 設定URLConnection的參數和普通請求屬性。 如果只是發送GET方式請求,使用connect方法建立和遠端資源之間的實際串連即可;如果需要發送POST方式的請求,需要擷取URLConnection執行個體對應的輸出資料流來發送請求參數。 遠端資源變為可用,程式可以訪問遠端資源的頭欄位或通過輸入資料流讀取遠端資源的資料。 在建立和遠端資源的實際串連之前,程式可以通過如下方法來佈建要求頭欄位: setAllowUserInteraction:設定該URLConnection的allowUserInteraction要求標頭欄位的值。 setDoInput:設定該URLConnection的doInput要求標頭欄位的值。 setDoOutput:設定該URLConnection的doOutput要求標頭欄位的值。 setIfModifiedSince:設定該URLConnection的ifModifiedSince要求標頭欄位的值。 setUseCaches:設定該URLConnection的useCaches要求標頭欄位的值。 除此之外,還可以使用如下方法來設定或增加通用頭欄位: setRequestProperty(String key, String value):設定該URLConnection的key要求標頭欄位的值為value。如下代碼所示:
conn.setRequestProperty("accept" , "*/*") |
addRequestProperty(String key, String value):為該URLConnection的key要求標頭欄位的增加value值,該方法並不會覆蓋原要求標頭欄位的值,而是將新值追加到原要求標頭欄位中。 當遠端資源可用之後,程式可以使用以下方法用於訪問頭欄位和內容: Object getContent():擷取該URLConnection的內容。 String getHeaderField(String name):擷取指定回應標頭欄位的值。 getInputStream():返回該URLConnection對應的輸入資料流,用於擷取URLConnection響應的內容。 getOutputStream():返回該URLConnection對應的輸出資料流,用於向URLConnection發送請求參數。 如果既要使用輸入資料流讀取URLConnection響應的內容,也要使用輸出資料流發送請求參數,一定要先使用輸出資料流,再使用輸入資料流。 getHeaderField方法用於根據回應標頭欄位來返回對應的值。而某些頭欄位由於經常需要訪問,所以Java提供以下方法來訪問特定回應標頭欄位的值: getContentEncoding:擷取content-encoding回應標頭欄位的值。 getContentLength:擷取content-length回應標頭欄位的值。 getContentType:擷取content-type回應標頭欄位的值。 getDate():擷取date回應標頭欄位的值。 getExpiration():擷取expires回應標頭欄位的值。 getLastModified():擷取last-modified回應標頭欄位的值。 下面程式示範了如何向Web網站發送GET請求、POST請求,並從Web網站取得響應的樣本。 程式清單:codes/17/17-2/TestGetPost.java
public class TestGetPost { /** * 向指定URL發送GET方法的請求 * @param url 發送請求的URL * @param param 請求參數,請求參數應該是name1=value1&name2=value2的形式。 * @return URL所代表遠端資源的響應 */ public static String sendGet(String url , String param) { String result = ""; BufferedReader in = null; try { String urlName = url + "?" + param; URL realUrl = new URL(urlName); //開啟和URL之間的串連 URLConnection conn = realUrl.openConnection(); //設定通用的請求屬性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); //建立實際的串連 conn.connect(); //擷取所有回應標頭欄位 Map<String, List<String>> map = conn.getHeaderFields(); //遍曆所有的回應標頭欄位 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } //定義BufferedReader輸入資料流來讀取URL的響應 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine())!= null) { esult += "\n" + line; } } catch(Exception e) { System.out.println("發送GET請求出現異常!" + e); e.printStackTrace(); } //使用finally塊來關閉輸入資料流 finally { try { if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; } /** * 向指定URL發送POST方法的請求 * @param url 發送請求的URL * @param param 請求參數,請求參數應該是name1=value1&name2=value2的形式。 * @return URL所代表遠端資源的響應 */ public static String sendPost(String url,String param) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); //開啟和URL之間的串連 URLConnection conn = realUrl.openConnection(); //設定通用的請求屬性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); //發送POST請求必須設定如下兩行 conn.setDoOutput(true); conn.setDoInput(true); //擷取URLConnection對象對應的輸出資料流 out = new PrintWriter(conn.getOutputStream()); //發送請求參數 out.print(param); //flush輸出資料流的緩衝 out.flush(); //定義BufferedReader輸入資料流來讀取URL的響應 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine())!= null) { result += "\n" + line; } } catch(Exception e) { System.out.println("發送POST請求出現異常!" + e); e.printStackTrace(); } //使用finally塊來關閉輸出資料流、輸入資料流 finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; } //提供主方法,測試發送GET請求和POST請求 public static void main(String args[]) { //發送GET請求 String s = TestGetPost.sendGet("http://localhost:8888/abc/ login.jsp",null); System.out.println(s); //發送POST請求 String s1 = TestGetPost.sendPost("http://localhost:8888/abc/a.jsp", "user=李剛&pass=abc"); System.out.println(s1); } } |
上面程式中發送GET請求時只需將請求參數放在URL字串之後,以?隔開,程式直接調用URLConnection對象的connect方法即 可,如程式中sendGet方法中粗體字代碼所示;如果程式需要發送POST請求,則需要先設定doIn和doOut兩個要求標頭欄位的值,再使用 URLConnection對應的輸出資料流來發送請求參數即可,如程式中sendPost()方法中粗體字代碼所示。 不管是發送GET請求,還是發送POST請求,程式擷取URLConnection響應的方式完全一樣:如果程式可以確定遠程響應是字元流,則可以使用字元流來讀取;如果程式無法確定遠程響應是字元流,則使用位元組流讀取即可。 上面程式中發送請求的兩個URL是筆者在本機部署的Web應用,關於如何建立Web應用,編寫JSP頁面請參考筆者所著的《輕量級J2EE公司專屬應用程式 實戰》。由於程式可以使用這種方式直接向伺服器發送請求——相當於提交Web應用中的登入表單頁,這樣就可以讓程式不斷地變換使用者名稱、密碼來提交登入請 求,直到返回登入成功,這就是所謂的暴力破解。 |