天天看點

(五) 建立URL連接配接

為了在Java程式中通路Web伺服器,會希望在更高的級别上進行處理,而不隻是建立套接字連接配接和發送HTTP請求。

1.URL和URI

(1)URL和URLConnection類封裝了大量複雜的實作細節,這些細節設計如何從遠端站點擷取資訊。

例:通過傳遞字元串來建構一個URL對象

URL url = new URL(urlString);
           

如果隻是想獲得該資源的内容,可以使用URL類中的openStream方法。該方法傳回一個InputStream對象,然後就可以按照一般的用法來使用這個對象了,比如用它建構一個Scanner對象。

例:通過URL建構Scanner對象

InputStream inStream = url.openStream();
    Scanner in = new Scanner(inStream);
           

(2)java.net包對 統一資源定位符(uniform resource locator, URL) 和 統一資源辨別符(uniform resource identifier, URI) 做了非常有用的區分。

URI是個純粹的句法結構,用于指定辨別Web資源的字元串的各個不同部分。URL是URI的一個特例,它包含了用于定位Web資源的足夠資訊。

其他URI,比如 mailto:[email protected] 則不屬于定位符,因為根據該辨別符我們無法定位任何資料。像這樣的URI稱之為URN(uniform resource name, 統一資源名稱)

(3)在java類庫中,URI類不包含任何用于通路資源的方法,它的惟一作用就是解析。想法的是,URL類可以打開一個到達資源的流。是以,URL類隻能用于那些Java類庫知道該如何處理的模式。

URL可以處理的模式包含 http: 、https 、ftp: 、本地檔案系統(file:)和JAR檔案(jar:)。

(4)URI的解析并不是可有可無的,要考慮到它也許會變的非常複雜。

例: http://maps.yahoo.com/py/maps.py?csz=Cupertino+CA

    ftp://username:[email protected]/pub/file.txt

