Java DNS解析源码分析

在一次网络访问中,如果用域名,那肯定是需要进行dns解析的步骤。那如何在java中抓取这个解析的时间呢?

Java中有很多网络库可以选择,但在dns解析这一步,都会用到InetAddress.getAllByName()这个方法,通过返回一个InetAddress数组,保存了解析该域名对应的所有Ip地址。虽然简单一句话讲完了,但其中是需要经过论证。既然都是用这个方法,那能不能通过hook这个方法,来获取我们需要的时间呢?

源码分析

 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);
      }
  }

  #####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();
}

重要的两行,先从cache中取,如果没有,则从DNS服务器中去取。cache有两个:

 private static Cache addressCache = new Cache(Cache.Type.Positive);

private static Cache negativeCache = new Cache(Cache.Type.Negative);

negativeCache存的是错误的解析结果,这个设计很聪明。正确的结果存起来,错误的结果也会存起来,用来节省获取dns的时间。所以在第一次获取到dns解析结果,第二次直接是从缓存中获取。所以通过打点的方式,能获取到dns的解析时间,后面,则同一个域名的解析时间都是0 。所以通过简单的通过重新调用InetAddress.getAllByName的方式,计算这个方法的执行时间,并不能获取dns的解析时间。

真实代码中,第二次以后解析的时间都是为0的。这对于监控没有意义,我们需要的是,实时的获取第一次的解析时间。但代码中解析dns数据的代码,在第三方网络库,简单的打点可能工作量大,而且不易实现。

顺藤摸瓜:

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;
                  }
              }
          }

结果是从lookUpAllHostAddr来获取的。而这个nameService是从集合中遍历而来。所以着重来观察下nameServices集合。看下它的初始化和集合里存的内容。

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);
  }
}

初始化在这个静态代码块中,通过add一个createNSProvider(“default”)来获取的nameService。

 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);
          }
      };

所以最后的真相是通过全局变量impl来调用lookUpALlHostAddr()来获取的数据。那看下这个impl是怎么实现的就能明白了。

class InetAddressImplFactory {

static InetAddressImpl create() {
  return InetAddress.loadImpl(isIPv6Supported() ?
                              "Inet6AddressImpl" : "Inet4AddressImpl");
}

static native boolean isIPv6Supported();
}
static InetAddressImpl loadImpl(String implName) {
  Object impl = null;

  /*
    * Property "impl.prefix" will be prepended to the classname
    * of the implementation object we instantiate, to which we
    * delegate the real work (like native methods). This
    * property can vary across implementations of the java.
    * classes. The default is an empty String "".
    */
  String prefix = AccessController.doPrivileged(
                new GetPropertyAction("impl.prefix", ""));
  try {
      impl = Class.forName("java.net." + prefix + implName).newInstance();
  } catch (ClassNotFoundException e) {
      System.err.println("Class not found: java.net." + prefix +
                          implName + ":\ncheck impl.prefix property " +
                          "in your properties file.");
  } catch (InstantiationException e) {
      System.err.println("Could not instantiate: java.net." + prefix +
                          implName + ":\ncheck impl.prefix property " +
                          "in your properties file.");
  } catch (IllegalAccessException e) {
      System.err.println("Cannot access class: java.net." + prefix +
                          implName + ":\ncheck impl.prefix property " +
                          "in your properties file.");
  }

  if (impl == null) {
      try {
          impl = Class.forName(implName).newInstance();
      } catch (Exception e) {
          throw new Error("System property impl.prefix incorrect");
      }
  }

  return (InetAddressImpl) impl;
}

impl是通过ImplFactory来创建,而真正的创建是通过反射来创建的impl对象,是一个Inet6AddressImpl或者Inet4AddressImpl. 在看这两个实现:(在java.net包下)

class Inet6AddressImpl 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 scope, int timeout, byte[] inf, int ttl, int if_scope) throws IOException;

