分类
Articles

JDK DNS解析策略

应开发内网命名服务的需求,需要调研Java是如何使用DNS的。以下文字是调研的一些结果,主要关注本地缓存、过期时间、多条A记录的选择策略以及如何自定义解析规则等方面。调研对象为JDK8。

应开发内网命名服务的需求,需要调研Java是如何使用DNS的。以下文字是调研的一些结果,主要关注本地缓存、过期时间、多条A记录的选择策略以及如何自定义解析规则等方面。调研对象为JDK8。

解析逻辑

在现有的网络体系中,域名必须被解析为IP地址,才能正确的传递数据。JDK中java.net.InetSocketAddress表示一个Socket地址,当我们需要开启端口或者创建连接的时候需要创建InetSocketAddress对象。而InetSocketAddress内部使用java.net.InetAddress类将域名解析为IP地址。

@InetSocketAddress
    public InetSocketAddress(String hostname, int port) {
        checkHost(hostname);
        InetAddress addr = null;
        String host = null;
        try {
            addr = InetAddress.getByName(hostname);
        } catch(UnknownHostException e) {
            host = hostname;
        }
        holder = new InetSocketAddressHolder(host, addr, checkPort(port));
    }

InetAddress.getByName(host)方法是域名解析的入口类,接下来深入InetAddress内部来追溯域名解析的逻辑。

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

首先getByName方法能得出两个结论,第一如果域名有多个A记录JDK会一次获得所有的A记录,第二多个A记录的选择策略是简单的总是返回第一个A记录。继续跟踪代码到getAllByName0方法,大概逻辑是首先检查本地缓存,本地不存在就检查DNS服务器:

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

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

检查缓存的部分比较简单,直接跳过,继续跟进检查DNS服务器部分,并最终跟进到Inet4AddressImpl类的lookupAllHostAddr方法,这是一个native方法。大概的逻辑就是首先检查本地hosts文件,然后向远端DNS服务器发起请求,并返回解析结果。

至此DNS的请求流程就完成了,接下来分析一下缓存策略。

缓存策略

InetAddress内部有两个缓存:”addressCache”缓存解析成功的结果;”negativeCache”缓存解析失败的结果。两个缓存都是InetAddress的内部类Cache的对象。关于缓存就必须提过期策略。Cache的过期策略有三种FOREVER\NEVER\TIMED,顾名思义是永远缓存、永不缓存和定时过期的意思。

接下来看JDK默认的过期策略与过期时间是怎样的,过期策略与过期时间的逻辑主要在InetAddressCachePolicy类中,该类中定义了一些静态代码块来给”addressCache”和”negativeCache”两个缓存设置过期策略。以下是”addressCache”缓存的过期策略设置规则:

//addressCache缓存的过期策略设置规则
@InetAddressCachePolicy

    cachePolicy = -1;
    if(var0 != null) {
        cachePolicy = var0.intValue();
        if(cachePolicy < 0) {
            cachePolicy = -1;
        }

        propertySet = true;
    } else if(System.getSecurityManager() == null) {
        cachePolicy = 30;
    }

var0是可以自定义的一些参数值,默认为空,下文会详细介绍。可以看到因为var0默认为空,所以过期策略取决于System.getSecurityManager(),关于Security Manager请参考。System.getSecurityManager()默认是空的,所以默认情况下”addressCache”的过期策略是30秒过期。”addressCache”存方的是DNS服务器的正确解析结果,所以JDK默认的DNS解析记录的过期策略是30秒过期。

接下来看如何自定义过期策略,主要的逻辑依然在InetAddressCachePolicy的静态代码快中。

//自定义addressCache缓存的过期策略逻辑
@InetAddressCachePolicy
    Integer var0 = (Integer)AccessController.doPrivileged(new PrivilegedAction<Integer>() {
        public Integer run() {
            String var1;
            try {
                var1 = Security.getProperty("networkaddress.cache.ttl");
                if(var1 != null) {
                    return Integer.valueOf(var1);
                }
            } catch (NumberFormatException var3) {
                ;
            }

            try {
                var1 = System.getProperty("sun.net.inetaddr.ttl");
                if(var1 != null) {
                    return Integer.decode(var1);
                }
            } catch (NumberFormatException var2) {
                ;
            }

            return null;
        }
    });

可以看到主要通过读取Security参数”networkaddress.cache.ttl”或者System参数”sun.net.inetaddr.ttl”的形式进行改变过期策略,其中”networkaddress.cache.ttl”的优先级要高。Security参数可以通过修改${JAVA_HOME}/jre/lib/security/java.security的方式生效,而System参数可以通过在java启动脚本上添加”-D”参数的形式生效。

//通过System参数设置过期策略
-Dsun.net.inetaddr.ttl=-1 //永不过期
-Dsun.net.inetaddr.ttl=0  //立即过期
-Dsun.net.inetaddr.ttl=10 //10秒过期

自定义解析规则

如果想改变首先解析本地hosts文件然后发送DNS请求的解析方式的话,可以通过自定义NameService的方式自定义解析规则。首先看看JDK创建NameService的流程:

  • 第一步:创建Inet4AddressImpl类,Inet4AddressImpl类会调用native方法进行默认解析,默认的NameService就是使用Inet4AddressImpl类进行域名解析。
  • 第二步:查看是否指定了NameService的实现类,通过查看System参数”sun.net.spi.nameservice.provider.1 2 3…”
  • 第三步:如果没有指定,则创建默认NameService。
//创建NameService的流程
@InetAddress

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

通过观察createNSProvider方法的代码,发现如果provider=“default”,则创建默认的NameService使用Inet4AddressImpl进行域名解析,否则使用JDK ServiceLoader机制,从classpath下加载sun.net.spi.nameservice.NameServiceDescriptor的实现类,并跟provider进行匹配,并最终创建NameService。

综上所述自定以解析规则的步骤大概是,首先通过System参数”sun.net.spi.nameservice.provider.1 2 3…”定义一系列NameServiceDescriptor的实现类;其次自定义sun.net.spi.nameservice.NameServiceDescriptor与sun.net.spi.nameservice.NameService实现解析逻辑;最后按照ServiceLoader的规范进行配置。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可, 转载时请注明原文链接。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注