從JDK原始碼看Java域名解析

超人汪小建發表於2018-07-23

前言

在網際網路中通訊需要藉助 IP 地址來定位到主機,而 IP 地址由很多數字組成,對於人類來說記住某些組合數字很困難,於是,為了方便大家記住某地址而引入主機名和域名。

早期的網路中的機器數量很少,能很方便地通過 hosts 檔案來完成主機名稱和 IP 地址的對映,這種方式需要使用者自己維護網路上所有主機的對映關係。後來網際網路迅猛發展起來,hosts 檔案方式已經無法勝任,於是引入域名系統(DNS)來解決主機名稱和 IP 地址的對映。

區域網中常用來表示 IP 地址的名稱更多稱為主機名,而網際網路上用來表示 IP 地址的名稱更多稱為域名。核心內容都相同,都是解決名稱和 IP 地址間的對映。

Java 中提供了很多網際網路主機名稱和地址操作相關的介面,現在來看看 JDK 內部對域名解析相關功能的實現。其實,InetAddress 類內部存在一個 NameService 內部介面用於實現域名及IP的對映。

對於 JDK 主要使用了兩種對映解析方案,一種是 hosts 檔案機制,另外一種是作業系統自帶的解析方案。

相關類

--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。對應邏輯程式碼如下:

    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;
    }
複製程式碼

介面定義

private interface NameService {

InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException;

String getHostByAddr(byte[] addr) throws UnknownHostException;

}
複製程式碼

NameService 介面主要定義了兩個方法,用於獲取主機名稱對應的 IP 地址和 IP 地址對應的主機名稱。

HostsFileNameService 類

類定義如下:

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 陣列並返回。
  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,該方法不再貼出。
  • 一旦找到主機名稱後則不再往下遍歷,跳出迴圈並返回主機名稱。
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類

類定義如下:

private static final class PlatformNameService implements NameService
複製程式碼

該類即是對作業系統自帶的解析方案的封裝,核心的兩個方法如下,因為這兩個方法與作業系統相關,所以通過它們通過 InetAddressImpl 介面呼叫了對應的本地方法,本地方法分別為 lookupAllHostAddr 和 getHostByAddr。

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 函式將主機名放到新建的字串物件中。

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;
}
複製程式碼

-------------推薦閱讀------------

我的2017文章彙總——機器學習篇

我的2017文章彙總——Java及中介軟體

我的2017文章彙總——深度學習篇

我的2017文章彙總——JDK原始碼篇

我的2017文章彙總——自然語言處理篇

我的2017文章彙總——Java併發篇

------------------廣告時間----------------

公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

歡迎關注:

這裡寫圖片描述

相關文章