JAVA网络编程之URI、URL研究
类别: JSP教程
URI、URL和URN是识别、定位和命名互联网上的资源的标准途径。本文分析了URI、URL和URN和Java API的URI和URL类(以及与URL相关的类),并演示了如何在程序中使用这些类。
1989年Tim Berners-Lee发明了互联网(World Wide Web)。WWW被认为是全球互连的实际的和抽象的资源的集合--它按需求提供信息实体--通过互联网访问。实际的资源的范围从文件到人,抽象的资源包括数据库查询。因为要通过多样的方式识别资源(人的名字可能相同,然而计算机文件只能通过唯一的路径名称组合访问),所以需要标准的识别WWW资源的途径。为了满足这种需要,Tim Berners-Lee引入了标准的识别、定位和命名的途径:URI、URL和URN。
URI、URL和URN是什么?
体系中的URI、URL和URN是彼此关联的。URI的范畴位于体系的顶层,URL和URN的范畴位于体系的底层。这种排列显示URL和URN都是URI的子范畴,如图1所示:
screen.width-333)this.width=screen.width-333;">
图1:URI、URL和URN之间的层次关系。URL和URN是URI的子范畴。
URI表示的是统一的资源标识,它是以某种统一的(标准化的)方式标识资源的简单字符串。典型情况下,这种字符串以scheme(命名URI的名字空间的标识符--一组相关的名称)开头,语法如下:
[scheme:] scheme-specific-part
URI以scheme和冒号开头。Scheme用大写/小写字母开头,后面为空或者跟着更多的大写/小写字母、数字、加号、减号和点号。冒号把scheme与scheme-specific-part分开了,并且scheme-specific-part的语法和语义(意思)由URI的名字空间决定。其中一个例子是http://www.cnn.com,其中http是scheme,//http://www.cnn.com是 scheme-specific-part,并且它的scheme与scheme-specific-part被冒号分开了。
我们可以把URI按照绝对的或相对的分类。绝对的URI指以scheme(后面跟着冒号)开头的URI。前面提到的http://www.cnn.com就是绝对的URI的一个例子,其它的例子还有mailto:jeff@javajeff.com、news:comp.lang.java.help和xyz://whatever。你可以把绝对的URI看作是以某种方式引用某种资源,而这种方式对标识符出现的环境没有依赖。如果使用文件系统作类比,绝对的URI类似于从根目录开始的某个文件的路径。与绝对的URI不同的,相对的URI不是以scheme(后面跟着冒号)开始的URI。它的一个例子是articles/articles.html。你可以把相对的URI看作是以某种方式引用某种资源,而这种方式依赖于标识符出现的环境。如果用文件系统作类比,相对的URI类似于从当前目录开始的文件路径。
标准化可以通过目录术语来理解。假定目录x直接位于根目录之下,x有子目录a和b,b有文件memo.txt,a是当前目录。为了显示memo.txt中的内容(在微软Windows下),你可能输入type x.memo.txt。你也可能输入type xa..memo.txt,在这种情况下,a和..的出现是没有必要的。这两种形式都不是最简单的。但是如果输入xmemo.txt,你就指定了最简单的路径了,从根目录开始访问memo.txt。最简单的xmemo.txt路径就是标准化的路径。
通常通过基本的和相对的URI访问资源。基本的URI是绝对的URI,它唯一地标识了某种资源的名字空间,而相对的URI标识了与基础的URI相对的资源。(与基本的URI不同,相对的URI在某种资源的生存周期内可以永远不需要改变)。因为基本的和相对的URI都不能完整的识别某种资源,有必要把两种URI通过解析过程合并。相反地,通过相对化从合并的URI中提取相对的URI也是可行的。
注意
不透明的URI与其它的URI不同,它不服从标准化、分解和相对化。
假定你把x://a/作为基础的URI,并把b/c作为相对的URI。根据基础URI分解这个相对的URI将产生x://a/b/c。根据x://a/相对化x://a/b/c将产生b/c。
URI不能定位或读取/写入资源。这是统一的资源定位器(URL)的任务。URL是一种URI,但是它的大纲组件是已知的网络协议(简称协议),并且它把URI组件与某种协议处理程序(一种资源定位器和根据协议建立的约束规则与资源通讯的读/写机制)。
URI一般不能为资源提供持久不便的名称。这是统一的资源命名(URN)的任务。URN也是一种URI,但是全球唯一的、持久不便的,即使资源不在存在或不再使用。
使用URI
网络API通过提供了URI类(位于java.net程序包中),使我们在源代码层使用URI成为可能。URI的构造函数建立了封装URI的URI对象;URI的方法建立URI对象;如果授权机构组件是基于服务器的就分析它,提取URI组件,决定URI对象的URI是绝对的还是相对的;决定URI对象的URI是不透明的还是分层的;比较两个URI对象中的URI;标准化(normalize)URI对象的URI;根据URI对象的基础URI分解某个相对的URI以得到已分解的URI;根据URI对象的基础URI关联某个已分解的URI以得到相对的URI,把URI对象转换为URL对象。
我们进一步查看URI类,在它里面有五个构造函数。最简单的是URI(String uri)。这个构造函数把URI作为String类型的参数,把URI分解为组件,并把这些组件存储在一个新的URI对象中。如果String对象的URI(通过uri引用)违反了RFC 2396的语法规则,其它的四个构造函数URI(String uri)将会产生一个java.net.URISyntaxException对象。
下面的代码片断演示了使用URI(String uri)建立封装了一个简单的URI组件的URI对象:
URI uri = new URI ("http://www.cnn.com");
典型情况下URI构造函数用于建立封装用户指定的URI的URI对象。因为用户可能输入不正确的URI,所以URI构造函数产生已检查的URISyntaxException对象。这意味着你的代码必须明确地尝试着调用某个URI构造函数并捕捉异常,或者通过在该方法的Throws子句中列举URISyntaxException以"推卸责任"。
如果你知道URI是有效的(例如在源代码中的URI),将不会产生URISyntaxException对象。因为在这种情况下处理某个URI构造函数的异常处理要求可能有困难,所以URI提供了静态的create(String uri)方法。这个方法分解通过uri引用的String对象中包含URI,如果该URI没有违反任何语法规则就建立URI对象(并从方法中返回对它的引用),否则将捕捉到一个内部的URISyntaxException对象,把该对象包装金一个未检查的IllegalArgumentException对象中,并抛出这个IllegalArgumentException对象。因为IllegalArgumentException是未检查的,你不需要明确的尝试代码并捕捉异常或把它的类名称列举在Throws子句中。
下面的代码片断演示了create(String uri):
URI uri = URI.create ("http://www.cnn.com");
URI构造函数和create(String uri)方法试图分解出某个URI的授权机构组件的用户信息、主机和端口部分。对于按正常形式形成的基于服务器的授权机构组件,它们是会成功的。对于按拙劣的形式形成的基于服务器的授权机构组件,他们将会失败--并且把该授权机构组件当作是基于注册的。有时你可能知道某个URI的授权机构组件必须是基于服务器的。你可以确保该URI的授权机构组件分解出用户信息、主机和端口,或者你可以确保将产生一个异常(伴随着相应的诊断信息)。你可以通过调用URI的parseServerAuthority()方法实现这种操作。如果成功分解出URI,该方法将返回包含提取的用户信息、主机和端口部分的URI的新URI对象的一个引用(但是如果授权机构组件已经被分解过了,将会返回调用parseServerAuthority()的URI对象的引用。),否则该方法将产生一个URISyntaxException对象。
下面的代码片断演示了parseServerAuthority():
// 下面的parseServerAuthority()调用出现后会发生什么情况?
URI uri = new URI ("//foo:bar").parseServerAuthority();
一旦拥有了URI对象,你就可以通过调用getAuthority()、getFragment()、getHost()、getPath()、getPort()、getQuery()、getScheme()、getSchemeSpecificPart()和 getUserInfo()方法提取多种组件。你也可以通过调用isAbsolute()确定该URI是绝对的还是相对的,通过调用isOpaque()确定该URI是不透明的还是分层的。如果返回值是true意味着该URI是绝对的或不透明的,如果返回值是false意味着该URI是相对的或分层的。
列表1中的程序用命令行参数建立了一个URI对象,调用URI组件提取方法来检索URI的组件,并调用URI的isAbsolute()和isOpaque()方法把该URI分类为绝对的/相对性和不透明的/分层的。
列表1: URIDemo1.java
// URIDemo1.java
import java.net.*;
class URIDemo1
{
public static void main (String [] args) throws Exception
{
if (args.length != 1)
{
System.err.println ("usage: java URIDemo1 uri");
return;
}
URI uri = new URI (args [0]);
System.out.println ("Authority = " +uri.getAuthority ());
System.out.println ("Fragment = " +uri.getFragment ());
System.out.println ("Host = " +uri.getHost ());
System.out.println ("Path = " +uri.getPath ());
System.out.println ("Port = " +uri.getPort ());
System.out.println ("Query = " +uri.getQuery ());
System.out.println ("Scheme = " +uri.getScheme ());
System.out.println ("Scheme-specific part = " +
uri.getSchemeSpecificPart ());
System.out.println ("User Info = " +uri.getUserInfo ());
System.out.println ("URI is absolute: " +uri.isAbsolute ());
System.out.println ("URI is opaque: " +uri.isOpaque ());
}
}
输入java URIDemo1命令后,列表1的输出结果如下:
query://jeff@books.com:9000/public/manuals/appliances?stove#ge:
Authority = jeff@books.com:9000
Fragment = ge
Host = books.com
Path = /public/manuals/appliances
Port = 9000
Query = stove
Scheme = query
//jeff@books.com:9000/public/manuals/appliances?stove
User Info = jeff
URI is absolute: true
URI is opaque: false
上面的输出显示该URI是绝对的,因为它指定了一个大纲(query),并且该URI是分层的,因为query后面有/符号。
技巧
你应该调用URI的compareTo(Object o)和equals(Object o)来决定URI的次序(为了排序目的)和等同性。你可以参考SDK文档,查阅这些方法的更多信息。
URI类支持基本的URI操作,包括标准化(normalization)、分解(resolution)和相对化(relativization)。标准化是通过URI的normalize()方法支持的。调用normalize()时,它返回对新URI对象的引用,该对象包含调用的URI对象的URI的标准的表现。
列表2演示了normalize()方法。它把URI作为程序的唯一的参数,URIDemo2打印出标准的相等的URI。
列表2: URIDemo2.java
// URIDemo2.java
import java.net.*;
class URIDemo2
{
public static void main (String [] args) throws Exception
{
if (args.length != 1)
{
System.err.println ("usage: java URIDemo2 uri");
return;
}
URI uri = new URI (args [0]);
System.out.println ("Normalized URI = " +
uri.normalize ().toString ());
}
}
在编译URIDemo2后,在命令行输入java URIDemo2 x/y/../z/./q,将看到下面的输出:
Normalized URI = x/z/q
上面的输出显示y、..和.消失了。这是因为..意味着你想直接在x下面访问名字空间的z部分,.意味着你希望访问与z部分相关的名字空间的q部分。
URI通过提供resolve(String uri)、resolve(URI uri)和relativize(URI uri)方法支持反向解析和相对化操作。如果uri引用是空的(null)这三个方法都会产生NullPointerException对象。同样,如果指定的URI违反了RFC 2396语法规则,resolve(String uri)通过的内部的create(String uri)调用间接地产生一个IllegalArgumentException对象。
列表3的代码演示了resolve(String uri)和relativize(URI uri)。
列表3: URIDemo3.java
// URIDemo3.java
import java.net.*;
class URIDemo3
{
public static void main (String [] args) throws Exception
{
if (args.length != 2)
{
System.err.println ("usage: " +
"java URIDemo3 uriBase uriRelative");
return;
}
URI uriBase = new URI (args [0]);
System.out.println ("Base URI = " +uriBase.toString ());
URI uriRelative = new URI (args [1]);
System.out.println ("Relative URI = " +uriRelative.toString ());
URI uriResolved = uriBase.resolve (uriRelative);
System.out.println ("Resolved URI = " +uriResolved.toString ());
URI uriRelativized = uriBase.relativize (uriResolved);
System.out.println ("Relativized URI = " +uriRelativized.toString ());
}
}
在编译URIDemo3后,在命令行输入java URIDemo3 http://www.somedomain.com/ x/../y. ,输出如下:
Base URI = http://www.somedomain.com/
Relative URI = x/../y
Resolved URI = http://www.somedomain.com/y
Relativized URI = y
上面的输出显示相对的URI的x/../y根据基础URI http://www.somedomain.com/分解并(在内部)标准化,取得了已分解的http://www.somedomain.com/URI。给定该URI和基础URI,该已分解的URI根据基础URI相对化获得了y,它是原始的但是标准的相对的URI。
技巧
调用URI的toURL()方法把URI转换为URL。
在本周日的专题中我将向读者介绍如何使用URL以及MIME(多用途的网际邮件扩充协议)的概念以及它如何与URL发生联系,敬请期待。
继续向大家介绍如何使用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 (´ ´);
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类的一个具体的子类所建立的对象的引用。这意味着你必须按字节次序读取资源数据,这种做法是恰当的,因为你不知道将要读取的数据是什么类型的。如果你事先知道要读取的数据是文本的,并且每一行以换行符()结束,你就可以按行读取而不是按字节读取数据了。
下面的代码片断演示了把一个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的关系。现在你应该编写一些代码熟悉一下所学的内容了。
1989年Tim Berners-Lee发明了互联网(World Wide Web)。WWW被认为是全球互连的实际的和抽象的资源的集合--它按需求提供信息实体--通过互联网访问。实际的资源的范围从文件到人,抽象的资源包括数据库查询。因为要通过多样的方式识别资源(人的名字可能相同,然而计算机文件只能通过唯一的路径名称组合访问),所以需要标准的识别WWW资源的途径。为了满足这种需要,Tim Berners-Lee引入了标准的识别、定位和命名的途径:URI、URL和URN。
URI、URL和URN是什么?
体系中的URI、URL和URN是彼此关联的。URI的范畴位于体系的顶层,URL和URN的范畴位于体系的底层。这种排列显示URL和URN都是URI的子范畴,如图1所示:
screen.width-333)this.width=screen.width-333;">
图1:URI、URL和URN之间的层次关系。URL和URN是URI的子范畴。
URI表示的是统一的资源标识,它是以某种统一的(标准化的)方式标识资源的简单字符串。典型情况下,这种字符串以scheme(命名URI的名字空间的标识符--一组相关的名称)开头,语法如下:
[scheme:] scheme-specific-part
URI以scheme和冒号开头。Scheme用大写/小写字母开头,后面为空或者跟着更多的大写/小写字母、数字、加号、减号和点号。冒号把scheme与scheme-specific-part分开了,并且scheme-specific-part的语法和语义(意思)由URI的名字空间决定。其中一个例子是http://www.cnn.com,其中http是scheme,//http://www.cnn.com是 scheme-specific-part,并且它的scheme与scheme-specific-part被冒号分开了。
我们可以把URI按照绝对的或相对的分类。绝对的URI指以scheme(后面跟着冒号)开头的URI。前面提到的http://www.cnn.com就是绝对的URI的一个例子,其它的例子还有mailto:jeff@javajeff.com、news:comp.lang.java.help和xyz://whatever。你可以把绝对的URI看作是以某种方式引用某种资源,而这种方式对标识符出现的环境没有依赖。如果使用文件系统作类比,绝对的URI类似于从根目录开始的某个文件的路径。与绝对的URI不同的,相对的URI不是以scheme(后面跟着冒号)开始的URI。它的一个例子是articles/articles.html。你可以把相对的URI看作是以某种方式引用某种资源,而这种方式依赖于标识符出现的环境。如果用文件系统作类比,相对的URI类似于从当前目录开始的文件路径。
标准化可以通过目录术语来理解。假定目录x直接位于根目录之下,x有子目录a和b,b有文件memo.txt,a是当前目录。为了显示memo.txt中的内容(在微软Windows下),你可能输入type x.memo.txt。你也可能输入type xa..memo.txt,在这种情况下,a和..的出现是没有必要的。这两种形式都不是最简单的。但是如果输入xmemo.txt,你就指定了最简单的路径了,从根目录开始访问memo.txt。最简单的xmemo.txt路径就是标准化的路径。
通常通过基本的和相对的URI访问资源。基本的URI是绝对的URI,它唯一地标识了某种资源的名字空间,而相对的URI标识了与基础的URI相对的资源。(与基本的URI不同,相对的URI在某种资源的生存周期内可以永远不需要改变)。因为基本的和相对的URI都不能完整的识别某种资源,有必要把两种URI通过解析过程合并。相反地,通过相对化从合并的URI中提取相对的URI也是可行的。
注意
不透明的URI与其它的URI不同,它不服从标准化、分解和相对化。
假定你把x://a/作为基础的URI,并把b/c作为相对的URI。根据基础URI分解这个相对的URI将产生x://a/b/c。根据x://a/相对化x://a/b/c将产生b/c。
URI不能定位或读取/写入资源。这是统一的资源定位器(URL)的任务。URL是一种URI,但是它的大纲组件是已知的网络协议(简称协议),并且它把URI组件与某种协议处理程序(一种资源定位器和根据协议建立的约束规则与资源通讯的读/写机制)。
URI一般不能为资源提供持久不便的名称。这是统一的资源命名(URN)的任务。URN也是一种URI,但是全球唯一的、持久不便的,即使资源不在存在或不再使用。
使用URI
网络API通过提供了URI类(位于java.net程序包中),使我们在源代码层使用URI成为可能。URI的构造函数建立了封装URI的URI对象;URI的方法建立URI对象;如果授权机构组件是基于服务器的就分析它,提取URI组件,决定URI对象的URI是绝对的还是相对的;决定URI对象的URI是不透明的还是分层的;比较两个URI对象中的URI;标准化(normalize)URI对象的URI;根据URI对象的基础URI分解某个相对的URI以得到已分解的URI;根据URI对象的基础URI关联某个已分解的URI以得到相对的URI,把URI对象转换为URL对象。
我们进一步查看URI类,在它里面有五个构造函数。最简单的是URI(String uri)。这个构造函数把URI作为String类型的参数,把URI分解为组件,并把这些组件存储在一个新的URI对象中。如果String对象的URI(通过uri引用)违反了RFC 2396的语法规则,其它的四个构造函数URI(String uri)将会产生一个java.net.URISyntaxException对象。
下面的代码片断演示了使用URI(String uri)建立封装了一个简单的URI组件的URI对象:
URI uri = new URI ("http://www.cnn.com");
典型情况下URI构造函数用于建立封装用户指定的URI的URI对象。因为用户可能输入不正确的URI,所以URI构造函数产生已检查的URISyntaxException对象。这意味着你的代码必须明确地尝试着调用某个URI构造函数并捕捉异常,或者通过在该方法的Throws子句中列举URISyntaxException以"推卸责任"。
如果你知道URI是有效的(例如在源代码中的URI),将不会产生URISyntaxException对象。因为在这种情况下处理某个URI构造函数的异常处理要求可能有困难,所以URI提供了静态的create(String uri)方法。这个方法分解通过uri引用的String对象中包含URI,如果该URI没有违反任何语法规则就建立URI对象(并从方法中返回对它的引用),否则将捕捉到一个内部的URISyntaxException对象,把该对象包装金一个未检查的IllegalArgumentException对象中,并抛出这个IllegalArgumentException对象。因为IllegalArgumentException是未检查的,你不需要明确的尝试代码并捕捉异常或把它的类名称列举在Throws子句中。
下面的代码片断演示了create(String uri):
URI uri = URI.create ("http://www.cnn.com");
URI构造函数和create(String uri)方法试图分解出某个URI的授权机构组件的用户信息、主机和端口部分。对于按正常形式形成的基于服务器的授权机构组件,它们是会成功的。对于按拙劣的形式形成的基于服务器的授权机构组件,他们将会失败--并且把该授权机构组件当作是基于注册的。有时你可能知道某个URI的授权机构组件必须是基于服务器的。你可以确保该URI的授权机构组件分解出用户信息、主机和端口,或者你可以确保将产生一个异常(伴随着相应的诊断信息)。你可以通过调用URI的parseServerAuthority()方法实现这种操作。如果成功分解出URI,该方法将返回包含提取的用户信息、主机和端口部分的URI的新URI对象的一个引用(但是如果授权机构组件已经被分解过了,将会返回调用parseServerAuthority()的URI对象的引用。),否则该方法将产生一个URISyntaxException对象。
下面的代码片断演示了parseServerAuthority():
// 下面的parseServerAuthority()调用出现后会发生什么情况?
URI uri = new URI ("//foo:bar").parseServerAuthority();
一旦拥有了URI对象,你就可以通过调用getAuthority()、getFragment()、getHost()、getPath()、getPort()、getQuery()、getScheme()、getSchemeSpecificPart()和 getUserInfo()方法提取多种组件。你也可以通过调用isAbsolute()确定该URI是绝对的还是相对的,通过调用isOpaque()确定该URI是不透明的还是分层的。如果返回值是true意味着该URI是绝对的或不透明的,如果返回值是false意味着该URI是相对的或分层的。
列表1中的程序用命令行参数建立了一个URI对象,调用URI组件提取方法来检索URI的组件,并调用URI的isAbsolute()和isOpaque()方法把该URI分类为绝对的/相对性和不透明的/分层的。
列表1: URIDemo1.java
// URIDemo1.java
import java.net.*;
class URIDemo1
{
public static void main (String [] args) throws Exception
{
if (args.length != 1)
{
System.err.println ("usage: java URIDemo1 uri");
return;
}
URI uri = new URI (args [0]);
System.out.println ("Authority = " +uri.getAuthority ());
System.out.println ("Fragment = " +uri.getFragment ());
System.out.println ("Host = " +uri.getHost ());
System.out.println ("Path = " +uri.getPath ());
System.out.println ("Port = " +uri.getPort ());
System.out.println ("Query = " +uri.getQuery ());
System.out.println ("Scheme = " +uri.getScheme ());
System.out.println ("Scheme-specific part = " +
uri.getSchemeSpecificPart ());
System.out.println ("User Info = " +uri.getUserInfo ());
System.out.println ("URI is absolute: " +uri.isAbsolute ());
System.out.println ("URI is opaque: " +uri.isOpaque ());
}
}
输入java URIDemo1命令后,列表1的输出结果如下:
query://jeff@books.com:9000/public/manuals/appliances?stove#ge:
Authority = jeff@books.com:9000
Fragment = ge
Host = books.com
Path = /public/manuals/appliances
Port = 9000
Query = stove
Scheme = query
//jeff@books.com:9000/public/manuals/appliances?stove
User Info = jeff
URI is absolute: true
URI is opaque: false
上面的输出显示该URI是绝对的,因为它指定了一个大纲(query),并且该URI是分层的,因为query后面有/符号。
技巧
你应该调用URI的compareTo(Object o)和equals(Object o)来决定URI的次序(为了排序目的)和等同性。你可以参考SDK文档,查阅这些方法的更多信息。
URI类支持基本的URI操作,包括标准化(normalization)、分解(resolution)和相对化(relativization)。标准化是通过URI的normalize()方法支持的。调用normalize()时,它返回对新URI对象的引用,该对象包含调用的URI对象的URI的标准的表现。
列表2演示了normalize()方法。它把URI作为程序的唯一的参数,URIDemo2打印出标准的相等的URI。
列表2: URIDemo2.java
// URIDemo2.java
import java.net.*;
class URIDemo2
{
public static void main (String [] args) throws Exception
{
if (args.length != 1)
{
System.err.println ("usage: java URIDemo2 uri");
return;
}
URI uri = new URI (args [0]);
System.out.println ("Normalized URI = " +
uri.normalize ().toString ());
}
}
在编译URIDemo2后,在命令行输入java URIDemo2 x/y/../z/./q,将看到下面的输出:
Normalized URI = x/z/q
上面的输出显示y、..和.消失了。这是因为..意味着你想直接在x下面访问名字空间的z部分,.意味着你希望访问与z部分相关的名字空间的q部分。
URI通过提供resolve(String uri)、resolve(URI uri)和relativize(URI uri)方法支持反向解析和相对化操作。如果uri引用是空的(null)这三个方法都会产生NullPointerException对象。同样,如果指定的URI违反了RFC 2396语法规则,resolve(String uri)通过的内部的create(String uri)调用间接地产生一个IllegalArgumentException对象。
列表3的代码演示了resolve(String uri)和relativize(URI uri)。
列表3: URIDemo3.java
// URIDemo3.java
import java.net.*;
class URIDemo3
{
public static void main (String [] args) throws Exception
{
if (args.length != 2)
{
System.err.println ("usage: " +
"java URIDemo3 uriBase uriRelative");
return;
}
URI uriBase = new URI (args [0]);
System.out.println ("Base URI = " +uriBase.toString ());
URI uriRelative = new URI (args [1]);
System.out.println ("Relative URI = " +uriRelative.toString ());
URI uriResolved = uriBase.resolve (uriRelative);
System.out.println ("Resolved URI = " +uriResolved.toString ());
URI uriRelativized = uriBase.relativize (uriResolved);
System.out.println ("Relativized URI = " +uriRelativized.toString ());
}
}
在编译URIDemo3后,在命令行输入java URIDemo3 http://www.somedomain.com/ x/../y. ,输出如下:
Base URI = http://www.somedomain.com/
Relative URI = x/../y
Resolved URI = http://www.somedomain.com/y
Relativized URI = y
上面的输出显示相对的URI的x/../y根据基础URI http://www.somedomain.com/分解并(在内部)标准化,取得了已分解的http://www.somedomain.com/URI。给定该URI和基础URI,该已分解的URI根据基础URI相对化获得了y,它是原始的但是标准的相对的URI。
技巧
调用URI的toURL()方法把URI转换为URL。
在本周日的专题中我将向读者介绍如何使用URL以及MIME(多用途的网际邮件扩充协议)的概念以及它如何与URL发生联系,敬请期待。
继续向大家介绍如何使用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 (´ ´);
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类的一个具体的子类所建立的对象的引用。这意味着你必须按字节次序读取资源数据,这种做法是恰当的,因为你不知道将要读取的数据是什么类型的。如果你事先知道要读取的数据是文本的,并且每一行以换行符()结束,你就可以按行读取而不是按字节读取数据了。
下面的代码片断演示了把一个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的关系。现在你应该编写一些代码熟悉一下所学的内容了。
- 上一篇: 使用JAVA实现网络传输数据的压缩
- 下一篇: 从SOCKET中读取数据时使用超时
-= 资 源 教 程 =-
文 章 搜 索