URI
、
URL
和
URN
是识别、定位和命名网上资源的标准途径。本文分析了
URI
、
URL
和
URN
的概念,以及
Java
的
URI
和
URL
类(以及与
URL
相关的类),并演示了如何在程序中使用这些类。
Internet 被认为是全球的实际和抽象的资源的集合。实际的资源包括从文件( file )到人 (person) ,抽象的资源包括数据库查询等。因为要通过多样的方式识别资源,所以需要标准的识别 Internet 资源的途径。为了满足这种需要,引入了 URI 、 URL 和 URN 。
URI 、 URL 和 URN 的概念
URI
Internet 被认为是全球的实际和抽象的资源的集合。实际的资源包括从文件( file )到人 (person) ,抽象的资源包括数据库查询等。因为要通过多样的方式识别资源,所以需要标准的识别 Internet 资源的途径。为了满足这种需要,引入了 URI 、 URL 和 URN 。
URI 、 URL 和 URN 的概念
URI
URI = Uniform Resource Identifier
There are two types of URIs: URLs and URNs.
See RFC 1630: Universal Resource Identifiers in WWW: A Unifying Syntax for the Expression of Names and Addresses of Objects on the Network as used in the WWW.
URL
URL = Uniform Resource Locator
See RFC 1738: Uniform Resource Locators (URL)
URN
URN = Uniform Resource Name.
URI 、 URL 和 URN 是彼此关联的。 URI 位于顶层, URL 和 URN 的范畴位于底层。 URL 和 URN 都是 URI 的子范畴。
URI 翻译为统一资源标识,它是以某种标准化的方式标识资源的字符串。这种字符串以 scheme 开头,语法如下:
[scheme:] scheme-specific-part
URI 以 scheme 和冒号开头。冒号把 scheme 与 scheme-specific-part 分开,并且 scheme-specific-part 的语法由 URI 的 scheme 决定。例如 http://www.cnn.com ,其中 http 是 scheme , //www.cnn.com 是 scheme-specific-part 。
URI
分为绝对(
absolute
)或相对(
relative
)两类。绝对
URI
指以
scheme
(后面跟着冒号)开头的
URI
。前面提到的
http://www.cnn.com
就是绝对的
URI
的一个例子,其它的例子还有
mailto:[email protected]
、
news:comp.lang.java.help
和
xyz://whatever
。可以把绝对
URI
看作是以某种方式引用某种资源,而对环境没有依赖。如果使用文件系统作类比,绝对
URI
类似于从根目录开始的某个文件的路径。相对
URI
不以
scheme
开始,一个例子是
articles/articles.html
。可以把相对
URI
看作是以某种方式引用某种资源,而这种方式依赖于标识符出现的环境。如果用文件系统作类比,相对
URI
类似于从当前目录开始的文件路径。
URI
可以进一步分为不透明的(
opaque
)和分层(
hierarchical
)的两类。不透明的
URI
指
scheme-specific-part
不是以‘
/
’开头的绝对的
URI
。其例子有
news:comp.lang.java
和前面的
mailto:[email protected]
。不透明的
URI
不能做进一步的解析,不需要验证
scheme-specific-part
的有效性。与它不同的是,分层的
URI
是以‘
/
’开头的绝对的
URI
或相对的
URL
。分层的
URI
的
scheme-specific-part
必须被分解为几个组成部分。分层的
URI
的
scheme-specific-part
必须符合下面的语法:
[//authority] [path] [?query] [#fragment]
可选的授权机构( authority )标识了该 URI 名字空间的命名机构。如果有这一部分则以‘ // ’开始。它可以是基于服务器或基于授权机构的。基于授权机构有特定的语法(本文没有讨论,因为很少使用它),而基于服务器的语法如下:
[userinfo@] host [:port]
基于服务器的 authority 以用户信息(例如用户名)开始,后面跟着一个 @ 符号,紧接着是主机的名称,以及冒号和端口号。例如 [email protected]:90 就是一个基于服务器的 authority ,其中 jeff 为用户信息, x.com 为主机, 90 为端口。
可选的 path 根据 authority (如果提供了)或 schema (如果没有 authority )定义资源的位置。路径( path )可以分成一系列的路径片断( path segment ),每个路径片断使用‘ / ’与其它片断隔开。如果第一个路径片断以‘ / ’开始,该路径就被认为是绝对的,否则路径就被认为是相对的。例如, /a/b/c 由三个路径片断 a 、 b 和 c 组成,此外这个路径是绝对的,因为第一个路径片断( a )的前缀是‘ / ’。
可选的 query 定义要传递给资源的查询信息。资源使用该信息获取或生成其它的的数据传递回调用者。例如, http://www.somesite.net/a?x=y, x=y 就是一个 query ,在这个查询中 x 是某种实体的名称, y 是该实体的值。
最后一个部分是 fragment 。当使用 URI 进行某种检索操作时,后面执行操作的软件使用 fragment 聚焦于软件感兴趣的资源部分。
分析一个例子 ftp://[email protected]:90/public/notes?text=shakespeare#hamlet
上面的 URI 把 ftp 识别为 schema ,把 [email protected]:90 识别为基于服务器的 authority (其中 george 是用户信息, x.com 是主机, 90 是端口),把 /public/notes 识别为路径,把 text=shakespeare 识别为查询,把 hamlet 识别为片断。本质上它是一个叫做 george 的用户希望通过 /public/notes 路径在服务器 x.com 的 90 端口上检索 shakespeare 文本的 hamlet 信息。
[//authority] [path] [?query] [#fragment]
可选的授权机构( authority )标识了该 URI 名字空间的命名机构。如果有这一部分则以‘ // ’开始。它可以是基于服务器或基于授权机构的。基于授权机构有特定的语法(本文没有讨论,因为很少使用它),而基于服务器的语法如下:
[userinfo@] host [:port]
基于服务器的 authority 以用户信息(例如用户名)开始,后面跟着一个 @ 符号,紧接着是主机的名称,以及冒号和端口号。例如 [email protected]:90 就是一个基于服务器的 authority ,其中 jeff 为用户信息, x.com 为主机, 90 为端口。
可选的 path 根据 authority (如果提供了)或 schema (如果没有 authority )定义资源的位置。路径( path )可以分成一系列的路径片断( path segment ),每个路径片断使用‘ / ’与其它片断隔开。如果第一个路径片断以‘ / ’开始,该路径就被认为是绝对的,否则路径就被认为是相对的。例如, /a/b/c 由三个路径片断 a 、 b 和 c 组成,此外这个路径是绝对的,因为第一个路径片断( a )的前缀是‘ / ’。
可选的 query 定义要传递给资源的查询信息。资源使用该信息获取或生成其它的的数据传递回调用者。例如, http://www.somesite.net/a?x=y, x=y 就是一个 query ,在这个查询中 x 是某种实体的名称, y 是该实体的值。
最后一个部分是 fragment 。当使用 URI 进行某种检索操作时,后面执行操作的软件使用 fragment 聚焦于软件感兴趣的资源部分。
分析一个例子 ftp://[email protected]:90/public/notes?text=shakespeare#hamlet
上面的 URI 把 ftp 识别为 schema ,把 [email protected]:90 识别为基于服务器的 authority (其中 george 是用户信息, x.com 是主机, 90 是端口),把 /public/notes 识别为路径,把 text=shakespeare 识别为查询,把 hamlet 识别为片断。本质上它是一个叫做 george 的用户希望通过 /public/notes 路径在服务器 x.com 的 90 端口上检索 shakespeare 文本的 hamlet 信息。
URI
的标准化(
normalize
)
标准化可以通过目录术语来理解。假定目录 x 直接位于根目录之下, x 有子目录 a 和 b , b 有文件 memo.txt , a 是当前目录。为了显示 memo.txt 中的内容,你可能输入 type \x\.\b\memo.txt 。你也可能输入 type \x\a\..\b\memo.txt ,在这种情况下, a 和 .. 的出现是没有必要的。这两种形式都不是最简单的。但是如果输入 \x\b\memo.txt ,你就指定了最简单的路径了,从根目录开始访问 memo.txt 。最简单的 \x\b\memo.txt 路径就是标准化的路径。
通常通过 base + relative URI 访问资源。 Base URI 是绝对 URI ,而 Relative URI 标识了与 Base URI 相对的资源。因此有必要把两种 URI 通过解析过程合并,相反地从合并的 URI 中提取 Relative URI 也是可行的。
假定把 x://a/ 作为 Base URI ,并把 b/c 作为 Relative URI 。 Resolve 这个相对 URI 将产生 x://a/b/c 。根据 x://a/ 相对化( Relative ) x://a/b/c 将产生 b/c 。
URI 不能读取 / 写入资源,这是统一的资源定位器( URL )的任务。 URL 是一种 URI ,它的 schema 是已知的网络协议,并且它把 URI 与某种协议处理程序联系起来(一种与资源通讯的读 / 写机制)。
URI 一般不能为资源提供持久不变的名称。这是统一的资源命名( URN )的任务。 URN 也是一种 URI ,但是全球唯一的、持久不便的,即使资源不再存在或不再使用。
使用
URI
Java API 通过提供 URI 类(位于 java.net 包中),使我们在代码中使用 URI 成为可能。 URI 的构造函数建立 URI 对象,并且分析 URI 字符串,提取 URI 组件。 URI 的方法提供了如下功能: 1 )决定 URI 对象的 URI 是绝对的还是相对的; 2 )决定 URI 对象是 opaque 还是 hierarchical ; 3 )比较两个 URI 对象; 4 )标准化( normalize ) URI 对象; 5 )根据 Base URI 解析某个 Relative URI ; 6 )根据 Base URI 计算某个 URI 的相对 URI ; 7 )把 URI 对象转换为 URL 对象。
Java API 通过提供 URI 类(位于 java.net 包中),使我们在代码中使用 URI 成为可能。 URI 的构造函数建立 URI 对象,并且分析 URI 字符串,提取 URI 组件。 URI 的方法提供了如下功能: 1 )决定 URI 对象的 URI 是绝对的还是相对的; 2 )决定 URI 对象是 opaque 还是 hierarchical ; 3 )比较两个 URI 对象; 4 )标准化( normalize ) URI 对象; 5 )根据 Base URI 解析某个 Relative URI ; 6 )根据 Base URI 计算某个 URI 的相对 URI ; 7 )把 URI 对象转换为 URL 对象。
在
URI
里面有多个构造函数,最简单的是
URI(String uri)
。这个构造函数把
String
类型的参数
URI
分解为组件,并把这些组件存储在新的
URI
对象中。如果
String
对象的
URI
违反了
RFC 2396
的语法规则,将会产生一个
java.net.URISyntaxException
。
下面的代码演示了使用 URI(String uri) 建立 URI 对象:
下面的代码演示了使用 URI(String uri) 建立 URI 对象:
URI uri = new URI ("http://www.cnn.com");
|
如果知道 URI 是有效的,不会产生 URISyntaxException ,可以使用静态的 create(String uri) 方法。这个方法分解 uri ,如果没有违反语法规则就建立 URI 对象,否则将捕捉到一个内部 URISyntaxException ,并把该对象包装在一个 IllegalArgumentException 中抛出。
下面的代码片断演示了
create(String uri)
:
URI uri = URI.create ("http://www.cnn.com");
|
URI 构造函数和 create(String uri) 方法试图分解出 URI 的 authority 的用户信息、主机和端口部分。对于正确形式的字符串会成功,对于错误形式的字符串,他们将会失败。如果想确认某个 URI 的 authority 是基于服务器的,并且 能 分解出用户信息、主机和端口,这时候可以调用 URI 的 parseServerAuthority() 方法。如果成功分解出 URI ,该方法将返回包含用户信息、主机和端口部分的新 URI 对象,否则该方法将产生一个 URISyntaxException 。
下面的代码片断演示了 parseServerAuthority() :
//
下面的
parseServerAuthority()
调用出现后会发生什么情况?
URI uri = new URI ("//foo:bar").parseServerAuthority(); |
一旦拥有了
URI
对象,你就可以通过调用
getAuthority()
、
getFragment()
、
getHost()
、
getPath()
、
getPort()
、
getQuery()
、
getScheme()
、
getSchemeSpecificPart()
和
getUserInfo()
方法提取信息。以及
isAbsolute()
、
isOpaque()
等方法。
程序 1: URIDemo1.java
程序 1: URIDemo1.java
import java.net.*; public 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 命令后,输出结果如下:
query://[email protected]:9000/public/manuals/appliances?stove#ge Authority = [email protected]:9000 Fragment = ge Host = books.com Path = /public/manuals/appliances Port = 9000 Query = stove Scheme = query Scheme-specific part = //[email protected]:9000/public/manuals/appliances?stove User Info = jeff URI is absolute: true URI is opaque: false |
URI 类支持基本的操作,包括标准化( normalize )、分解( resolution )和相对化( relativize )。下例演示了 normalize() 方法。
程序 2: 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()); } } |
在命令行输入 java URIDemo2 x/y/../z/./q ,将看到下面的输出:
Normalized URI = x/z/q
上面的输出显示 y 、 .. 和 . 消失了。
URI
通过提供
resolve(String uri)
、
resolve(URI uri)
和
relativize(URI uri)
方法支持反向解析和相对化操作。如果指定的
URI
违反了
RFC 2396
语法规则,
resolve(String uri)
通过的内部的
create(String uri)
调用间接地产生一个
IllegalArgumentException
。
下面
的代码演示了
resolve(String uri)
和
relativize(URI uri)
。
程序 3: URIDemo3.java
程序 3: 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); URI uriRelative = new URI (args [1]); System.out.println ("Relative URI = " +uriRelative); URI uriResolved = uriBase.resolve (uriRelative); System.out.println ("Resolved URI = " +uriResolved); URI uriRelativized = uriBase.relativize (uriResolved); System.out.println ("Relativized URI = " +uriRelativized); } } |
编译 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 |
使用
URL
Java 提供了 URL 类,每一个 URL 对象都封装了资源标识符和协议处理程序。获得 URL 对象的途径之一是调用 URI 的 toURL() 方法,也可以直接调用 URL 的构造函数来建立 URL 对象。
Java 提供了 URL 类,每一个 URL 对象都封装了资源标识符和协议处理程序。获得 URL 对象的途径之一是调用 URI 的 toURL() 方法,也可以直接调用 URL 的构造函数来建立 URL 对象。
URL 类有多个构造函数。其中最简单的是 URL(String url) ,它有一个 String 类型的参数。如果某个 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() 、 getDefaultPort() 等方法提取各种组件。如果 URL 中没有指定端口, getDefaultPort() 方法返回 URL 对象的协议默认端口。 getFile() 方法返回路径和查询组件的结合体。 getProtocol() 方法返回资源的连接类型(例如 http 、 mailto 、 ftp )。 getRef() 方法返回 URL 的片断。最后, getUserInfo() 方法返回 Authority 的用户信息部分。还可以调用 openStream() 方法得到 java.io.InputStream 引用。使用这种引用,可以用面向字节的方式读取资源。
下面是
URLDemo1
的代码。该程序建立一个
URL
对象,调用
URL
的各种方法来检索该
URL
的信息,调用
URL
的
openStream()
方法打开与资源的连接并读取
/
打印这些字节。
程序 4: URLDemo1.java
程序 4: 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
…
|
URL 的 openStream() 方法返回的 InputStream 类型,这意味着你必须按字节次序读取资源数据,这种做法是恰当的,因为你不知道将要读取的数据是什么类型。如果你事先知道要读取的数据是文本,并且每一行以换行符( \n )结束,你就可以按行读取而不是按字节读取数据了。
下面的代码片断演示了把一个 InputStream 对象包装进 InputStreamReader 以从 8 位过渡到 16 位字符,进而把结果对象包装进 BufferedReader 以调用其 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() 方法时,它会返回某种对象的引用,而你可以调用该对象的方法(在转换成适当的类型后),采用更方便的方式取得数据。但是在调用该方法前,最好使用 instanceof 验证对象的类型,防止类产生异常。
对于 JPEG 资源, getContent() 返回一个对象,该对象实现了 java.awt.Image.ImageProducer 接口。下面的代码演示了使用如何 getContent() 。
URL url = new URL (args [0]); Object o = url.getContent (); if (o instanceof ImageProducer) { ImageProducer ip = (ImageProducer) o; // ... } |
查看一下 getContent() 方法的源代码,你会找到 openConnection().getContent() 。 URL 的 openConnection() 方法返回一个 java.net.URLConnection 对象。 URLConnection 的方法反映了资源和连接的细节信息,使我们能编写代码访问资源。
下面 的 URLDemo2 代码演示了 openConnection() ,以及如何调用 URLConnection 的方法。
程序 5: 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 (); // 打印 header 的内容 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 ()); } } |
URLConnection
的
getHeaderFields()
方法返回一个
java.util.Map
。该
map
包含
header
名称和值的集合。
header
是基于文本的名称
/
值对,它识别资源数据的类型、数据的长度等等。
编译 URLDemo2 后,在命令行输入 java URLDemo2 http://www.javajeff.com ,输出如下:
编译 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 |
仔细看一下前面的输出,会看到叫做 Content-Type 的东西。 Content-Type 识别了资源数据的类型是 text/html 。 text 部分叫做类型, html 部分叫做子类型。如果内容是普通的文本, Content-Type 的值可能是 text/plain 。 text/html 表明内容是文本的但是 html 格式的。
Content-Type
是多用途
Internet
邮件扩展(
MIME
)的一部分。
MIME
是传统的传输消息的
7
位
ASCII
标准的一种扩展。通过引入了多种
header
,
MIME
使视频、声音、图像、不同字符集的文本与
7
位
ASCII
结合起来。当使用
URLConnection
类的时候,你会遇到
getContentType()
和
getContentLength()
。这些方法返回的值是
Content-Type
和
Content