URI規範給出了标記這些辨別符的規則,一個URI具有以下文法

    [scheme:]schemeSpecificPart[#fragment]

上式中,[...]表示可選部分,它與:和#可以被包含在辨別符内。

(5)包含 scheme: 部分的URI被稱為絕對URI。否則稱為相對URI。

(6)如果絕對URI的schemeSpecificPart不是以/開頭的,我們就稱它是不透明的。

例: mailto:[email protected]

(7)所有絕對的透明的URI和所有相對的URI都是有 分層的(hierarchical)

例: http://java.sun.com/index.html

    ../../java/net/Socket.html#Socket()

(8)一個分層的URI的schemeSpecificPart具有以下結構:

[//authority][path][?query]

(9)對于那些基于伺服器的URI,authority部分采用以下形式

[user-info@]host[:port]

port必須是一個整數

RFC 2396(标準化URI的文獻)還支援一種基于系統資料庫的機制,此時authority采用了一種不同的格式。不過,這種情況并不常見。

(10)URI類的作用之一是解析辨別符并将它分解成各種不同的組成部分。可以用一下方法讀取它們:

    getScheme

    getSchemeSpecificPart

    getAuthority

    getUserInfo

    getHost

    getPort

    getQuery

    getFragment

(11)URI類的另一個作用是處理絕對辨別符和相對辨別符。

例: 如果存在一個如下的絕對URI:

    http://docs.mycompany.com/api/java/net/ServerSocket.html

    和一下如下的的相對URI

    ../../java/net/Socket.html#Socket()

    那麼可以将它們合并為一個絕對URI

    http://docs.mycompany.com/api/java/net/Socket.html#Socket()

這個過程被稱為相對URL的 轉換(resolving)。

(12)與此相反的過程稱為相對化(relativization)。

例: 有一個基本URI:

    http://docs.mycompany.com/api

    和另一個URI:

    http://docs.mycompany.com/api/java/lang/String.html

    那麼相對化之後的URI就是:

    java/lang/String.html

(13)URI類同時支援一下兩個操作:

relative = base.relativize(combined);
    combined = base.resolve(relative);
           

2.使用URLConnection擷取資訊

如果想從某個Web資源擷取更多資訊,那麼應該使用URLConnection類,它能得到比基本的URL類更多的控制功能。

當操作一個URLConnection對象時,必須像下面這樣非常小心的安排操作步驟:

(1)調用URL類中的openConnection方法獲得URLConnection對象:

URLConnection connection = url.openConnection();
           

(2)使用一下方法來設定任意的請求屬性

    setDoInput 

    setDoOutput

    setIfModifiedSince

    setUseCaches

    setAllowUserInteraction

    setRequestProperty

    setConnectTimeout

    setReadTimeout

(3)調用connect方法連接配接遠端資源:

connection.connect();
           

除了與伺服器建立套接字連接配接外,該方法還可以用于向伺服器查詢頭資訊(header information)。

(4)與伺服器建立連接配接後,可以查詢頭資訊。getHeaderFieldKey和getHeaderField兩個方法列舉了消息頭的所有字段。

getHeaderFields方法傳回一個包含了消息頭中所有字段的标準Map對象。為了友善使用,一下方法可以查詢各标準字段:

    getContentType

    getContentLength

    getContentEncoding

    getDate

    getExpiration

    getLastModified

(5)最後通路資源資料。使用getInputStream方法擷取一個輸入流用以讀取資訊(這個輸入流與URL類中的openStream方法所傳回的流相同)。

另一個方法getContent在實際操作中并不是很有用。有标準内容類型(比如text/plain和image/gif)所傳回的對象需要使用com.sun層次結構中的類來進行處理。也可以注冊自己的内容處理器。

(6)注意,URLConnection類中的getInputStream和getOutputStream方法與Socket類中的這些方法不同。

URLConnection類具有很多表面之外的功能,尤其在處理請求和相應消息頭時。正因為如此,嚴格遵循建立連接配接的每個步驟都顯得非常重要。

(7)URLConnection類中的一些方法。有幾個方法可以在與伺服器建立連接配接之前設定連接配接屬性。

其中最重要的是setDoInput和setDoOutput。

在預設情況下建立連接配接隻有從伺服器讀取資訊的輸入流(即setDoInput預設值為true),并沒有任何執行寫操作的輸出流(setDoOutput預設值為false)。如果想獲得輸出流(例如,向Web伺服器送出資料),需要調用:

    connection.setDoOutput(true);

(8)設定某些請求頭(request header)。請求頭是與請求指令一起發送到伺服器的。

例:

    GET www.server.com/index.html HTTP/1.0

    Referer: http://www.sonewhere.com/links.html

    Proxy-Connection: Keep-Alive

    User-Agent: Mozilla/5.0(X11; U; Linux i686; en-US; rv:1.8.1.4)

    Host:www.server.com

    Accept: text/html, image/gif, image/jpeg, image/png, */*

    Accept-Language: en

    Accept_Charset: iso-8859-1,*,utf-8

    Cookie: orangemilano=192218887821987

setIfModifiedSince(long ifmodifiedsince)方法用于告訴連接配接隻對自某個特定日期依賴被修改過的資料該興趣

setUseCaches(boolean usecaches)和setAllowUserInteraction(boolean allowuserinteraction)這兩個方法隻用于Applet

setUseCaches方法用于指令浏覽器首先檢查它的緩存,UseCaches 标志為 true,則允許連接配接使用任何可用的緩存。如果為 false,則忽略緩存,預設為 true。例如浏覽器中的“重新加載”

setAllowUserInteraction方法則用于在通路有密碼保護的資源時彈出對話框,以便查詢使用者名和密碼。

(9)一個總攬全局的方法:setRequestProperty,它可以用來設定對特定協定起作用的任何"名-值(name/value)對"。

關于HTTP請求頭的格式參加RFC 2616,其中的某些參數沒有很好地記錄在文檔中,它們通常在程式員直接口頭傳授。

例:通路一個由密碼保護的Web也,那麼必須按如下步驟操作:

    1)将使用者名、磨耗和密碼以字元串形式連接配接在一起。

String input = username + ":" + password;
           

     2)計算上一步驟所得字元串的base64編碼。(base64編碼用于将子就留編碼成可列印的ASCII字元流)

      可以通過sun.misc.BASE64Encoder進行編碼

String encoding = new sun.misc.BASE64Encoder().encode(input.getBytes());
           

      注意sun.misc.BASE64Encoder屬于未公開(undocumented)的類

    3)調用setRequestProperty方法,設定name參數的值為"Authorization"、value參數的值為"Basic"+encoding;

connection.setRequestProperty("Authorization", "Basic" + encoding);
           

    4)上述是通路有密碼保護的web頁,如果想通過FTP通路一個由密碼保護的檔案時,要采用一種完全不同的方法。可以直接建構一個如下格式的URL:

    ftp://username:[email protected]/pub/file.txt

(10)一旦調用了connect方法,就可以查詢響應頭資訊。

    列舉所有響應頭的字段,該操作采用了另一種疊代方式。

String key = connection.getHeaderFieldKey(n);
           

可以獲得響應頭的第n個鍵,其中n從1開始。如果n為0或大于消息頭的字段總數,該方法将傳回null值。沒有哪種方法可以傳回字段的數量,必須反複調用getHeaderFieldKey方法直到傳回null為止。

    得到第n個值

String value = connection.getHeaderField(n);
           

 getHeaderFields方法可以傳回一個封裝了響應頭字段的Map對象。

Map<String, List<String>> headerFields = connection.getHeaderFields();
    for(Map<String, List<String>> entry : headerFields){
        String key = entry.getKey();
        List<String> value = entry.getValue();
    }
           

(11)一組來自典型HTTP請求的相應字段頭

Date: Wed, 27 Aug 2008 00:15:48 GMT

Server: Apache/2.2.2(Unix)

Last-Modified: Sun, 22 Jun 2008 20:53:38 GMT

Accept-Ranges: bytes

Content-Length: 4813

Connection: close

Content-Type: text/html

long getDate() : 傳回 date 頭字段的值,即建立日期。 

long getExpiration() : 傳回 expires 頭字段的值,即過期日。

long getLastModified() : 傳回 last-modified 頭字段的值,即最後一次被修改日期。

int getContentLength() : 傳回 content-length 頭字段的值,即如果知道内容的長度,則傳回該長度,否則傳回-1。

String getContentType() : 傳回 content-type 頭字段的值,即擷取内容的類型,比如text/plain或image/gif。

String getContentEncoding() : 傳回 content-encoding 頭字段的值,即擷取内容的編碼,比如gzip,這個值不太常用,因為預設的identity編碼并不是Content-Encoding頭來設定的。

Java提供了6個方法用以通路大多數常用的消息頭類型的值,并在需要的時候将它們轉換成數字類型。其中傳回類型為long的方法傳回的是從格林威治時間1970年1月1日開始計算的秒數。

        用于通路響應頭值的簡便方法

鍵名                                方法名                         傳回類型

Date                               getDate                         long       

Expires                          getExpiration                long

Last-Modified                getLastModified          long

Content-Length            getContentLength        int

Content-Type                getContentType            String

Content-Encoding        getContentEncoding   String

(12)一個常會遇到的問題是Java平台是否支援對安全Web頁面的通路(https: URL) : 從Java SE 1.4開始,對安全套接字層ssl的支援已經成為标準程式庫的一部分

3.送出表單資料

當表單資料被發送給Web伺服器時,通常會有兩個指令會被用到: GET 和 POST

(1)在使用GET指令時,隻需将參數附在URL結尾處即可。

例: http://host/script?parameters

其中,每個參數都有"名字=值"的形式,而這些參數之間用&字元分隔開。

參數的值遵循的規則

    1)保留字元A-Z、a-z、0-9以及 . - * _

    2)用 + 字元替換所有的空格

    3)将其他所有字元編碼為UTF-8,并将每個位元組都編碼為 % 後面緊跟着一個兩位的十六進制數字。

        例如,發送街道名S. Main,可以使用S%2e+Main,因為十六進制2e是"."的ASCII碼值。

      這種編碼方式使得在任何中間程式中都不會混入空格,并且也不需要對其他特殊字元進行轉換。

GET指令很簡單,但是有一個重要的局限性,大多數浏覽器都對GET請求中可以包含的字元數做了限制。

(2)使用POST指令時,并不需要在URL中添加任何參數,但是從URLConnection中擷取輸入流,并将名-值對寫入該流中。當然,仍然需要對這些值進行URL編碼,并用&字元将它們隔開。

POST送出資料流程

    1)建立一個URLConnection對象

URL url = new URL("http://host/script");
    URLConnection connection = url.openConnection();
           

    2)調用setDoOutput方法建立一個用于輸出的連接配接。

connection.setDoOutput(true);
           

     3)調用getOutputStream方法獲得一個流,可以通過這個流向伺服器發送資料。

      如果要向伺服器發送文本資訊,那麼可以将流包裝在PrintWriter對象中。

PrintWriter out = new PrintWriter(connection.getOutputStream());
           

    4)現在可以向伺服器發送資料了

out.print(name1 + "=" + URLEncoder.encode(value1, "UTF-8") + "&");
    out.print(name2 + "=" + URLEncoder.encode(value2, "UTF-8"));
           

    5)關閉輸出流

out.close();
           

    6)調用getInputStream方法讀取伺服器的響應。

例:通過POST向伺服器發送資訊,并接收傳回資料。其中urlString為位址,nameValuePairs為表單資料

public static String doPost(String urlString, Map<String, String> nameValuePairs) throws IOException{
        URL url = new URL(urlString);
        URLConnection connection = url.openConnection();
        //打開輸出流
        connection.setDoOutput(true);
       
        PrintWriter out = new PrintWriter(connection.getOutputStream());
        boolean first = true;
        for(Map.Entry<String, String> pair : nameValuePairs.entrySet()){
            if(first){
                first = false;
            }else{
                out.print("&");
            }
            String name = pair.getKey();
            String value = pair.getValue();
            out.print(name);
            out.print("=");
            out.print(URLEncoder.encode(value, "UTF-8"));
        }
       
        out.close();
        Scanner in;
        StringBuilder response = new StringBuilder();
        try{
            in = new Scanner(connection.getInputStream());
        }catch(IOException e){
            if(!(connection instanceof HttpURLConnection)){
                throw e;
            }
            //捕獲錯誤頁面
            InputStream err = ((HttpURLConnection) connection).getErrorStream();
            if(err == null){
                throw e;
            }
            in = new Scanner(err);
        }
       
        while(in.hasNextLine()){
            response.append(in.nextLine());
            response.append("\n");
        }
        in.close();
        return response.toString();
    }
 
           

在讀取響應時,如果伺服器端運作錯誤,那麼調用connection.getInputStream()時就會抛出FileNotFoundException異常,但是此時伺服器會傳回一個錯誤頁面(常見的404)。

為了捕獲錯誤頁面,可以将URLConnection對象轉換為HttpURLConnection類,并調用它的getErrorStream方法。

InputStream err = ((HttpURLConnection) connection).getErrorStream();
           

(3)URLConnection向伺服器發送的内容。

URLConnection對象先向伺服器發送一個請求頭,當送出表單資料時,該請求頭必須包含

    Content-type: application/x-www-form-urlencoded

而POST的請求頭還必須包括長度,例

    Content-Length: 24

是以URLConnection對象會把發送到輸出流的所有資料都緩存起來,這是因為在發送之前首先确定内容的總長度。

DEMO

import java.awt.EventQueue;

import javax.swing.JFrame;

public class PostTest {
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable(){
			public void run(){
				JFrame frame = new PostTestFrame();
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				frame.setVisible(true);
			}
		});
	}
}
           
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingWorker;

public class PostTestFrame extends JFrame {
	
	private JPanel northPanel;
	
	public PostTestFrame(){
		setTitle("PostTest");
		
		northPanel = new JPanel();
		add(northPanel, BorderLayout.NORTH);
		northPanel.setLayout(new GridLayout(0, 2));
		northPanel.add(new JLabel("Host: ", SwingConstants.TRAILING));
		final JTextField hostField = new JTextField();
		northPanel.add(hostField);
		northPanel.add(new JLabel("Action: ", SwingConstants.TRAILING));
		final JTextField actionField = new JTextField();
		northPanel.add(actionField);
		for(int i=1;i<=8;i++){
			northPanel.add(new JTextField());
		}
		
		final JTextArea result = new JTextArea(20, 40);
		add(new JScrollPane(result));
		
		JPanel southPanel = new JPanel();
		add(southPanel, BorderLayout.SOUTH);
		JButton addButton = new JButton("More");
		southPanel.add(addButton);
		addButton.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent event){
				northPanel.add(new JTextField());
				northPanel.add(new JTextField());
				pack();
			}
		});
		
		JButton getButton = new JButton("Get");
		southPanel.add(getButton);
		getButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				result.setText("");
				final Map<String, String> post = new HashMap<String, String>();
				for(int i=4;i<northPanel.getComponentCount();i+=2){
					String name = ((JTextField)northPanel.getComponent(i)).getText();
					if(name.length()>0){
						String value = ((JTextField)northPanel.getComponent(i + 1)).getText();
						post.put(name, value);
					}
				}
				new SwingWorker<Void, Void>(){
					protected Void doInBackground() throws Exception{
						try{
							String urlString = hostField.getText() + "/" + actionField.getText();
							result.setText(doPost(urlString, post));
						}catch(IOException e){
							result.setText("" + e);
						}
						return null;
					}
				}.execute();
			}
		});
		
		
	}
	
	public static String doPost(String urlString, Map<String, String> nameValuePairs) throws IOException{
		URL url = new URL(urlString);
		URLConnection connection = url.openConnection();
		connection.setDoOutput(true);
		
		PrintWriter out = new PrintWriter(connection.getOutputStream());
		boolean first = true;
		for(Map.Entry<String, String> pair : nameValuePairs.entrySet()){
			if(first){
				first = false;
			}else{
				out.print("&");
			}
			String name = pair.getKey();
			String value = pair.getValue();
			out.print(name);
			out.print("=");
			out.print(URLEncoder.encode(value, "UTF-8"));
		}
		
		out.close();
		Scanner in;
		StringBuilder response = new StringBuilder();
		try{
			in = new Scanner(connection.getInputStream());
		}catch(IOException e){
			if(!(connection instanceof HttpURLConnection)){
				throw e;
			}
			InputStream err = ((HttpURLConnection) connection).getErrorStream();
			if(err == null){
				throw e;
			}
			in = new Scanner(err);
		}
		
		while(in.hasNextLine()){
			response.append(in.nextLine());
			response.append("\n");
		}
		in.close();
		return response.toString();
	}
	
}
           

繼續閱讀