重要的方法都是native,在c层实现。到这里,咱们的分析也就结束了。 归根节点,所有的dns解析都是都会调用Inet6AddressImpl或者Inet4AddressImpl的lookupAllhostAddr实现的。而这些类都是Java系统内部受保护的类,但可以利用反射来实现!

既然条理理清楚了,实现的方式还是很多。这里我的一个思路是:

实现

我自己自定义一个NameSerive,插入到nameSerives集合的第一个位置中,那所有的dns解析都会走我自定义的解析方法中,这样便hook住了所有域名的dns的解析。真实的解析,就调用Inet6AddressImpl或者Inet4AddressImpl的实现过程。贴代码:

class MyInetAddress implements NameService {

MyInetAddress() {
}

private boolean isInstalled = false;

private Object inetAddressImpl;
private Class inetAddressImplClass;

boolean installDnsMonitor() {
  try {
      InetAddress.getByName("127.0.0.1");
      Class<InetAddress> klass = InetAddress.class;
      Field acf = klass.getDeclaredField("nameServices");
      acf.setAccessible(true);
      Object addressCache = acf.get(null);
      ArrayList<NameService> list = (ArrayList<NameService>) addressCache;
      list.add(0, this);

      if (isSupportIpv6()) {
          inetAddressImplClass = Class.forName("java.net.Inet6AddressImpl");
      } else {
          inetAddressImplClass = Class.forName("java.net.Inet4AddressImpl");
      }
      Constructor<?> constructor = inetAddressImplClass.getDeclaredConstructor();
      constructor.setAccessible(true);
      inetAddressImpl= constructor.newInstance();

  } catch (Exception e) {
      e.printStackTrace();
      return isInstalled;
  }

  return isInstalled = true;

}

private boolean isSupportIpv6() {
  try {
      Class klass = Class.forName("java.net.InetAddressImplFactory");
      Method method = klass.getDeclaredMethod("isIPv6Supported");
      method.setAccessible(true);
      Boolean boo = (Boolean) method.invoke(null);

      return boo;
  } catch (Exception e) {
      e.printStackTrace();
  }
  return true;
}

@Override
public InetAddress[] lookupAllHostAddr(String s) throws UnknownHostException {
  return getInetAddress(s);
}

@Override
public String getHostByAddr(byte[] bytes) throws UnknownHostException {
  return getHostByAddr0(bytes);
}

private InetAddress[] getInetAddress(String host) throws UnknownHostException {

  try {
      Method mm = inetAddressImplClass.getMethod("lookupAllHostAddr", String.class);
      mm.setAccessible(true);

      long startTime=System.currentTimeMillis();

      InetAddress[] addresses = (InetAddress[]) mm.invoke(inetAddressImpl, host);

      long endTime=System.currentTimeMillis();
       
      //耗时:::endTime-startTime
       
      return addresses;
  } catch (NoSuchMethodException e) {
      e.printStackTrace();
  } catch (IllegalAccessException e) {
      e.printStackTrace();
  } catch (InvocationTargetException e) {
      e.printStackTrace();
  }
  return new InetAddress[0];
}

private String getHostByAddr0(byte[] bytes) {
  try {

      Method mm = inetAddressImplClass.getMethod("getHostByAddr", byte[].class);
      mm.setAccessible(true);
      return (String) mm.invoke(inetAddressImpl, bytes);
  } catch (NoSuchMethodException e) {
      e.printStackTrace();
  } catch (IllegalAccessException e) {
      e.printStackTrace();
  } catch (InvocationTargetException e) {
      e.printStackTrace();
  }

  return "";
}
}

最终:调用installDNSMonitor之后,就成功hook了所有的dns解析,即所有的调用系统InetAddress.getAllByName方法的代码,轻松获取解析时间!

PS::简单的清除InetAddress中cache的缓存,第二次的时间是不是就不是0了??有兴趣的可以自己验证。

总结:

阅读和思考源码是很好的习惯,能提高自己和学习源码的精妙之处。不积跬步,无以至千里!很多问题,也只有从源码中找到答案……