Java中互联网地址解析方法与模式

在本教程中,我们将讨论 Java 的JEP 418,它为 Internet 主机和地址解析建立了新的服务提供商接口 (SPI) 。

什么是互联网地址解析
连接到计算机网络的任何设备都会分配一个数值或IP(互联网协议)地址。 IP 地址有助于唯一地识别网络上的设备,并且还有助于在设备之间路由数据包。

它们通常有两种类型。 IPv4是第四代IP标准,是32位地址。由于互联网的快速发展,还发布了较新的 IP 标准 v6,该标准更大并且包含十六进制字符。

此外,还有另一种相关类型的地址。网络设备(例如以太网端口或网络接口卡 (NIC))具有MAC(媒体访问控制)地址。它们是全球分布的,并且所有网络接口设备都可以通过 MAC 地址进行唯一标识。

互联网地址解析广义上是指将较高级别的网络地址转换为较低级别的网络地址(例如 IP)地址或 MAC 地址。

Java 中的 Internet 地址解析
如今,Java 使用java.net.InetAddress API提供了多种解析 Internet 地址的方法。 API 在内部使用操作系统的本机解析器进行DNS查找。

InetAddress API 当前使用的操作系统本机地址解析涉及多个步骤。涉及系统级 DNS 缓存,其中存储常用查询的 DNS 映射。如果本地 DNS 缓存中发生缓存未命中,系统解析器配置会提供有关 DNS 服务器的信息以执行后续查找。

然后,操作系统会向上一步中获取的配置的 DNS 服务器查询该信息。此步骤可能会递归发生几次。

如果匹配和查找成功,则 DNS 地址将缓存在所有服务器上并返回到原始客户端。然而,如果没有匹配,则会触发根服务器的迭代查找过程,提供有关权威 Nave 服务器 (ANS) 的信息。这些权威名称服务器 (ANS) 存储有关顶级域名 (TLD) 的信息,例如 .org、.com 等。

这些步骤最终将域与 Internet 地址匹配(如果该地址有效)或向客户端返回失败信息。

使用Java的InetAddress API
InetAddress API提供了多种执行 DNS 查询和解析的方法。这些 API 作为java.net包的一部分提供。

1. getAllByName() API
getAllByName () API 尝试将主机名映射到一组 IP 地址:

InetAddress[] inetAddresses =  InetAddress.getAllByName(host);
Assert.assertTrue(Arrays.stream(inetAddresses).map(InetAddress::getHostAddress).toArray(String[]::new) > 1);

这也称为前向查找。

2. getByName() API
getByName() API与之前的正向查找 API 类似,只不过它仅将主机映射到第一个匹配的 IP 地址:

InetAddress inetAddress = InetAddress.getByName("www.google.com");
Assert.assertNotNull(inetAddress.getHostAddress());
// returns an IP Address

3. getByAddress() API
这是执行反向查找的最基本的 API,其中它将 IP 地址作为输入并尝试返回与其关联的主机:

InetAddress inetAddress =  InetAddress.getByAddress(ip);
Assert.assertNotNull(inetAddress.getHostName()); // returns a host (eg. google.com)

4. getCanonicalHostName() API 和getHostName() API
这些 API 执行类似的反向查找并尝试返回与其关联的完全限定域名 (FQDN):

InetAddress inetAddress = InetAddress.getByAddress(ip); 
Assert.assertNotNull(inetAddress.getCanonicalHostName()); // returns a FQDN
Assert.assertNotNull(inetAddress.getHostName());

服务提供商接口(SPI)
服务提供商接口(SPI)模式是软件开发中使用的重要设计模式。此模式的目的是允许特定服务的可插入组件和实现。

它允许开发人员在不修改服务的任何核心期望的情况下扩展系统的功能,并使用任何实现而不受单一实现的束缚。

1. InetAddress中的 SPI 组件
遵循 SPI 设计模式,此 JEP 提出了一种用自定义解析器替换默认系统解析器的方法。 SPI 从 Java 18 开始可用。需要服务定位器来定位要使用的提供者。如果服务定位器无法识别任何提供者服务,它将返回到默认实现。

与任何 SPI 实现一样,有四个主要组件:

  1. 服务是第一个组件,是提供特定功能的接口和类的集合。在我们的例子中,我们正在将互联网地址解析作为服务来处理
  2. 服务提供者接口是充当服务代理的接口或抽象类。该接口将其定义的所有操作委托给其实现。InetAddressResolver接口是我们用例的服务提供商接口,它定义了查找主机名和 IP 地址以进行解析的操作
  3. 第三个组件是服务提供者,它定义了服务提供者接口的具体实现。 InetAddressResolverProvider是一个抽象类, 其用途是充当解析器的许多自定义实现的工厂。我们将通过扩展这个抽象类来定义我们的实现。JVM 维护一个系统范围的解析器,然后由InetAddress使用,并且通常在 VM 初始化期间设置
  4. 最后一个组件 Service Loader 组件将所有这些联系在一起。ServiceLoader机制将找到符合条件的InetAddressResolverProvider提供程序实现并将其设置为默认的系统范围解析器。如果发生故障,后备机制会在系统范围内设置默认解析器

