歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Java DNS查詢內部實現

Java DNS查詢內部實現

日期:2017/3/1 9:14:15   编辑:Linux編程

源碼分析

在Java中,DNS相關的操作都是通過通過InetAddress提供的API實現的。比如查詢域名對應的IP地址:

String dottedQuadIpAddress = InetAddress.getByName( "blog.arganzheng.me" ).getHostAddress();

或者反過來IP對應域名:

        InetAddress[] addresses = InetAddress.getAllByName("8.8.8.8"); // ip or DNS name
        for (int i = 0; i < addresses.length; i++) {
            String hostname = addresses[i].getHostName();
            System.out.println(hostname);
        }

輸出:

google-public-dns-a.google.com

那麼InetAddress是如何實現DNS解析的呢?讓我們深入代碼一步步挖掘下去:

import java.net.UnknownHostException;

public class InetAddress extends java.net.InetAddress implements java.io.Serializable {

    public static InetAddress getByName(String host)
            throws UnknownHostException {
        return InetAddress.getAllByName(host)[0];
    }

    public static InetAddress[] getAllByName(String host)
            throws UnknownHostException {
        return getAllByName(host, null);
    }

    private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
            throws UnknownHostException {

        // ... 省略對於IPV6地址判斷,HostName或者IP地址判斷
        // hostname, resolve it
        return getAllByName0(host, reqAddr, true);
    }


    private static InetAddress[] getAllByName0(String host, InetAddress reqAddr, boolean check)
            throws UnknownHostException {

    /* If it gets here it is presumed to be a hostname */
    /* Cache.get can return: null, unknownAddress, or InetAddress[] */

    /* make sure the connection to the host is allowed, before we
     * give out a hostname
     */
        if (check) {  // 安全檢查
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkConnect(host, -1);
            }
        }

        // 從DNS Cache中獲取
        InetAddress[] addresses = getCachedAddresses(host);

    /* If no entry in cache, then do the host lookup */
        if (addresses == null) {
            addresses = getAddressesFromNameService(host, reqAddr);
        }

        if (addresses == unknown_array)
            throw new UnknownHostException(host);

        return addresses.clone();
    }

這裡需要注意,JVM會先查詢DNS緩存。有一個問題:默認的緩存時間是永遠!這個是JDK實現中比較坑的地方。

InetAddress Caching

The InetAddress class has a cache to store successful as well as unsuccessful host name resolutions. By default, when a security manager is installed, in order to protect against DNS spoofing attacks, the result of positive host name resolutions are cached forever. When a security manager is not installed, the default behavior is to cache entries for a finite (implementation dependent) period of time. The result of unsuccessful host name resolution is cached for a very short period of time (10 seconds) to improve performance.

If the default behavior is not desired, then a Java security property can be set to a different Time-to-live (TTL) value for positive caching. Likewise, a system admin can configure a different negative caching TTL value when needed.

Two Java security properties control the TTL values used for positive and negative host name resolution caching:

networkaddress.cache.ttl Indicates the caching policy for successful name lookups from the name service. The value is specified as as integer to indicate the number of seconds to cache the successful lookup. The default setting is to cache for an implementation specific period of time. A value of -1 indicates "cache forever".

networkaddress.cache.negative.ttl (default: 10) Indicates the caching policy for un-successful name lookups from the name service. The value is specified as as integer to indicate the number of seconds to cache the failure for un-successful lookups. A value of 0 indicates "never cache". A value of -1 indicates "cache forever".

如果Cache miss,那麼就會調用配置的nameServices執行真正DNS查詢:

