Vertx DNS解析源码

最近在生产环境碰到过多次域名解析失败的问题,有时候还是客户windowns环境报障,是时候深入了解下Vertx内部的域名解析机制了。

1、Vertx使用DNS方法
import java.util.Arrays;

import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;

public class DemoMain {
public static void main(String[] args) throws Exception {
AddressResolverOptions addressResolverOptions = new AddressResolverOptions();
addressResolverOptions.setNdots(1);
addressResolverOptions.setServers(Arrays.asList("8.8.8.8"));
addressResolverOptions.setSearchDomains(Arrays.asList(".com"));

VertxOptions vertxOptions = new VertxOptions();
vertxOptions.setAddressResolverOptions(addressResolverOptions);
Vertx vertx = Vertx.vertx(vertxOptions);
HttpClientOptions clientOp = new HttpClientOptions();
clientOp.setSsl(true);
clientOp.setTrustAll(true);
clientOp.setVerifyHost(false);
HttpClient httpClient = vertx.createHttpClient(clientOp);
HttpClientRequest req = httpClient.get(443, "www.baidu.com", "/index.html", resp -> {
  System.out.println(resp.statusCode());
  vertx.close();
});
req.end();

}
}
AddressResolverOptions 有几个重要属性
servers: 8.8.8.8,8.8.4.4 #对应Linux /etc/resolv.conf的nameserver,DNS服务器地址,支持配置多个,以逗号隔开
ndots: 1 #对应linux /etc/resolv.conf里面的options: ndots, 作用就是如果给的域名里面包含的点的个数少于该阈值,那么DNS解析的时候就会默认加上searchDomains的值,这个必须和searchDomains搭配使用,Linux默认为1,华为公有云PAAS(包含容器)默认是4
searchDomains: a,b,c #对应linux /etc/resolv.conf里面的search,和ndots搭配使用,如果当前域名的点个数少于设置值,解析时就会把这些值添加到域名后面一起解析,比如ndots设置的为4,当前域名为servicecomb.cn-north-1.myhwclouds.com,只有三个点,那么解析的时候就会自动加上servicecomb.cn-north-1.myhwclouds.com.a去解析,没解析出来在用servicecomb.cn-north-1.myhwclouds.com.b,直到能最后解析出来
optResourceEnabled: true #optional record is automatically included in DNS queries
cacheMinTimeToLive: 0 #最小缓存时间
cacheMaxTimeToLive: 10000 #最大缓存时间
cacheNegativeTimeToLive: 0 #DNS解析失败后,下次重试的等待时间
queryTimeout: 5000 #查询超时时间
maxQueries: 4 #查询次数
rdFlag: true #设置DNS递归查询
rotateServers: true #设置是否支持轮询,如果有多个域名服务器,轮训可以加快域名解析速度

2、代码解析过程
使用Vertx vertx = Vertx.vertx(vertxOptions),在VertxImpl构造方法里面会初始化this.addressResolver = new AddressResolver(this, options.getAddressResolverOptions());
2.1 io.vertx.core.impl.AddressResolver里面有个static块,会读取/etc/resolv.conf文件,解析得到ndots和rotate默认值,在构造方法内初始化
public AddressResolver(Vertx vertx, AddressResolverOptions options) {
this.provider = ResolverProvider.factory(vertx, options);
this.resolverGroup = provider.resolver(options);
this.vertx = vertx;
}

ResolverProvider获取provider的时候,默认得到DnsResolverProvider,这个是Vertx提供的默认域名解析。

2.2 DnsResolverProvider代码解析

