Java中網際網路地址解析方法與模式

banq發表於2024-04-13

在本教程中,我們將討論 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(<font>"www.google.com");
Assert.assertNotNull(inetAddress.getHostAddress());
// returns an IP Address<i>

3. getByAddress() API
這是執行反向查詢的最基本的 API,其中它將 IP 地址作為輸入並嘗試返回與其關聯的主機:

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

4. getCanonicalHostName() API 和getHostName() API
這些 API 執行類似的反向查詢並嘗試返回與其關聯的完全限定域名 (FQDN):

InetAddress inetAddress = InetAddress.getByAddress(ip); 
Assert.assertNotNull(inetAddress.getCanonicalHostName()); <font>// returns a FQDN<i>
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 <font>"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(<font>"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(<font>"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(<font>"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使用特定檔案進行主機匹配。然而,維持一份詳盡的清單是很困難的。


 

相關文章