private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
    throws UnknownHostException
{
    InetAddress[] addresses = null;
    boolean success = false;
    UnknownHostException ex = null;

    // Check whether the host is in the lookupTable.
    // 1) If the host isn't in the lookupTable when
    //    checkLookupTable() is called, checkLookupTable()
    //    would add the host in the lookupTable and
    //    return null. So we will do the lookup.
    // 2) If the host is in the lookupTable when
    //    checkLookupTable() is called, the current thread
    //    would be blocked until the host is removed
    //    from the lookupTable. Then this thread
    //    should try to look up the addressCache.
    //     i) if it found the addresses in the
    //        addressCache, checkLookupTable()  would
    //        return the addresses.
    //     ii) if it didn't find the addresses in the
    //         addressCache for any reason,
    //         it should add the host in the
    //         lookupTable and return null so the
    //         following code would do  a lookup itself.
    if ((addresses = checkLookupTable(host)) == null) {
        try {
            // This is the first thread which looks up the addresses
            // this host or the cache entry for this host has been
            // expired so this thread should do the lookup.

            /*  
             * 這裡可以看到nameServices是鏈狀的,這是JDK7+的邏輯。 
             * 插入自定義nameservice��邏輯就在這裡。
             */
            for (NameService nameService : nameServices) { 
                try {
                    /*
                     * Do not put the call to lookup() inside the
                     * constructor.  if you do you will still be
                     * allocating space when the lookup fails.
                     */

                    addresses = nameService.lookupAllHostAddr(host);
                    success = true;
                    break;
                } catch (UnknownHostException uhe) {
                    if (host.equalsIgnoreCase("localhost")) {
                        InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
                        addresses = local;
                        success = true;
                        break;
                    }
                    else {
                        addresses = unknown_array;
                        success = false;
                        ex = uhe;
                    }
                }
            }

            // More to do?
            if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
                // Find it?
                int i = 1;
                for (; i < addresses.length; i++) {
                    if (addresses[i].equals(reqAddr)) {
                        break;
                    }
                }
                // Rotate
                if (i < addresses.length) {
                    InetAddress tmp, tmp2 = reqAddr;
                    for (int j = 0; j < i; j++) {
                        tmp = addresses[j];
                        addresses[j] = tmp2;
                        tmp2 = tmp;
                    }
                    addresses[i] = tmp2;
                }
            }
            // Cache the address.
            cacheAddresses(host, addresses, success);

            if (!success && ex != null)
                throw ex;

        } finally {
            // Delete host from the lookupTable and notify
            // all threads waiting on the lookupTable monitor.
            updateLookupTable(host);
        }
    }

    return addresses;
}

這裡有一段非常關鍵的代碼:

for (NameService nameService : nameServices) { 
    try {
        /*
         * Do not put the call to lookup() inside the
         * constructor.  if you do you will still be
         * allocating space when the lookup fails.
         */
        addresses = nameService.lookupAllHostAddr(host);
        success = true;
        break;
    } catch (UnknownHostException uhe) {
        if (host.equalsIgnoreCase("localhost")) {
            InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
            addresses = local;
            success = true;
            break;
        }
        else {
            addresses = unknown_array;
            success = false;
            ex = uhe;
        }
    }
}

這是真正執行DNS查詢的地方,而且可以看到是通過NameService鏈來依次解析的。其中nameServices是InetAddress的一個成員變量:

/* Used to store the name service provider */
private static List<NameService> nameServices = null;

通過static塊進行初始化的:

static {
    // create the impl
    impl = InetAddressImplFactory.create();

    // get name service if provided and requested
    String provider = null;;
    String propPrefix = "sun.net.spi.nameservice.provider.";
    int n = 1;
    nameServices = new ArrayList<NameService>();
    provider = AccessController.doPrivileged(
            new GetPropertyAction(propPrefix + n));
    while (provider != null) {
        NameService ns = createNSProvider(provider);
        if (ns != null)
            nameServices.add(ns);

        n++;
        provider = AccessController.doPrivileged(
                new GetPropertyAction(propPrefix + n));
    }

    // if not designate any name services provider,
    // create a default one
    if (nameServices.size() == 0) {
        NameService ns = createNSProvider("default");
        nameServices.add(ns);
    }
}

因為是通過InetAddress的static塊初始化的,所以必須在使用InetAddress之前配置sun.net.spi.nameservice.provider.<n>=<default|dns,sun|...>配置項。否則不會生效。具體可以參見SO上的這篇帖子:Clean DNS server in JVM。

另外需要注意的是在JDK7之前,只有第一個成功加載的nameservice參與解析。其他的nameservice並不起作用。

sun.net.spi.nameservice.provider.<n>=<default|dns,sun|...>

Specifies the name service provider that you can use. By default, Java will use the system configured name lookup mechanism, such as file, nis, etc. You can specify your own by setting this option. takes the value of a positive number, it indicates the precedence order with a small number takes higher precendence over a bigger number. Aside from the default provider, the JDK includes a DNS provider named "dns,sun".