public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) {
List<String> dnsServers = options.getServers();
List<InetSocketAddress> serverList = new ArrayList<>();
// 看是否有配置dns服务地址,如果配置了则使用配置的地址
if (dnsServers != null && dnsServers.size() > 0) {
for (String dnsServer : dnsServers) {
// 解析域名地址和端口,默认是53
int sep = dnsServer.indexOf(':');
String ipAddress;
int port;
if (sep != -1) {
ipAddress = dnsServer.substring(0, sep);
port = Integer.parseInt(dnsServer.substring(sep + 1));
} else {
ipAddress = dnsServer;
port = 53;
}
try {
// 检查地址是否正确
serverList.add(new InetSocketAddress(InetAddress.getByAddress(NetUtil.createByteArrayFromIpAddressString(ipAddress)), port));
} catch (UnknownHostException e) {
throw new VertxException(e);
}
}
} else {
// 如果没有配置域名地址,则需要读取服务器上的地址
// 这里直接使用了Netty提供的获取服务器上默认域名地址,这里最终会调用DefaultDnsServerAddressStreamProvider,详细参考2.3
DnsServerAddressStream stream = DnsServerAddresses.defaultAddresses().stream();
Set<InetSocketAddress> all = new HashSet<>();
while (true) {
InetSocketAddress address = stream.next();
if (all.contains(address)) {
break;
}
serverList.add(address);
all.add(address);
}
}
DnsServerAddresses nameServerAddresses = options.isRotateServers() ? DnsServerAddresses.rotational(serverList) : DnsServerAddresses.sequential(serverList);
DnsServerAddressStreamProvider nameServerAddressProvider = hostname -> nameServerAddresses.stream();

// 解析host文件,不需要通过域名服务器解析,比如/etc/hosts,支持配置文件路径和直接设置。如果都为空,则读取系统的默认配置,windows读取C:\Windows\System32\drivers\etc\hosts,Linux读取/etc/hosts
HostsFileEntries entries;
if (options.getHostsPath() != null) {
File file = vertx.resolveFile(options.getHostsPath()).getAbsoluteFile();
try {
if (!file.exists() || !file.isFile()) {
throw new IOException();
}
entries = HostsFileParser.parse(file);
} catch (IOException e) {
throw new VertxException("Cannot read hosts file " + file.getAbsolutePath());
}
} else if (options.getHostsValue() != null) {
try {
entries = HostsFileParser.parse(new StringReader(options.getHostsValue().toString()));
} catch (IOException e) {
throw new VertxException("Cannot read hosts config ", e);
}
} else {
entries = HostsFileParser.parseSilently();
}

int minTtl = intValue(options.getCacheMinTimeToLive(), 0);
int maxTtl = intValue(options.getCacheMaxTimeToLive(), Integer.MAX_VALUE);
int negativeTtl = intValue(options.getCacheNegativeTimeToLive(), 0);
DnsCache resolveCache = new DefaultDnsCache(minTtl, maxTtl, negativeTtl);
DnsCache authoritativeDnsServerCache = new DefaultDnsCache(minTtl, maxTtl, negativeTtl);

this.vertx = vertx;
// 初始化AddressResolverGroup
this.resolverGroup = new AddressResolverGroup<InetSocketAddress>() {
  @Override
  protected io.netty.resolver.AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
    ChannelFactory<DatagramChannel> channelFactory = () -> vertx.transport().datagramChannel();
    DnsAddressResolverGroup group = new DnsAddressResolverGroup(channelFactory, nameServerAddressProvider) {
      @Override
      protected NameResolver<InetAddress> newNameResolver(EventLoop eventLoop, ChannelFactory<? extends DatagramChannel> channelFactory, DnsServerAddressStreamProvider nameServerProvider) throws Exception {
        DnsNameResolverBuilder builder = new DnsNameResolverBuilder((EventLoop) executor);
        builder.hostsFileEntriesResolver(new HostsFileEntriesResolver() {
          @Override
          public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
            InetAddress address = lookup(inetHost, resolvedAddressTypes);
            if (address == null) {
              address = lookup(inetHost.toLowerCase(Locale.ENGLISH), resolvedAddressTypes);
            }
            return address;
          }
          InetAddress lookup(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
            switch (resolvedAddressTypes) {
              case IPV4_ONLY:
                return entries.inet4Entries().get(inetHost);
              case IPV6_ONLY:
                return entries.inet6Entries().get(inetHost);
              case IPV4_PREFERRED:
                Inet4Address inet4Address = entries.inet4Entries().get(inetHost);
                return inet4Address != null? inet4Address : entries.inet6Entries().get(inetHost);
              case IPV6_PREFERRED:
                Inet6Address inet6Address = entries.inet6Entries().get(inetHost);
                return inet6Address != null? inet6Address : entries.inet4Entries().get(inetHost);
              default:
                throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes);
            }
          }
        });
        builder.channelFactory(channelFactory);
        builder.nameServerProvider(nameServerAddressProvider);
        builder.optResourceEnabled(options.isOptResourceEnabled());
        builder.resolveCache(resolveCache);
        builder.authoritativeDnsServerCache(authoritativeDnsServerCache);
        builder.queryTimeoutMillis(options.getQueryTimeout());
        builder.maxQueriesPerResolve(options.getMaxQueries());
        builder.recursionDesired(options.getRdFlag());
        // 必须配置了searchDomains,ndots才起作用
        if (options.getSearchDomains() != null) {
          builder.searchDomains(options.getSearchDomains());
          int ndots = options.getNdots();
          if (ndots == -1) {
            ndots = AddressResolver.DEFAULT_NDOTS_RESOLV_OPTION;
          }
          builder.ndots(ndots);
        }
        return builder.build();
      }
    };

    io.netty.resolver.AddressResolver<InetSocketAddress> resolver = group.getResolver(executor);
    resolvers.add(new ResolverRegistration(resolver, (EventLoop) executor));

    return resolver;
  }
};

}

2.3 DefaultDnsServerAddressStreamProvider 读取系统默认域名解析地址,使用static块,利用jndi-DNS获取域名地址列表

