在上個專題《Java網路編程之URI、URL研究(上)》中我們介紹了URI、URL的慨念和體繫結構,以及如何使用URI在本文中我將繼續向大家介紹如何使用URL和MIME(多用途的網際郵件擴充協議)的概念以及它如何與URL發生聯絡的。
使用URL
網路API通過提供URL類讓我們能在原始碼層使用URL。每一個URL對象都封裝了資源的標識符和通訊協定處理常式。前面的技巧顯示了獲得URL對象的途徑之一是調用URI對象的toURL()方法。但是這種選擇不一定方便(為什麼在需要URL對象的時候必須建立URI對象呢?)。作為代替,你可以調用URL建構函式來建立URL對象。你也可以調用URL的方法來提取URL的組件,開啟一個輸入資料流(input stream)從資源中讀取資訊,獲得某個能方便檢索資源資料的對象的引用,比較兩個URL對象中的URL,獲得到資源的連線物件,該連線物件允許代碼瞭解(並寫入)更多的資源的資訊。
URL類有六個建構函式。其中最簡單的是URL(String url),它有一個String類型的參數,把URL分解為自己的組件,並把這些組件儲存在一個新的URL對象中。如果某個URL沒有包含通訊協定處理常式或該URL的協議是未知的,其它的五個建構函式會產生一個java.net.MalformedURLException對象。
下面的代碼片斷示範了使用URL(String url)建立一個URL對象,該對象封裝了一個簡單的URL組件和http通訊協定處理常式。
| URL url = new URL ("http://www.informit.com"); |
一旦擁有了URL對象,你就可以使用getAuthority()、getDefaultPort()、 getFile()、 getHost()、 getPath()、getPort()、 getProtocol()、getQuery()、getRef()和getUserInfo(). The getDefaultPort()等方法提取各種組件。如果URL中沒有指定連接埠的部分,getDefaultPort()方法返回URL對象的通訊協定處理常式使用(資源定位)的預設連接埠。getFile()方法返迴路徑和查詢組件的結合體。getProtocol()方法返回決定資源的連線類型(例如http、mailto、ftp)的協議的名稱。getRef()方法返回URL的部分區斷(我們所知道的引用)。最後,getUserInfo()方法返回授權機構組件的使用者資訊部分。在這些URL組件提取方法中,如果某些組件不存在(如果沒有給URL對象的通訊協定處理常式指定預設的連接埠,它也返回-1),這些方法就返回null或-1。
作為這些組件提取方法的補充,你還可以調用openStream()方法檢索java.io.InputStream引用。使用這種引用,你可以用面向位元組的方式讀取資源。
列表4是URLDemo1的原始碼。該程式從命令列參數建立了一個URL對象,調用URL組件提取方法來檢索該URL的組件,調用URL的openStream()方法開啟與資源的串連並返回一個用於從資源讀取位元組資料的InputStream引用,讀取/列印這些位元組,關閉輸入資料流。
列表4: URLDemo1.java
// URLDemo1.java import java.io.*; import java.net.*;class URLDemo1 { public static void main (String [] args) throws IOException { if (args.length != 1) { System.err.println ("usage: java URLDemo1 url"); return; } URL url = new URL (args [0]); System.out.println ("Authority = "+ url.getAuthority ()); System.out.println ("Default port = " +url.getDefaultPort ()); System.out.println ("File = " +url.getFile ()); System.out.println ("Host = " +url.getHost ()); System.out.println ("Path = " +url.getPath ()); System.out.println ("Port = " +url.getPort ()); System.out.println ("Protocol = " +url.getProtocol ()); System.out.println ("Query = " +url.getQuery ()); System.out.println ("Ref = " +url.getRef ()); System.out.println ("User Info = " +url.getUserInfo ()); System.out.print ('/n'); InputStream is = url.openStream (); int ch; while ((ch = is.read ()) != -1) System.out.print ((char) ch); is.close (); } } |
在命令列輸入java URLDemo1 http://www.javajeff.com/articles/articles/html後,上面的代碼的輸出如下:
Authority = http://www.javajeff.com Default port = 80 File = /articles/articles.html Host = http://www.javajeff.com Path = /articles/articles.html Port = -1 Protocol = http Query = null Ref = null User Info = null<html> <head> <title> Java Jeff - Articles </title> <meta http-equiv=Content-Type content="text/html; charset=ISO-8859-1"> <meta name=author content="Jeff Friesen"> <meta name=keywords content="java, virtual machine"> <script language=JavaScript> if (navigator.appName == "Netscape") document.write ("<br>"); </script> </head> <body bgcolor=#000000> <center> <table border=1 cellpadding=5 cellspacing=0> <tr> <td> <table cellpadding=0 cellspacing=0> <tr> <td> <a href=informit/informit.html> <img alt=InformIT border=0 src=informit.gif></a> </td> </tr> </table> </td> <td align=middle> <img src=title.gif><br> <a href=../welcome/welcome.html> <img alt="Welcome to Java Jeff!" border=0 src=jupiter.jpg> </a><br> <img src=../common/clear_dot.gif vspace=5><br> <a href=../ads/ads.html> <img alt="Welcome to Java Jeff!" border=0 src=jupiter.jpg> </td> <td> <table cellpadding=0 cellspacing=0> <tr> <td> <a href=javaworld/javaworld.html> <img alt=JavaWorld border=0 src=javaworld.gif></a> </td> </tr> </table> </td> </tr> </table> </center> <br> <font color=#ffffff> <center> Best viewed at a resolution of 1024x768 or higher.<br> <img src=../common/clear_dot.gif vspace=5><br> <i> Copyright © 2001-2002, Jeff Friesen. All rights reserved. </i> <p> <a href=../index.html> <img alt=Back border=0 src=../common/back.gif></a> </center> </font> </body> </html> |
在上面的資訊中,輸出標識符80是預設連接埠,HTTP是協議。上面給出的是輸出的HTML頁面的原始碼。
URL的openStream()方法通常返回抽象的InputStream類的一個具體的子類所建立的對象的引用。這意味著你必須按位元組次序讀取資源資料,這種做法是恰當的,因為你不知道將要讀取的資料是什麼類型的。如果你事Crowdsourced Security Testing道要讀取的資料是文本的,並且每一行以分行符號(/n)結束,你就可以按行讀取而不是按位元組讀取資料了。
下面的代碼片斷示範了把一個InputStream對象封裝進java.io.InputStreamReader對象以從8位過渡到16位字元,把結果對象封裝進java.io.BufferedReader對象以訪問BufferedReader的readLine()方法,並調用readLine()方法從資源讀取文本的所有行。
InputStream is = url.openStream (); BufferedReader br = new BufferedReader (new InputStreamReader (is)); String line; while ((line = br.readLine ()) != null) System.out.println (line); is.close (); |
有時候按位元組的次序讀取資料並不方便。例如,如果資源是JPEG檔案,那麼擷取一個影像處理過程並向該過程註冊一個使用者使用資料的方法更好。當映像完整下載後立即顯示它並不困難。如果出現這種情況,你就有必要使用getContent()方法。
當調用getContent()方法時,它會返回某種對象的Object引用,而你可以調用該對象的方法(在轉換成適當的類型後),採用更方便的方式檢索資料。但是在調用該方法前,你必須使用instanceof驗證對象的類型,防止類產生異常。
對於JPEG資源,getContent()返回一個對象,該對象的類實現了java.awt.Image.ImageProducer介面。下面的代碼片斷示範了使用instanceof驗證對象是ImageProducer的,並進行了轉換。接下來可以調用ImageProducer方法註冊一個使用者並初始化映像的使用過程。
URL url = new URL (args [0]); Object o = url.getContent (); if (o instanceof ImageProducer) { ImageProducer ip = (ImageProducer) o; // ... } |
技巧
調用URL的equals(Object o)和sameFile(Object o)方法來決定兩個URL是否相同。第一個方法包含了比較的片斷,而第二個方法沒有包含。你可以參閱SDK文檔尋找更多資訊。
查看一下getContent()方法的原始碼,你會找到openConnection().getContent()。此外,查看一下openStream()方法的原始碼,你會發現openConnection().getInputStream()。每個方法都首先調用URL的openConnection()方法。這個方法返回抽象的java.net.URLConnection類(描述與某些資源的串連)的一個子類建立的對象的引用。URLConnection的方法反映了資源和串連的細節資訊,使我們能編寫代碼向資源寫入資訊。
列表5的URLDemo2原始碼示範了openConnection(),以及調用一些URLConnection的方法。
列表5: URLDemo2.java
| // URLDemo2.java import java.io.*; import java.net.*; import java.util.*; class URLDemo2 { public static void main (String [] args) throws IOException { if (args.length != 1) { System.err.println ("usage: java URLDemo2 url"); return; } URL url = new URL (args [0]); // 返回代表某個資源的串連的新的特定協議對象的引用 URLConnection uc = url.openConnection (); // 進行串連 uc.connect (); // 列印多種頭部欄位的內容 Map m = uc.getHeaderFields (); Iterator i = m.entrySet ().iterator (); while (i.hasNext ()) System.out.println (i.next ()); // 如果資源允許輸入和輸出操作就找出來 System.out.println ("Input allowed = " +uc.getDoInput ()); System.out.println ("Output allowed = " +uc.getDoOutput ()); } } |
在對openConnection()的調用返回後,調用了connect()方法--用於建立某種資源的串連。(儘管openConnection()方法返回一個連線物件的引用,但是openConnection()不會串連到資源)。 URLConnection的getHeaderFields()方法返回一個對象的應用,該對象的類實現了java.util.Map介面。該圖表(map)包含頭部名稱和值的集合。什麼是頭部(header)?頭部是基於文本的成對的名稱和數值,它識別資源資料的類型、資料的長度等等。
在編譯了URLDemo2後,在命令列輸入java URLDemo2 http://www.javajeff.com,輸出如下:
Date=[Sun, 17 Feb 2002 17:49:32 GMT] Connection=[Keep-Alive] Content-Type=[text/html; charset=iso-8859-1] Accept-Ranges=[bytes] Content-Length=[7214] null=[HTTP/1.1 200 OK] ETag=["4470e-1c2e-3bf29d5a"] Keep-Alive=[timeout=15, max=100] Server=[Apache/1.3.19 (Unix) Debian/GNU] Last-Modified=[Wed, 14 Nov 2001 16:35:38 GMT] Input allowed = true Output allowed = false |
上面的輸出識別了很多頭部(包括Date、null、Content-Length、 Server、Last-Modified等等)和它們的值。輸出也顯示只允許從資源讀取資料。
你對一個程式是如何識別資源資料的是否感到驚奇?仔細看一下前面的輸出,你會看到叫做Content-Type的東西。Content-Type是一個頭部,它識別了資源資料(內容)的類型是text/html。text部分就是我們所知道的類型,html部分是我們所知道的子類型。(如果內容是普通的文本,Content-Type的值可能是text/plain。上面的類型表明內容是文本的但不是沒有格式的)。Content-Type頭部是我們所知道的多用途Internet郵件擴充(MIME)的一部分。
MIME是傳統的傳輸訊息的7位ASCII標準的一種擴充。通過引入了多種頭部,MIME使視頻、聲音、映像、不同字元集的文本與7位ASCII結合起來。有了Content-Type,MIME可以識別Content-Length和其它標準的頭部。當你使用URLConnection類的時候,你會遇到getContentType()和getContentLength()。這些方法返回的值是Content-Type和Content-Length頭部。
你也許聽說過HTML表單(<form>、 </form>)和其它的HTML標記。表單使我們能夠從某種資源得到(GET)資料並按後來的處理把HTML表單的欄位資料發送(POST)到某種資源。你能夠使用URLConnection類和MIME類比可以得到和發送資料的HTML表單。下面說明你怎樣完成這種事務。
假設你想把表單資料發送(POST)到某個伺服器程式。發送需要對表單資料的操作。首先,表單的資料必須組織為成對的名稱和數值(name/value pair),其次每個對必須指定為name=value格式,再次如果發送多個成對的名稱和數值,必須使用 & 符號把每對分開,最後的name內容和value的內容必須使用application/x-www-form-urlencoded MIME類型編碼。例如x=y&a=b表現了兩個成對的名稱和數值--x/y和a/b。
為了輔助編碼,Java提供了java.net.URLEncoder類,它聲明了一對靜態encode()方法。每個方法有一個String參數並返回包含已編碼的參數內容的String對象的引用。例如,如果encode()發現參數中有空格,它在結果中用加號代替空格。
下面的代碼片斷示範了調用URLEncoder的encode(String s)方法,對a 空格 b字串進行編碼。結果a+b儲存在一個新的String對象中,result引用它。
| String result = URLEncoder.encode ("a b"); |
作為準備表單資料的補充,必須告訴URLConnection對象資料已經被發送了,因為URLConnection預設的操作是擷取資料。為了完成這種事務,你可以首先把openConnection()的傳回值轉換為HttpURLConnection類型(在確保該傳回值的類型正確後)。接著調用結果對象的setRequestMethod(String method)方法,把POST作為method參數引用的對象的值。
另一個必須完成的事務是調用URLConnection的setDoOutput(boolean doOutput)方法,其參數的值必須為true。這種事務是必要的,因為URLConnection對象在預設情況下不支援輸出。(接著程式最終可以調用URLConnection的getOutputStream()方法,為發送的表單資料返回一個資源的輸出資料流的引用)。
列表6是URLDemo3的原始碼,它示範了把表單資料發送給某個"瞭解"application/x-www-form-urlencoded內容類型的資源。它實現了前面提到的各種事務。
列表6: URLDemo3.java
| // URLDemo3.java import java.io.*; import java.net.*; class URLDemo3 { public static void main (String [] args) throws IOException { // 檢查最後兩個參數和參數的數量 if (args.length < 2 || args.length % 2 != 0) { System.err.println ("usage: java URLDemo3 name value " + "[name value ...]"); return; } // 建立程式串連伺服器程式資源的URL對象,它返回一個表單的成對的名稱和數值 URL url; url = new URL ("http://banshee.cs.uow.edu.au:2000/~nabg/echo.cgi"); // 向某個特定協議對象返回表現http資源串連的引用 URLConnection uc = url.openConnection (); // 驗證串連的類型,必須是HttpURLConnection的 if (!(uc instanceof HttpURLConnection)) { System.err.println ("Wrong connection type"); return; } // 表明程式必須把成對的名稱和數值輸出到伺服器程式資源 uc.setDoOutput (true); // 表明只能返回有用的資訊 uc.setUseCaches (false); //設定Content-Type頭部指示指定URL已編碼資料的表單MIME類型 uc.setRequestProperty ("Content-Type", "application/x-www-form-urlencoded"); // 建立成對的名稱和數值內容發送給伺服器 String content = buildContent (args); //設定Content-Type頭部指示指定URL已編碼資料的表單MIME類型 uc.setRequestProperty ("Content-Length", "" + content.length ()); // 提取串連的適當的類型 HttpURLConnection hc = (HttpURLConnection) uc; // 把HTTP要求方法設定為POST(預設的是GET) hc.setRequestMethod ("POST"); // 輸出內容 OutputStream os = uc.getOutputStream (); DataOutputStream dos = new DataOutputStream (os); dos.writeBytes (content); dos.flush (); dos.close (); // 從伺服器程式資源輸入和顯示內容 InputStream is = uc.getInputStream (); int ch; while ((ch = is.read ()) != -1) System.out.print ((char) ch); is.close (); } static String buildContent (String [] args) { StringBuffer sb = new StringBuffer (); for (int i = 0; i < args.length; i++) { // 為正確的傳輸對參數編碼 String encodedItem = URLEncoder.encode (args [i]); sb.append (encodedItem); if (i % 2 == 0) sb.append ("="); // 分離名稱和值 else sb.append ("&"); // 分離成對的名稱和數值 } // 刪除最後的 & 間隔符 sb.setLength (sb.length () - 1); return sb.toString (); } } |
你可以會奇怪為什麼URLDemo3沒有調用URLConnection的connect()的方法。這個方法沒有被明顯的調用,因為如果連向資源的串連沒有建立的話,其它的URLConnection方法(例如getContentLength())會明確的調用connect()方法。但是一旦連建立了接,調用這些方法(例如setDoOutput(boolean doOutput))就是違反規定的。在connect()被(明確地或隱含地)調用後,這些方法會產生一個IllegalStateException對象。
在URLDemo3編譯後,在命令列輸入java URLDemo3 name1 value1 name2 value2 name3 value3,你可以看到下面的輸出:
<html> <head> <title>Echoing your name value pairs</title> </head> <body> <ol> <li>name1 : value1 <li>name2 : value2 <li>name3 : value3 </ol> <hr> Mon Feb 18 08:58:45 2002 </body> </html> |
該伺服器程式資源的輸出由HTML組成,這些HTML回應的是name1、value1、name2、 value2、name3和value3。
技巧
如果你需要URL對象的URL的字串表現形式,請調用toExternalForm()或toString()。兩種方法的功能是相同的。
總結
本文研究了Java的網路API,聚焦於URI、URL和URN。你學習了這些概念,以及怎樣使用URI和URL(URL相關)的類工作,同時你學習了MIME的知識以及它與URL的關係。現在你應該編寫一些代碼熟悉一下所學的內容了。