Prior to JDK 7, the first provider that was successfully loaded was used. In JDK 7, providers are chained, which means that if a lookup on a provider fails, the next provider in the list is consulted to resolve the name.

不過我看一下OpenJDK6的代碼,貌似也是鏈狀的。

然後我們來看看NameService的定義:

package sun.net.spi.nameservice;

import java.net.UnknownHostException;

public interface NameService {
    public java.net.InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException;
    public String getHostByAddr(byte[] addr) throws UnknownHostException;
}

其實是個很簡單的接口。默認有兩個實現:

  • 匿名類:就是provider名稱為default的實現類
  • DNSNameService

private static NameService createNSProvider(String provider) { if (provider == null) return null;

  NameService nameService = null;
  if (provider.equals("default")) {
      // initialize the default name service
      nameService = new NameService() {
          public InetAddress[] lookupAllHostAddr(String host)
              throws UnknownHostException {
              return impl.lookupAllHostAddr(host);
          }
          public String getHostByAddr(byte[] addr)
              throws UnknownHostException {
              return impl.getHostByAddr(addr);
          }
      };
  } else {
      final String providerName = provider;
      try {
          nameService = java.security.AccessController.doPrivileged(
              new java.security.PrivilegedExceptionAction<NameService>() {
                  public NameService run() {
                      Iterator<NameServiceDescriptor> itr =
                          ServiceLoader.load(NameServiceDescriptor.class)
                              .iterator();
                      while (itr.hasNext()) {
                          NameServiceDescriptor nsd = itr.next();
                          if (providerName.
                              equalsIgnoreCase(nsd.getType()+","
                                  +nsd.getProviderName())) {
                              try {
                                  return nsd.createNameService();
                              } catch (Exception e) {
                                  e.printStackTrace();
                                  System.err.println(
                                      "Cannot create name service:"
                                       +providerName+": " + e);
                              }
                          }
                      }

                      return null;
                  }
              }
          );
      } catch (java.security.PrivilegedActionException e) {
      }
  }

  return nameService;
}

默認的NameService其實將域名解析操作委托給了impl變量:

static InetAddressImpl  impl;

InetAddressImpl本身也是一個接口:

package java.net;
import java.io.IOException;
/*
 * Package private interface to "implementation" used by
 * {@link InetAddress}.
 * <p>
 * See {@link java.net.Inet4AddressImp} and
 * {@link java.net.Inet6AddressImp}.
 *
 * @since 1.4
 */
interface InetAddressImpl {

    String getLocalHostName() throws UnknownHostException;
    InetAddress[]
        lookupAllHostAddr(String hostname) throws UnknownHostException;
    String getHostByAddr(byte[] addr) throws UnknownHostException;

    InetAddress anyLocalAddress();
    InetAddress loopbackAddress();
    boolean isReachable(InetAddress addr, int timeout, NetworkInterface netif,
                        int ttl) throws IOException;
}

有兩個實現:

  • Inet4AddressImpl
  • Inet6AddressImpl

這裡我們僅僅挑選Inet4AddressImpl說明。

package java.net;
import java.io.IOException;

/*
 * Package private implementation of InetAddressImpl for IPv4.
 *
 * @since 1.4
 */
class Inet4AddressImpl implements InetAddressImpl {
    public native String getLocalHostName() throws UnknownHostException;
    public native InetAddress[]
        lookupAllHostAddr(String hostname) throws UnknownHostException;
    public native String getHostByAddr(byte[] addr) throws UnknownHostException;
    private native boolean isReachable0(byte[] addr, int timeout, byte[] ifaddr, int ttl) throws IOException;

    public synchronized InetAddress anyLocalAddress() {
        if (anyLocalAddress == null) {
            anyLocalAddress = new Inet4Address(); // {0x00,0x00,0x00,0x00}
            anyLocalAddress.holder().hostName = "0.0.0.0";
        }
        return anyLocalAddress;
    }

    public synchronized InetAddress loopbackAddress() {
        if (loopbackAddress == null) {
            byte[] loopback = {0x7f,0x00,0x00,0x01};
            loopbackAddress = new Inet4Address("localhost", loopback);
        }
        return loopbackAddress;
    }

