2.1 套接字地址

回顾前面章节所讲的内容,一个客户端要发起一次通信,首先必须知道运行服务器端程序的主机的IP地址。然后由网络基础设施利用目标地址(destination address),将客户端发送的信息传递到正确的主机上。在Java中,地址可以由一个字符串来定义,这个字符串可以是数字型的地址(不同版本的IP地址有不同的型式,如192.0.2.27是一个IPv4地址,fe20:12a0:0abc:1234是一个IPv6地址),也可以是主机名(如server.example.com)。在后面的例子中,主机名必须被解析(resolved)成数字型地址才能用来进行通信。

InetAddress类代表了一个网络目标地址,包括主机名和数字类型的地址信息。该类有两个子类,Inet4Address和Inet6Address,分别对应了目前IP地址的两个版本。InetAddress实例是不可变的,一旦创建,每个实例就始终指向同一个地址。我们将通过一个示例程序来示范InetAddress类的用法。在这个例子中,首先打印出与本地主机关联的所有IP地址,包括IPv4和IPv6,然后对于每个在命令行中指定的主机,打印出其相关的主机名和地址。

为了获得本地主机地址,示例程序利用了NetworkInterface类的功能。前面已经讲过,IP地址实际上是分配给了主机与网络之间的连接,而不是主机本身。NetworkInterface类提供了访问主机所有接口的信息的功能。这个功能非常有用,比如当一个程序需要通知其他程序其IP地址时就会用到。

InetAddressExample.java

figure_0021_0004

figure_0021_0005

figure_0022_0006

1.获取主机的网络接口列表:第9行

静态方法getNetworkInterfaces()返回一个列表,其中包含了该主机每一个接口所对应的NetworkInterface类实例。

2.空列表检测:第10~12行

通常情况下,即使主机没有任何其他网络连接,回环接口(loopback)也总是存在的。因此,只有当一个主机根本没有网络子系统时,列表检测才为空。

3.获取并打印出列表中每个接口的地址:第13~27行

·打印接口名:第15行

getName()方法为接口返回一个本地名称。接口的本地名称通常由字母与数字的联合组成,代表了接口的类型和具体实例,如“lo0”或“eth0”。

·获取与接口相关联的地址:第16行

getInetAddresses()方法返回了另一个Enumeration类对象,其中包含了InetAddress类的实例,即该接口所关联的每一个地址。根据主机的不同配置,这个地址列表可能只包含IPv4或IPv6地址,或者包含了两种类型地址的混合列表。

·空列表检测:第17~19行

·列表的迭代,打印出每个地址:第20~26行

对每个地址实例进行检测以判断其属于哪个IP地址子类(目前InetAddress的子类只有上面列出的那些,但可以想象到,将来也许还会有其他子类)。InetAddress类的getHostAddress()方法返回一个字符串来代表主机的数字型地址。不同类型的地址对应了不同的格式:IPv4是点分形式,IPv6是冒号分隔的16进制形式。参考下文中的“字符串表示法”概要,其对不同类型的IP地址格式进行了描述。

4.捕获异常:第29~31行

对getNetworkInterfaces()方法的调用将会抛出SocketException异常。

5.获取从命令行输入的每个参数所对应的主机名和地址:第34~44行

·获取给定主机名/地址的相关地址列表:第37行

·迭代列表,打印出列表中的每一项:第38~40行

对于列表中的每个主机,我们通过调用getHostName()方法来打印主机名,并把调用getHostAddress()方法所获得的数字型地址打印在主机名后面。

为了使用这个应用程序来获取本地主机信息、网站(www.mkp.com)服务器信息、一个虚假地址信息(blah.blah)以及一个IP地址的信息,需要在命令行中运行如下代码:

figure_0023_0007

运行结果为:

figure_0023_0008

你也许已经注意到,一些IPv6地址带有%d型式的后缀,其中d是一个数字。这样的地址在一个有限的范围内(通常它们是本地链接),其后缀表明了该地址所关联的特定范围。这就保证了列出的每个地址字符串都是唯一的。IPv6的本地链接地址由fe8开头。

你可能还注意到,当程序解析blah.blah这个虚假地址时,会有一定的延迟。地址解析器在放弃对一个主机名的解析之前,会到多个不同的地方查找该主机名。如果由于某些原因使名字服务失效(例如由于程序所运行的机器并没有连接到所有网络),试图通过名字来定位一个主机就可能失败。而且这还将耗费大量的时间,因为系统将尝试各种不同的方法来将主机名解析成IP地址,因此最好能直接使用点分形式的IP地址来访问一个主机。在本书的所有例子中,如果远程主机由名字指定,运行示例程序的主机必须配置为能够将名字解析成地址,否则示例程序将无法正确运行。如果能通过主机的名字ping到该主机(如,在命令行窗口中执行命令“ping server.example.com”),那么在示例程序中就可以使用主机名。如果ping测试失败或示例程序挂起,可以尝试使用IP地址来定位主机,这就完全避免了从名字到地址的转换。(参见后文将要讨论的InetAddress类的isReachable()方法)

InetAddress:创建和访问

