java.net.InetAddress类是Java对IP地址(包括IPv4和IPv6)的高层表示。大多数其他网络类都要用到这个类,包括Socket,ServerSocket,URL,DatagramSocket,DatagramPacket等。一般地讲,它包括一个主机名和一个IP地址。
创建新的InetAddress对象
InetAddress类没有公共构造函数。实际上,InetAddress有一些静态工厂方法,可以连接到DNS服务器来解析主机名。最常用的是InetAddress.getByName()。 例如,可以如下查找www.oreilly.com:
InetAddress address = InetAddress.getByName("www.oreilly.com");
这个方法并不只是设置InetAddress类中的一个私有String字段。实际上它会建立与本地DNS服务器的一个连接,来查找名字和数字地址(如果你之前查找过这个主机,这个信息可能会在本地缓存,如果是这样,就不需要再建立网络连接)。如果DNS服务器找不到这个地址,这个方法会抛出一个UnknownHostException异常,这是IOException的一个子类。
下例展示了一个完整的程序,它为www.oreilly.com创建一个InetAddress对象,这里包括所有必要的导入和异常处理:
package o2;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class OreillyByName {
public static void main(String[] args) {
try {
InetAddress address = InetAddress.getByName("www.oreilly.com");
System.out.println(address.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
之前我们提到过www.oreilly.com实际上有两个地址。getByName()返回哪一个地址是不确定的。如果出于某种原因你需要得到一个主机的所有地址,可以调用getAllByName(),它会返回一个数组:
package o2;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class OreillyByName {
public static void main(String[] args) {
try {
InetAddress[] addrs = InetAddress.getAllByName("www.oreilly.com");
if(addrs != null && addrs.length > 0){
for(InetAddress addr : addrs){
System.out.println(addr.getHostAddress());
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
最后,getLocalHost()方法会为运行这个代码的主机返回一个InetAddress对象:
InetAddress me = InetAddress.getLocalHost();
这个方法尝试连接DNS来得到一个真正的主机名和IP地址,如“elharo.laptop.corp.com”和“192.1.254.68”;不过如果失败,它就会返回回送地址,即主机名“localhost”和点分四段地址“127.0.0.1”。查找本地机器的地址示例:
package o2;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class MyAddress {
public static void main(String[] args) {
try {
InetAddress address = InetAddress.getLocalHost();
System.out.println(address.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
如果你知道一个数字地址,可以由这个地址创建一个InetAddress对象,而不必使用InetAddress.getByName()与DNS交互。这个方法可以为不存在或者无法解析的主机创建地址:
public static InetAddress getByAddress(byte[] addr)
throws UnknownHostException
public static InetAddress getByAddress(String hostname,byte[] addr)
throws UnknownHostException
第一个InetAddress.getByAddress()工厂方法用一个IP地址(而没有主机名)创建一个InetAddress对象。第二个InetAddress.getByAddress()方法使用一个IP地址和一个主机名创建InetAddress对象。例如:
byte[] addr = {107,23,(byte)216,(byte)196};
InetAddress lessWrong = InetAddress.getByAddress(addr);
InetAddress lessWrongWithName = InetAddress.getByAddress("lesswrong.com", addr);
需要说明,它必须把两个大数字转换为字节(上例中)。与其他工厂方法不同,这两个方法不能保证这个主机一定存在,或者主机名能正确地映射到IP地址。只有当作为address参数传入的字节数组大小不合法时(不是4字节,也不是16字节),这两个方法才会抛出一个UnknownHostException异常。
缓存
由于DNS查找的开销可能相当大(如果请求需要经过多个中间服务器,或者尝试解析一个不可达的主机,这大约需要几秒的时间),所以InetAddress类会缓存查找的结果。一旦得到一个给定主机的地址,就不会再次查找,即使你为同一个主机创建一个新的InetAddress对象,也不会再次查找地址。只要在程序运行期间IP地址没有改变,这就没有问题。
负面结果有可能刚开始尝试解析一个主机时失败,但随后再次尝试时解析会成功,这种情况并不少见。由于从远程DNS服务器发来的信息还在传输中,第一次尝试超时。然后这个地址到达本地服务器,所以下一次请求时可用。出于这个原因,Java对于不成功的DNS查询只缓存10秒。
这些时间可以用系统属性 networkaddress.cache.ttl和networkaddress.cache.negative.ttl来控制。其中第一个属性networkaddress.cache.ttl指定了成功的DNS查找结果在Java缓存中保留的时间(秒数),networkaddress.cache.negative.ttl指定了不成功的查找结果缓存的时间(秒数)。在这些时限内,再次尝试查找相同的主机会返回相同的值。-1解释为“永不过期”。
按IP地址查找
调用getByName()并提供一个IP地址串作为参数时,会为所请求的IP地址创建一个InetAddress对象,而不检查DNS。这说明,可能会为实际上不存在也无法连接的主机创建InetAddress对象。由包含IP地址的字符串来创建InetAddress对象时,这个对象的主机名初始设置为这个字符串。只有当请求主机名时(显式地通过getHostName()请求),才会真正完成主机名的DNS查找。例:
package o2;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ByName {
public static void main(String[] args) {
try {
InetAddress address = InetAddress.getByName("208.201.239.37");
System.out.println(address.getHostName());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
如果请求主机名并最终完成了一个DNS查找,但是指定IP地址的主机无法找到,那么主机名会保持为最初的点分四段字符串。不过,不会抛出UnknownHostException异常。主机名要比IP地址稳定得多。有些服务多年以来一直使用一个主机名,但IP更换了很多次。如果要在使用主机名(如www.oreilly.com)或使用IP地址(如208.201.239.371)之间做出选择,一定要选择主机名。只有当主机名不可用时才使用IP地址。
安全性问题
从主机名创建一个新的InetAddress对象被认为是一个潜在的不安全操作,因为这需要一个DNS查找。在默认安全管理器控制下的不可信applet允许获得它的初始主机(其代码基)的IP地址,这可能是本地主机。不允许不可信代码用任何其他主机名创建InetAddress对象。不论代码使用InetAddress.getByName()方法,InetAddress.getAllByName()方法,InetAddress.getLocalHost()方法,还是其他方法,都是如此。不可信代码可以由字符串形式的IP地址构造InetAddress对象,但不会为这样的地址完成DNS查找。
由于禁止与代码基之外的主机建立网络连接,不可信的代码不允许对第三方主机完成任意的DNS查找。任意的DNS查找会打开一个隐藏的通道,通过它,程序可以与第三方主机对话。例如,假设一个从www.bigisp.com下载的applet希望将消息“macfaq.dialup.cloud9.net is vulnerable”发送给crackersinc.com。它只需要请求macfaq.dialup.cloud9.net.is .valnerable.crackersinc.com的DNS信息。为了解析这个主机名,这个applet会联系本地DNS服务器。本地DNS服务器会联系位于crackersinc.com的DNS服务器。尽管这些主机不存在,但黑客可以查看crackersinc.com的DNS错误日志来获取这个消息。如果再结合压缩,纠错,加密,以及将电子邮件消息发送给一个第四方网站的定制DNS服务器,这个机制还可以复杂得多,但这个版本已经足以证明上述观点。由于任意DNS查找会泄漏信息,所以要禁止任意的DNS查找。
不可信代码允许调用InetAddress.getLocalHost()。不过,在这种环境下,getLocalHost()总是返回主机名localhost/127.0.0.1。禁止applet找出真正的主机名和地址的原因在于,运行applet的计算机可能故意隐藏在防火墙的后面。在这种情况下,applet不应作为通道来获得web服务器还没有的信息。
与所有安全性检查一样,禁止DNS解析可以对可信代码放宽要求。要测试一个主机能否解析,所用的特定SecurityManager方法是checkConnect():
public void checkConnect(String hostname, int port)
当port参数为-1时,这个方法检查能否调用DNS解析指定的hostname。(如果port参数大于-1,这个方法检查是否允许在指定端口对指定主机建立连接)。hostname参数可以是主机名(如www.oreilly.com),也可以是点分四段IP地址(如208.201.239.37),或者还可以以十六进制IPv6地址如FEDC::DC:0:7076:10。