本帖最后由 小刀葛小伦 于 2020-1-9 20:12 编辑
前言在互联网中通信需要借助 IP 地址来定位到主机,而 IP 地址由很多数字组成,对于人类来说记住某些组合数字很困难,于是,为了方便大家记住某地址而引入主机名和域名。 早期的网络中的机器数量很少,能很方便地通过 hosts 文件来完成主机名称和 IP 地址的映射,这种方式需要用户自己维护网络上所有主机的映射关系。后来互联网迅猛发展起来,hosts 文件方式已经无法胜任,于是引入域名系统(DNS)来解决主机名称和 IP 地址的映射。 局域网中常用来表示 IP 地址的名称更多称为主机名,而互联网上用来表示 IP 地址的名称更多称为域名。核心内容都相同,都是解决名称和 IP 地址间的映射。 Java 中提供了很多互联网主机名称和地址操作相关的接口,现在来看看 JDK 内部对域名解析相关功能的实现。其实,InetAddress 类内部存在一个 NameService 内部接口用于实现域名及IP的映射。 对于 JDK 主要使用了两种映射解析方案,一种是 hosts 文件机制,另外一种是操作系统自带的解析方案。 相关类[Java] 纯文本查看 复制代码 --java.lang.Object
--java.net.InetAddress$HostsFileNameService
--java.net.InetAddress$PlatformNameService
JDK选择的方案以上两种主机名称 IP 映射机制,JDK 是怎样选择的呢?其实就是根据 jdk.net.hosts.file系统属性来确定的,默认情况下使用基于操作系统的 PlatformNameService 方案,而如果配置了jdk.net.hosts.file系统属性则使用基于 hosts 文件的 HostsFileNameService 方案,比如可以在启动时配置参数 -Djdk.net.hosts.file=/etc/hosts。对应逻辑代码如下: [Java] 纯文本查看 复制代码 private static NameService createNameService() {
String hostsFileName =
GetPropertyAction.privilegedGetProperty("jdk.net.hosts.file");
NameService theNameService;
if (hostsFileName != null) {
theNameService = new HostsFileNameService(hostsFileName);
} else {
theNameService = new PlatformNameService();
}
return theNameService;
} 接口定义
[Java] 纯文本查看 复制代码 private interface NameService {
InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException;
String getHostByAddr(byte[] addr) throws UnknownHostException;
}
NameService 接口主要定义了两个方法,用于获取主机名称对应的 IP 地址和 IP 地址对应的主机名称。 HostsFileNameService 类类定义如下: [Java] 纯文本查看 复制代码 private static final class HostsFileNameService implements NameService 该类即是对基于 hosts 文件方案的封装,主要看看核心的两个方法, lookupAllHostAddr方法该方法根据主机名称实现基于 hosts 文件的 IP 地址查找方案。它要完成的逻辑如下:
* 根据指定的 hosts 文件路径扫描每一行,如果不存在文件则抛出 FileNotFoundException 异常。
* 遍历每行内容,如果以 # 号开头则表示该行为注释内容,直接忽略,否则继续。
* 标准情况下内容可以为 127.0.0.1 localhost #local,# 号后面为注释内容,所以调用 removeComments 方法去掉 #local,该方法不再贴出。
* 处理后的内容为127.0.0.1 localhost,接着看是否包含了传进来的主机名称有的话则说明是该主机名称映射的 IP 地址,通过 extractHostAddr 方法提取IP地址,值为 127.0.0.1,该方法不再贴出。
* 处理后的内容为127.0.0.1字符串,需要调用 createAddressByteArray 将其转换为 byte 数组以方便得到 InetAddress 对象,该方法不再贴出。
* 将得到的 添加到 ArrayList 对象中,最终转换为 InetAddress 数组并返回。 [Java] 纯文本查看 复制代码 public InetAddress[] lookupAllHostAddr(String host)
throws UnknownHostException {
String hostEntry;
String addrStr = null;
InetAddress[] res = null;
byte addr[] = new byte[4];
ArrayList<InetAddress> inetAddresses = null;
try (Scanner hostsFileScanner = new Scanner(new File(hostsFile), "UTF-8")) {
while (hostsFileScanner.hasNextLine()) {
hostEntry = hostsFileScanner.nextLine();
if (!hostEntry.startsWith("#")) {
hostEntry = removeComments(hostEntry);
if (hostEntry.contains(host)) {
addrStr = extractHostAddr(hostEntry, host);
if ((addrStr != null) && (!addrStr.equals(""))) {
addr = createAddressByteArray(addrStr);
if (inetAddresses == null) {
inetAddresses = new ArrayList<>(1);
}
if (addr != null) {
inetAddresses.add(InetAddress.getByAddress(host, addr));
}
}
}
}
}
} catch (FileNotFoundException e) {
throw new UnknownHostException("Unable to resolve host " + host
+ " as hosts file " + hostsFile + " not found ");
}
if (inetAddresses != null) {
res = inetAddresses.toArray(new InetAddress[inetAddresses.size()]);
} else {
throw new UnknownHostException("Unable to resolve host " + host
+ " in hosts file " + hostsFile);
}
return res;
} getHostByAddr方法该方法根据 IP 地址实现基于 hosts 文件的主机名称查找方案。它要完成的逻辑如下:
* 传入的参数为 IP 地址的字节数组,比如new byte[] {127, 0, 0, 1},先调用 addrToString 方法将其转换为”127.0.0.1”字符串,该方法不再贴出。
* 根据指定的 hosts 文件路径扫描每一行,如果不存在文件则抛出 FileNotFoundException 异常。
* 遍历每行内容,如果以 # 号开头则表示该行为注释内容,直接忽略,否则继续。
* 标准情况下内容可以为 127.0.0.1 localhost #local,# 号后面为注释内容,所以调用 removeComments 方法去掉 #local,该方法不再贴出。
* 处理后的内容为127.0.0.1 localhost,接着看是否包含了传进来的 IP 地址,有的话则说明是该 IP 地址对应的主机名称,通过 extractHost 方法提取主机名称localhost,该方法不再贴出。
* 一旦找到主机名称后则不再往下遍历,跳出循环并返回主机名称。 [Java] 纯文本查看 复制代码 public String getHostByAddr(byte[] addr) throws UnknownHostException {
String hostEntry;
String host = null;
String addrString = addrToString(addr);
try (Scanner hostsFileScanner = new Scanner(new File(hostsFile), "UTF-8")) {
while (hostsFileScanner.hasNextLine()) {
hostEntry = hostsFileScanner.nextLine();
if (!hostEntry.startsWith("#")) {
hostEntry = removeComments(hostEntry);
if (hostEntry.contains(addrString)) {
host = extractHost(hostEntry, addrString);
if (host != null) {
break;
}
}
}
}
} catch (FileNotFoundException e) {
throw new UnknownHostException("Unable to resolve address "
+ addrString + " as hosts file " + hostsFile
+ " not found ");
}
if ((host == null) || (host.equals("")) || (host.equals(" "))) {
throw new UnknownHostException("Requested address "
+ addrString
+ " resolves to an invalid entry in hosts file "
+ hostsFile);
}
return host;
} PlatformNameService类
类定义如下:
[Java] 纯文本查看 复制代码 private static final class PlatformNameService implements NameService
该类即是对操作系统自带的解析方案的封装,核心的两个方法如下,因为这两个方法与操作系统相关,所以通过它们通过 InetAddressImpl 接口调用了对应的本地方法,本地方法分别为 lookupAllHostAddr 和 getHostByAddr。
[Java] 纯文本查看 复制代码 public InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException{
return impl.lookupAllHostAddr(host);
}
public String getHostByAddr(byte[] addr) throws UnknownHostException{
return impl.getHostByAddr(addr);
}
lookupAllHostAddr方法该本地方法中要完成的工作主要就是先通过操作系统提供的主机名称服务接口来获取对应的 IP 地址,然后再生成 InetAddress 对象数组,即要生成 Java 层的数据结构。 Windows 和 unix-like 操作系统实现的代码都比较长,这里不再贴出,核心就是通过 getaddrinfo 函数来实现名称解析,获取到主机名对应的所有地址。然后通过 JNI 的 NewObjectArray 函数创建对象数组,接着再通过 JNI 的 NewObject函数创建 InetAddress 对象并设置地址和主机名称的属性值,最后通过 JNI 的 SetObjectArrayElement 函数逐一将 InetAddress 对象放入数组中。 getaddrinfo 函数用于名称解析,可将域名转成对应的 IP 地址和端口。它查找时可能会去 DNS 服务器上查找指定域名对应的地址,也可能会在本地的 hosts 文件,也可能在其他的命名服务。而且一般每个域名都会对应多个 IP 地址。通过该函数获取到的结果为 addrinfo 结构体指针。 结构 | 参数 | typedef struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; char* ai_canonname; struct sockaddr* ai_addr; struct addrinfo* ai_next; } | ai_addrlen must be zero or a null pointer ai_canonname must be zero or a null pointer ai_addr must be zero or a null pointer ai_next must be zero or a null pointer ai_flags:AI_PASSIVE,AI_CANONNAME,AI_NUMERICHOST ai_family: AF_INET,AF_INET6 ai_socktype:SOCK_STREAM,SOCK_DGRAM ai_protocol:IPPROTO_IP, IPPROTO_IPV4, IPPROTO_IPV6 etc. | getHostByAddr方法该本地方法用于根据 IP 地址获取主机名,传入的参数为 byte[],返回为字符串。它要完成的工作就是通过操作系统提供的主机名称服务接口获取主机名,然后返回字符串。 Windows 和 unix-like 操作系统实现的代码都差不多,这里只贴出 Windows的,基本的逻辑为:先通过 JNI 的 GetByteArrayRegion 函数获取传入的4个字节,这里因为字节可能是负数,所以需要进行移位操作;然后通过 getnameinfo 函数获取主机名;最后通过 JNI 的 NewStringUTF 函数将主机名放到新建的字符串对象中。 [Java] 纯文本查看 复制代码 JNIEXPORT jstring JNICALL
Java_java_net_Inet4AddressImpl_getHostByAddr(JNIEnv *env, jobject this,
jbyteArray addrArray) {
jstring ret = NULL;
char host[NI_MAXHOST + 1];
jbyte caddr[4];
jint addr;
struct sockaddr_in sa;
memset((char *)&sa, 0, sizeof(struct sockaddr_in));
(*env)->GetByteArrayRegion(env, addrArray, 0, 4, caddr);
addr = ((caddr[0] << 24) & 0xff000000);
addr |= ((caddr[1] << 16) & 0xff0000);
addr |= ((caddr[2] << 8) & 0xff00);
addr |= (caddr[3] & 0xff);
sa.sin_addr.s_addr = htonl(addr);
sa.sin_family = AF_INET;
if (getnameinfo((struct sockaddr *)&sa, sizeof(struct sockaddr_in),
host, NI_MAXHOST, NULL, 0, NI_NAMEREQD)) {
JNU_ThrowByName(env, "java/net/UnknownHostException", NULL);
} else {
ret = (*env)->NewStringUTF(env, host);
if (ret == NULL) {
JNU_ThrowByName(env, "java/net/UnknownHostException", NULL);
}
}
return ret;
}
|