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了??有兴趣的可以自己验证。
总结:
阅读和思考源码是很好的习惯,能提高自己和学习源码的精妙之处。不积跬步,无以至千里!很多问题,也只有从源码中找到答案……