  public boolean isReachable(InetAddress addr, int timeout, NetworkInterface netif, int ttl) throws IOException {
      byte[] ifaddr = null;
      if (netif != null) {
          /*
           * Let's make sure we use an address of the proper family
           */
          java.util.Enumeration<InetAddress> it = netif.getInetAddresses();
          InetAddress inetaddr = null;
          while (!(inetaddr instanceof Inet4Address) &&
                 it.hasMoreElements())
              inetaddr = it.nextElement();
          if (inetaddr instanceof Inet4Address)
              ifaddr = inetaddr.getAddress();
      }
      return isReachable0(addr.getAddress(), timeout, ifaddr, ttl);
  }
    private InetAddress      anyLocalAddress;
    private InetAddress      loopbackAddress;
}

可以看到這個類基本沒有實現什麼邏輯,主要實現都是通過JNI調用native方法了。native方法其實就是系統調用了。這裡就不展開了,邏輯不外乎就是查看/etc/resolv.conf下配置的nameserver和/etc/hosts下面的配置,然後使用DNS協議查詢。

其他NameService通過NameServiceDescriptor來創建相應。NameServiceDescriptor本身也是一個接口:

package sun.net.spi.nameservice;

public interface NameServiceDescriptor {
    /**
     * Create a new instance of the corresponding name service.
     */
    public NameService createNameService () throws Exception ;

    /**
     * Returns this service provider's name
     *
     */
    public String getProviderName();

    /**
     * Returns this name service type
     * "dns" "nis" etc
     */
    public String getType();
}

這裡我們先看看JDK自帶一個實現——DNSNameService。

package sun.net.spi.nameservice.dns;

/*
 * A name service provider based on JNDI-DNS.
 */
public final class DNSNameService implements NameService {

    // List of domains specified by property
    private LinkedList<String> domainList = null;

    // JNDI-DNS URL for name servers specified via property
    private String nameProviderUrl = null;

    // Per-thread soft cache of the last temporary context
    private static ThreadLocal<SoftReference<ThreadContext>> contextRef =
            new ThreadLocal<>();

    // Simple class to encapsulate the temporary context
    private static class ThreadContext {
        private DirContext dirCtxt;
        private List<String> nsList;

        ...
    }   

}

看一下構造函數:

public DNSNameService() throws Exception {

    // default domain
    String domain = AccessController.doPrivileged(
        new GetPropertyAction("sun.net.spi.nameservice.domain"));
    if (domain != null && domain.length() > 0) {
        domainList = new LinkedList<String>();
        domainList.add(domain);
    }

    // name servers
    String nameservers = AccessController.doPrivileged(
        new GetPropertyAction("sun.net.spi.nameservice.nameservers"));
    if (nameservers != null && nameservers.length() > 0) {
        nameProviderUrl = createProviderURL(nameservers);
        if (nameProviderUrl.length() == 0) {
            throw new RuntimeException("malformed nameservers property");
        }
    } else {
        // no property specified so check host DNS resolver configured
        // with at least one nameserver in dotted notation.
        //
        List<String> nsList = ResolverConfiguration.open().nameservers();
        if (nsList.isEmpty()) {
            throw new RuntimeException("no nameservers provided");
        }
        boolean found = false;
        for (String addr: nsList) {
            if (IPAddressUtil.isIPv4LiteralAddress(addr) ||
                IPAddressUtil.isIPv6LiteralAddress(addr)) {
                found = true;
                break;
            }
        }
        if (!found) {
            throw new RuntimeException("bad nameserver configuration");
        }
    }
}

可以看到它主要是解析下面兩個配置項,得到nameserver:

  • sun.net.spi.nameservice.domain=<domainname>
  • sun.net.spi.nameservice.nameservers=<server1_ipaddr,server2_ipaddr ...>

如果 sun.net.spi.nameservice.nameservers 沒有配置,那麼會使用 ResolverConfiguration 得到系統配置的nameserver:

List<String> nsList = ResolverConfiguration.open().nameservers();

ResolverConfiguration的實現邏輯是加載/etc/resolv.conf配置文件中的nameserver。具體參見:ResolverConfigurationImpl。這裡就不贅述。

得到NameServer,DNS的解析就很簡單了,對NameServer分別執行DNS查詢就可以了。具體代碼大家可以參見 DNSNameService。