static {
final List<InetSocketAddress> defaultNameServers = new ArrayList<InetSocketAddress>(2);

    // Using jndi-dns to obtain the default name servers.
    //
    // See:
    // - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html
    // - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html
    Hashtable<String, String> env = new Hashtable<String, String>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
    env.put("java.naming.provider.url", "dns://");
    try {
        DirContext ctx = new InitialDirContext(env);
        // 获取dnsUrl地址
        String dnsUrls = (String) ctx.getEnvironment().get("java.naming.provider.url");
        // Only try if not empty as otherwise we will produce an exception
        if (dnsUrls != null && !dnsUrls.isEmpty()) {
            String[] servers = dnsUrls.split(" ");
            for (String server : servers) {
                try {
                    URI uri = new URI(server);
                    String host = new URI(server).getHost();

                    if (host == null || host.isEmpty()) {
                        logger.debug(
                                "Skipping a nameserver URI as host portion could not be extracted: {}", server);
                        // If the host portion can not be parsed we should just skip this entry.
                        continue;
                    }
                    int port  = uri.getPort();
                    defaultNameServers.add(SocketUtils.socketAddress(uri.getHost(), port == -1 ? DNS_PORT : port));
                } catch (URISyntaxException e) {
                    logger.debug("Skipping a malformed nameserver URI: {}", server, e);
                }
            }
        }
    } catch (NamingException ignore) {
        // Will try reflection if this fails.
    }

    if (defaultNameServers.isEmpty()) {
        try {
            Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration");
            Method open = configClass.getMethod("open");
            Method nameservers = configClass.getMethod("nameservers");
            Object instance = open.invoke(null);

            @SuppressWarnings("unchecked")
            final List<String> list = (List<String>) nameservers.invoke(instance);
            for (String a: list) {
                if (a != null) {
                    defaultNameServers.add(new InetSocketAddress(SocketUtils.addressByName(a), DNS_PORT));
                }
            }
        } catch (Exception ignore) {
            // Failed to get the system name server list via reflection.
            // Will add the default name servers afterwards.
        }
    }

    if (!defaultNameServers.isEmpty()) {
        if (logger.isDebugEnabled()) {
            logger.debug(
                    "Default DNS servers: {} (sun.net.dns.ResolverConfiguration)", defaultNameServers);
        }
    } else {
        // Depending if IPv6 or IPv4 is used choose the correct DNS servers provided by google:
        // https://developers.google.com/speed/public-dns/docs/using
        // https://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html
        if (NetUtil.isIpV6AddressesPreferred() ||
                (NetUtil.LOCALHOST instanceof Inet6Address && !NetUtil.isIpV4StackPreferred())) {
            Collections.addAll(
                    defaultNameServers,
                    SocketUtils.socketAddress("2001:4860:4860::8888", DNS_PORT),
                    SocketUtils.socketAddress("2001:4860:4860::8844", DNS_PORT));
        } else {
            Collections.addAll(
                    defaultNameServers,
                    SocketUtils.socketAddress("8.8.8.8", DNS_PORT),
                    SocketUtils.socketAddress("8.8.4.4", DNS_PORT));
        }

        if (logger.isWarnEnabled()) {
            logger.warn(
                    "Default DNS servers: {} (Google Public DNS as a fallback)", defaultNameServers);
        }
    }

    DEFAULT_NAME_SERVER_LIST = Collections.unmodifiableList(defaultNameServers);
    DEFAULT_NAME_SERVER_ARRAY = defaultNameServers.toArray(new InetSocketAddress[defaultNameServers.size()]);
    DEFAULT_NAME_SERVERS = sequential(DEFAULT_NAME_SERVER_ARRAY);
}

3、潜在的问题
3.1 jndi-DNS获取的域名解析机制,底层使用native code方法,具体实现不详。猜测Linux是直接获取/etc/resolv.conf里面配置的地址,windows获取网卡配置的域名地址。在windows下面,如果有多个网卡,并且有网卡是不通外网的。

3.2 DNS默认的searchDomain没有配置,也没有获取系统的配置,单独设置ndots是不生效的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,569评论 18 139
  • 1. 概述 在网络环境中一般用户只需要在浏览器中输入url如www.sunny.com就可以到对应服务器获取相应的...
    ghbsunny阅读 2,855评论 0 7
  • dnsmasq是什么我就不说了,请自行百度。 目前我需要使用的用途是:1.dhcp(分配一个或者多个内网ip地址)...
    dnaEMx阅读 16,035评论 0 6
  • 在使用consul做docker容器服务化的过程中,使用到了dnsmasq做DNS请求转发,于是研究了下DNS协议...
    __七把刀__阅读 3,970评论 2 13
  • 世界,没有我小时候想象的那么简单,却远比我现在以为的更复杂。 小时候,我以为非对即错,绝不能将黑白混为一谈。你骂我...
    水瓶淡泊阅读 228评论 0 1