2. InetAddressResolverProvider的自定义实现
通过此 SPI 进行的更改可在java.net.spi 包中找到,并且新添加了以下类:
  • InetAddressResolverProvider
  • InetAddressResolver
  • InetAddressResolver.LookupPolicy
  • InetAddressResolverProvider.Configuration

在本节中,我们将尝试为InetAddressResolver编写自定义解析器实现来替代系统默认解析器。在编写自定义解析器之前,我们可以定义一个小型实用程序类,它将地址映射注册表从文件加载到内存(或缓存)。

根据注册表项,我们的自定义地址解析器将能够将地址主机解析为 IP,反之亦然。

首先,我们通过从抽象类 InetAddressResolverProvider 扩展来定义我们的类CustomAddressResolverImpl  。这样做需要我们立即提供两个方法的实现:get(Configuration配置) 和name()。 

我们可以使用name()返回当前实现类的名称或任何其他相关标识符:

@Override
public String name() {
    return "CustomInternetAddressResolverImpl";
}

现在让我们实现get()方法。get()方法返回InetAddressResolver 类的实例,我们可以内联或单独定义该实例。为了简单起见,我们将内联定义它。

InetAddressResolver接口 有两个方法:

  • Stream<InetAddress> LookupByName(String host, LookupPolicy LookupPolicy) 抛出 UnknownHostException
  • String LookupByAddress(byte[] addr) 抛出 UnknownHostException

我们可以编写任何自定义逻辑来将主机映射到其 IP 地址(以InetAddress的形式),反之亦然。在这个例子中,我们将让我们的注册表功能处理同样的事情:

@Override
public InetAddressResolver get(Configuration configuration) {
    LOGGER.info("Using Custom Address Resolver :: " + this.name());
    LOGGER.info(
"Registry initialised");
    return new InetAddressResolver() {
        @Override
        public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException {
            return registry.getAddressesfromHost(host);
        }

        @Override
        public String lookupByAddress(byte[] addr) throws UnknownHostException {
            return registry.getHostFromAddress(addr);
        }
    };
}

3.注册表类实现
在本文中,我们将使用HashMap 在内存中存储 IP 地址和主机名列表。我们也可以从系统上的文件加载列表。

Map 的类型为Map<String, List<byte[]>>,其中主机名存储为键,IP 地址存储为byte []列表。此数据结构允许将多个 IP 映射到单个主机。我们可以使用这个Map 执行前向和后向查找。

在这种情况下,正向查找是指我们将主机名作为参数传递并期望根据其 IP 地址解析它,例如,当我们输入www.baeldung.com时:

public Stream<InetAddress> getAddressesfromHost(String host) throws UnknownHostException {
    LOGGER.info("Performing Forward Lookup for HOST : " + host);
    if (!registry.containsKey(host)) {
        throw new UnknownHostException(
"Missing Host information in Resolver");
    }
    return registry.get(host)
      .stream()
      .map(add -> constructInetAddress(host, add))
      .filter(Objects::nonNull);
}

我们应该注意到,响应是一个InetAddress流,用于容纳多个 IP。

反向查找的一个例子是当我们想知道与 IP 地址关联的主机名时:

public String getHostFromAddress(byte[] arr) throws UnknownHostException {
    LOGGER.info("Performing Reverse Lookup for Address : " + Arrays.toString(arr));
    for (Map.Entry<String, List<byte[]>> entry : registry.entrySet()) {
        if (entry.getValue()
          .stream()
          .anyMatch(ba -> Arrays.equals(ba, arr))) {
            return entry.getKey();
        }
    }
    throw new UnknownHostException(
"Address Not Found");
}

最后,ServiceLoader模块加载我们的 InetAddress 解析的自定义实现。

为了发现我们的服务提供者,我们在resources/META-INF/services 层次结构下创建一个名为java.net.spi.InetAddressResolverProvider 的配置。配置文件应将我们的提供程序的完全限定路径维护为com.baeldung.inetspi.providers.CustomAddressResolverImpl.java。 

这告诉 JVM 按照 SPI 模式加载提供者的相应实现。

替代解决方案
如果我们不想添加地址解析的自定义实现,我们有一些解决方法:

  • 使用 JNDI 及其 DNS 提供程序是使用 InetAddress 进行解析的替代方法;但是,我们无法利用 InetAddress 提供的丰富 API 来更轻松地访问
  • 我们可以通过panama项目的 JNI 使用操作系统的本机解析器
  • 最后,我们可以直接修改 JDK 系统属性文件,例如jdk.net.hosts.file,以通知InetAddress使用特定文件进行主机匹配。然而,维持一份详尽的清单是很困难的。