NOTE 從代碼可以看出,系統配置的nameserver和通過SystemProperty配置的nameserver是或的關系,所以如果配置了sun.net.spi.nameservice.nameservers,那麼相當於繞過了系統配置的nameserver了。

前面說過,非默認的NameService是需要通過相應的NameServiceDescriptor傳教的,所以DNSNameService也需要有一個對應的NameServiceDescriptor,就是DNSNameServiceDescriptor:

package sun.net.spi.nameservice.dns;

import sun.net.spi.nameservice.*;

public final class DNSNameServiceDescriptor implements NameServiceDescriptor {
    /**
     * Create a new instance of the corresponding name service.
     */
    public NameService createNameService() throws Exception {
        return new DNSNameService();
    }

    /**
     * Returns this service provider's name
     *
     */
    public String getProviderName() {
        return "sun";
    }

    /**
     * Returns this name service type
     * "dns" "nis" etc
     */
    public String getType() {
        return "dns";
    }
}

可以看到DNSNameService的type為dns,name為sun。所以在配置的時候應該配置為dns,sun

但是NameServiceDescriptor本身是怎麼加載的呢?回到上面的代碼:

Iterator<NameServiceDescriptor> itr = ServiceLoader.load(NameServiceDescriptor.class).iterator();

原來是是通過java.util.ServiceLoader。這個類是專門用來加載service-provider的。這個類有非常詳細的JavaDoc。其中有一段說明怎麼告訴ServiceLoader加載自定義的ServiceProvider:

A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file's name is the fully-qualified binary name of the service's type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. Space and tab characters surrounding each name, as well as blank lines, are ignored. The comment character is '#' ('\u0023', NUMBER SIGN); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8.

Example Suppose we have a service type com.example.CodecSet which is intended to represent sets of encoder/decoder pairs for some protocol. In this case it is an abstract class with two abstract methods:

public abstract Encoder getEncoder(String encodingName);
public abstract Decoder getDecoder(String encodingName);

Each method returns an appropriate object or null if the provider does not support the given encoding. Typical providers support more than one encoding. If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named META-INF/services/com.example.CodecSet This file contains the single line:

com.example.impl.StandardCodecs    # Standard codecs

The CodecSet class creates and saves a single service instance at initialization:

private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);

To locate an encoder for a given encoding name it defines a static factory method which iterates through the known and available providers, returning only when it has located a suitable encoder or has run out of providers.

public static Encoder getEncoder(String encodingName) {
    for (CodecSet cp : codecSetLoader) {
        Encoder enc = cp.getEncoder(encodingName);
        if (enc != null)
           return enc;
    }
    return null;
}

A getDecoder method is defined similarly.

所以對於DNSNameServiceDescriptor,在sun/net/spi/nameservice/dns/META-INF/services目錄下有一個名稱為sun.net.spi.nameservice.NameServiceDescriptor的文件:

# dns service provider descriptor
sun.net.spi.nameservice.dns.DNSNameServiceDescriptor

這種機制後來廣泛用於Spring的自定義便簽(Creating a Custom Spring 3 XML Namespace),估計是受這個影響。

實際應用

知道這個有什麼用呢?比如說我們想讓我們的應用走谷歌的公共DNS服務器:8.8.8.8。但是我們又不想讓用戶去修改系統配置,這對用戶是個負擔,而且我們不希望影響到其他應用。怎麼處理呢?只需要在應用啟動之前簡單配置一下:

System.setProperty("sun.net.spi.nameservice.provider.1", "dns,sun");
System.setProperty("sun.net.spi.nameservice.nameservers", "8.8.8.8");
System.setProperty("sun.net.spi.nameservice.provider.2", "default");

再比如,如果你只想針對某些域名做特殊的解析,那麼你可以自定義一個NameServiceProvider,實現對應的NameServiceDescriptor,還有相應的META-INF說明。然後在應用啟動的時候配置一下:

System.setProperty("sun.net.spi.nameservice.provider.1", "dns,yourProviderName");
System.setProperty("sun.net.spi.nameservice.provider.2", "default");

參考文章

  1. DNS: Java Glossary 深入淺出的一篇介紹性文章,強烈推薦。
  2. Understanding host name resolution and DNS behavior in Java
  3. Networking Properties
  4. Local Managed DNS (Java)

Copyright © Linux教程網 All Rights Reserved