figure_0024_0009

这些静态工厂方法所返回的实例能够传递给另一个套接字方法来指定一个主机。这些方法的输入字符串可以是一个域名,如“skeezix”或“farm.example.com”,也可以是一个代表数字型地址的字符串。对于IPv6地址,第1章所提到的缩写形式同样适用。一个名字可能关联了多个数字地址,getAllByName()方法用于返回一组与给定主机名相关联的所有地址的实例。

getAddress()方法返回一个适当长度的字节数组,代表地址的二进制的形式。如果是一个Inet4Address实例,该数组的大小为4个字节;如果是Inet6Address实例,则为16个字节。返回的数组的第一个元素是该地址中最重要的字节。

我们已看到,一个InetAddress实例可以通过多种方式转换成字符串形式。

InetAddress:字符串表示

figure_0024_0010

上面这些方法返回主机名或数字型地址,或者以一定格式的字符串返回两者的联合形式。toString()方法重写了Object类的方法,返回如“hostname.example.com/192.0.2.127”或“never.example.net/2000:620:1a30:95b2”形式的字符串。单一的数字型地址表示形式由getHostAddress()方法返回。对于IPv6地址,字符串中总是包含了完整的8组数字(即显式地列出了7个“:”),这样做是为了消除二义性。因为通常情况下,地址字符串后还会附有由另一个分号隔开的端口号,后面我们将看到这样的例子。而且,对于有范围限制的IPv6地址,如本地链接地址,还会在后面附有一个范围标识符(scope identifier)。这只是一个用于消除二义性(因为同样的本地链接地址能用于不同的链接中)的本地标识符,不是数据报文中所传输的地址的一部分。

最后两个方法只返回主机名,它们的区别在于:如果实例最初通过主机名创建,getHostName()则直接返回这个名字,没有解析的步骤;否则,getHostName()要通过系统配置的名字解析机制将地址解析成名字。另一方面,getCanonicalName()方法总是尝试对地址进行解析,以获取主机域名全称(fully qualified domain name),如“ns1.internat.net”或“bam.example.com”。注意,如果不同名字映射到了同一地址,该方法所返回的主机名可能与最初用于创建实例的主机名不同。如果名字解析失败,两个方法都将返回数字型地址,而且在发送任何消息之前,都将用安全管理器进行许可检查。

InetAddress类还支持地址属性的检查,如判断其是否属于1.2节提到的“特殊用途”地址中的某一类以及检测其可达性,即与主机进行报文交互的能力。

InetAddress:检测属性

figure_0025_0011

这些方法检查一个地址是否属于某个特定类型。它们对IPv4地址和IPv6地址都适用。上述前三个方法分别检查地址实例是否属于“任意”本地地址、本地链接地址或回环地址(匹配127...*或:1的形式)。第4个方法检查其是否为一个多播地址(见4.3.2节),而isMC……()形式的方法检测多播地址的各种范围(scope)。(范围粗略地定义了到达该目的地址的数据报文从它的起始地址开始,所能传递的最远距离。)

最后两个方法检查是否真能与InetAddress地址确定的主机进行数据报文交换。注意,与其他句法检查方法不一样的是,这些方法引起网络系统执行某些动作,即发送数据报文。系统不断尝试发送数据报文,直到指定的时间(以ms为单位)用完才结束。后面这种形式更详细:它明确指出数据报文必须经过指定的NetworkInterface,并检查其是否能在指定的生命周期(time-to-live,TTL)内联系上目的地址。TTL限制了一个数据报文在网络上能够传输的距离。后面两个方法的有效性通常还受到安全管理配置方面的限制。

NetworkInterface类提供了更多的方法,其中有很多方法不属于本书的讨论范围。下面,我们只对与我们所讨论的问题最有用的方法进行描述。

NetworkInterface:创建,获取信息

figure_0026_0012

上面第一个方法非常有用,使用它可以很容易获取到运行程序的主机的IP地址:通过getNetworkInterfaces()方法可以获取一个接口列表,再使用实例的getInetAddresses()方法就可以获取每个接口的所有地址。注意:这个列表包含了主机的所有接口,包括不能够向网络中的其他主机发送或接收消息的虚拟回环接口。同样,列表中可能还包括外部不可达的本地链接地址。由于这些列表都是无序的,所以你不能简单地认为,列表中第一个接口的第一个地址一定能够通过互联网访问,而是要通过前面提到的InetAddress类的属性检查方法,来判断一个地址不是回环地址,不是本地链接地址等。

getName()方法返回一个接口(interface)的名字(不是主机名)。这个名字由字母字符串加上一个数字组成,如eth0。在很多系统中,回环地址的名字都是lo0。

[1]提示:对于本书中描述的每个Java网络相关类,我们只介绍了其最重要和最常用的方法,省略了那些不建议使用或不在本书讨论目标范围以内的方法。然而,这是一个不断变化的目标。比如,从Java 1.3版到1.6版,Socket类所提供的方法就从23个增加到了43个。建议读者在网站http://java.sun.com中参考Java API规范文档,那里包含了目前最新的正式资源。