diff --git a/all/pom.xml b/all/pom.xml
index 40861eb070..2fa83cdc45 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -189,6 +189,20 @@
compiletrue
+
+ ${project.groupId}
+ netty-resolver
+ ${project.version}
+ compile
+ true
+
+
+ ${project.groupId}
+ netty-resolver-dns
+ ${project.version}
+ compile
+ true
+ ${project.groupId}netty-transport
diff --git a/pom.xml b/pom.xml
index 7407184a4b..7dbc088704 100644
--- a/pom.xml
+++ b/pom.xml
@@ -341,6 +341,8 @@
codec-mqttcodec-sockscodec-stomp
+ resolver
+ resolver-dnstransporttransport-rxtxtransport-sctp
diff --git a/resolver-dns/pom.xml b/resolver-dns/pom.xml
new file mode 100644
index 0000000000..486a9260f7
--- /dev/null
+++ b/resolver-dns/pom.xml
@@ -0,0 +1,49 @@
+
+
+
+
+ 4.0.0
+
+ io.netty
+ netty-parent
+ 4.1.0.Beta4-SNAPSHOT
+
+
+ netty-resolver-dns
+ jar
+
+ Netty/Resolver/DNS
+
+
+
+ ${project.groupId}
+ netty-resolver
+ ${project.version}
+
+
+ ${project.groupId}
+ netty-codec-dns
+ ${project.version}
+
+
+ ${project.groupId}
+ netty-transport
+ ${project.version}
+
+
+
+
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
new file mode 100644
index 0000000000..42902164cf
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
@@ -0,0 +1,864 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.resolver.dns;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFactory;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoop;
+import io.netty.channel.FixedRecvByteBufAllocator;
+import io.netty.channel.ReflectiveChannelFactory;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.handler.codec.dns.DnsClass;
+import io.netty.handler.codec.dns.DnsQueryEncoder;
+import io.netty.handler.codec.dns.DnsQuestion;
+import io.netty.handler.codec.dns.DnsResource;
+import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.handler.codec.dns.DnsResponseCode;
+import io.netty.handler.codec.dns.DnsResponseDecoder;
+import io.netty.resolver.NameResolver;
+import io.netty.resolver.SimpleNameResolver;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.collection.IntObjectHashMap;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.Promise;
+import io.netty.util.concurrent.ScheduledFuture;
+import io.netty.util.internal.OneTimeTask;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.net.IDN;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+
+/**
+ * A DNS-based {@link NameResolver}.
+ */
+public class DnsNameResolver extends SimpleNameResolver {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class);
+
+ static final InetSocketAddress ANY_LOCAL_ADDR = new InetSocketAddress(0);
+
+ private static final InternetProtocolFamily[] DEFAULT_RESOLVE_ADDRESS_TYPES = new InternetProtocolFamily[2];
+
+ static {
+ // Note that we did not use SystemPropertyUtil.getBoolean() here to emulate the behavior of JDK.
+ if ("true".equalsIgnoreCase(SystemPropertyUtil.get("java.net.preferIPv6Addresses"))) {
+ DEFAULT_RESOLVE_ADDRESS_TYPES[0] = InternetProtocolFamily.IPv6;
+ DEFAULT_RESOLVE_ADDRESS_TYPES[1] = InternetProtocolFamily.IPv4;
+ logger.debug("-Djava.net.preferIPv6Addresses: true");
+ } else {
+ DEFAULT_RESOLVE_ADDRESS_TYPES[0] = InternetProtocolFamily.IPv4;
+ DEFAULT_RESOLVE_ADDRESS_TYPES[1] = InternetProtocolFamily.IPv6;
+ logger.debug("-Djava.net.preferIPv6Addresses: false");
+ }
+ }
+
+ private static final DnsResponseDecoder DECODER = new DnsResponseDecoder();
+ private static final DnsQueryEncoder ENCODER = new DnsQueryEncoder();
+
+ final Iterable nameServerAddresses;
+ final DatagramChannel ch;
+
+ /**
+ * An array whose index is the ID of a DNS query and whose value is the promise of the corresponsing response. We
+ * don't use {@link IntObjectHashMap} or map-like data structure here because 64k elements are fairly small, which
+ * is only about 512KB.
+ */
+ final AtomicReferenceArray promises = new AtomicReferenceArray(65536);
+
+ /**
+ * The cache for {@link #query(DnsQuestion)}
+ */
+ final ConcurrentMap queryCache = PlatformDependent.newConcurrentHashMap();
+
+ private final DnsResponseHandler responseHandler = new DnsResponseHandler();
+
+ private volatile long queryTimeoutMillis = 5000;
+
+ // The default TTL values here respect the TTL returned by the DNS server and do not cache the negative response.
+ private volatile int minTtl;
+ private volatile int maxTtl = Integer.MAX_VALUE;
+ private volatile int negativeTtl;
+ private volatile int maxTriesPerQuery = 2;
+
+ private volatile InternetProtocolFamily[] resolveAddressTypes = DEFAULT_RESOLVE_ADDRESS_TYPES;
+ private volatile boolean recursionDesired = true;
+ private volatile int maxQueriesPerResolve = 8;
+
+ private volatile int maxPayloadSize;
+ private volatile DnsClass maxPayloadSizeClass; // EDNS uses the CLASS field as the payload size field.
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with a single DNS server.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelType the type of the {@link DatagramChannel} to create
+ * @param nameServerAddress the address of the DNS server
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, Class extends DatagramChannel> channelType,
+ InetSocketAddress nameServerAddress) {
+ this(eventLoop, channelType, ANY_LOCAL_ADDR, nameServerAddress);
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with a single DNS server.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelType the type of the {@link DatagramChannel} to create
+ * @param localAddress the local address of the {@link DatagramChannel}
+ * @param nameServerAddress the address of the DNS server
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, Class extends DatagramChannel> channelType,
+ InetSocketAddress localAddress, InetSocketAddress nameServerAddress) {
+ this(eventLoop, new ReflectiveChannelFactory(channelType), localAddress, nameServerAddress);
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with a single DNS server.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel}
+ * @param nameServerAddress the address of the DNS server
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, ChannelFactory extends DatagramChannel> channelFactory,
+ InetSocketAddress nameServerAddress) {
+ this(eventLoop, channelFactory, ANY_LOCAL_ADDR, nameServerAddress);
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with a single DNS server.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel}
+ * @param localAddress the local address of the {@link DatagramChannel}
+ * @param nameServerAddress the address of the DNS server
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, ChannelFactory extends DatagramChannel> channelFactory,
+ InetSocketAddress localAddress, InetSocketAddress nameServerAddress) {
+ this(eventLoop, channelFactory, localAddress, DnsServerAddresses.singleton(nameServerAddress));
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelType the type of the {@link DatagramChannel} to create
+ * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new {@link Iterator} is
+ * created from this {@link Iterable} to determine which DNS server should be contacted
+ * for the next retry in case of failure.
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, Class extends DatagramChannel> channelType,
+ Iterable nameServerAddresses) {
+ this(eventLoop, channelType, ANY_LOCAL_ADDR, nameServerAddresses);
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelType the type of the {@link DatagramChannel} to create
+ * @param localAddress the local address of the {@link DatagramChannel}
+ * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new {@link Iterator} is
+ * created from this {@link Iterable} to determine which DNS server should be contacted
+ * for the next retry in case of failure.
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, Class extends DatagramChannel> channelType,
+ InetSocketAddress localAddress, Iterable nameServerAddresses) {
+ this(eventLoop, new ReflectiveChannelFactory(channelType), localAddress, nameServerAddresses);
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel}
+ * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new {@link Iterator} is
+ * created from this {@link Iterable} to determine which DNS server should be contacted
+ * for the next retry in case of failure.
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, ChannelFactory extends DatagramChannel> channelFactory,
+ Iterable nameServerAddresses) {
+ this(eventLoop, channelFactory, ANY_LOCAL_ADDR, nameServerAddresses);
+ }
+
+ /**
+ * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
+ *
+ * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers
+ * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel}
+ * @param localAddress the local address of the {@link DatagramChannel}
+ * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new {@link Iterator} is
+ * created from this {@link Iterable} to determine which DNS server should be contacted
+ * for the next retry in case of failure.
+ */
+ public DnsNameResolver(
+ EventLoop eventLoop, ChannelFactory extends DatagramChannel> channelFactory,
+ InetSocketAddress localAddress, Iterable nameServerAddresses) {
+
+ super(eventLoop);
+
+ if (channelFactory == null) {
+ throw new NullPointerException("channelFactory");
+ }
+ if (nameServerAddresses == null) {
+ throw new NullPointerException("nameServerAddresses");
+ }
+ if (!nameServerAddresses.iterator().hasNext()) {
+ throw new NullPointerException("nameServerAddresses is empty");
+ }
+ if (localAddress == null) {
+ throw new NullPointerException("localAddress");
+ }
+
+ this.nameServerAddresses = nameServerAddresses;
+ ch = newChannel(channelFactory, localAddress);
+
+ setMaxPayloadSize(4096);
+ }
+
+ private DatagramChannel newChannel(
+ ChannelFactory extends DatagramChannel> channelFactory, InetSocketAddress localAddress) {
+
+ Bootstrap b = new Bootstrap();
+ b.group(executor());
+ b.channelFactory(channelFactory);
+ b.handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(DatagramChannel ch) throws Exception {
+ ch.pipeline().addLast(DECODER, ENCODER, responseHandler);
+ }
+ });
+
+ DatagramChannel ch = (DatagramChannel) b.bind(localAddress).channel();
+ ch.closeFuture().addListener(new ChannelFutureListener() {
+ @Override
+ public void operationComplete(ChannelFuture future) throws Exception {
+ clearCache();
+ }
+ });
+
+ return ch;
+ }
+
+ /**
+ * Returns the minimum TTL of the cached DNS resource records (in seconds).
+ *
+ * @see #maxTtl()
+ * @see #setTtl(int, int)
+ */
+ public int minTtl() {
+ return minTtl;
+ }
+
+ /**
+ * Returns the maximum TTL of the cached DNS resource records (in seconds).
+ *
+ * @see #minTtl()
+ * @see #setTtl(int, int)
+ */
+ public int maxTtl() {
+ return maxTtl;
+ }
+
+ /**
+ * Sets the minimum and maximum TTL of the cached DNS resource records (in seconds). If the TTL of the DNS resource
+ * record returned by the DNS server is less than the minimum TTL or greater than the maximum TTL, this resolver
+ * will ignore the TTL from the DNS server and use the minimum TTL or the maximum TTL instead respectively.
+ * The default value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to respect
+ * the TTL from the DNS server.
+ *
+ * @return {@code this}
+ *
+ * @see #minTtl()
+ * @see #maxTtl()
+ */
+ public DnsNameResolver setTtl(int minTtl, int maxTtl) {
+ if (minTtl < 0) {
+ throw new IllegalArgumentException("minTtl: " + minTtl + " (expected: >= 0)");
+ }
+ if (maxTtl < 0) {
+ throw new IllegalArgumentException("maxTtl: " + maxTtl + " (expected: >= 0)");
+ }
+ if (minTtl > maxTtl) {
+ throw new IllegalArgumentException(
+ "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)");
+ }
+
+ this.maxTtl = maxTtl;
+ this.minTtl = minTtl;
+
+ return this;
+ }
+
+ /**
+ * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which
+ * disables the cache for negative results.
+ *
+ * @see #setNegativeTtl(int)
+ */
+ public int negativeTtl() {
+ return negativeTtl;
+ }
+
+ /**
+ * Sets the TTL of the cache for the failed DNS queries (in seconds).
+ *
+ * @return {@code this}
+ *
+ * @see #negativeTtl()
+ */
+ public DnsNameResolver setNegativeTtl(int negativeTtl) {
+ if (negativeTtl < 0) {
+ throw new IllegalArgumentException("negativeTtl: " + negativeTtl + " (expected: >= 0)");
+ }
+
+ this.negativeTtl = negativeTtl;
+
+ return this;
+ }
+
+ /**
+ * Returns the timeout of each DNS query performed by this resolver (in milliseconds).
+ * The default value is 5 seconds.
+ *
+ * @see #setQueryTimeoutMillis(long)
+ */
+ public long queryTimeoutMillis() {
+ return queryTimeoutMillis;
+ }
+
+ /**
+ * Sets the timeout of each DNS query performed by this resolver (in milliseconds).
+ *
+ * @return {@code this}
+ *
+ * @see #queryTimeoutMillis()
+ */
+ public DnsNameResolver setQueryTimeoutMillis(long queryTimeoutMillis) {
+ if (queryTimeoutMillis < 0) {
+ throw new IllegalArgumentException("queryTimeoutMillis: " + queryTimeoutMillis + " (expected: >= 0)");
+ }
+
+ this.queryTimeoutMillis = queryTimeoutMillis;
+
+ return this;
+ }
+
+ /**
+ * Returns the maximum number of tries for each query. The default value is 2 times.
+ *
+ * @see #setMaxTriesPerQuery(int)
+ */
+ public int maxTriesPerQuery() {
+ return maxTriesPerQuery;
+ }
+
+ /**
+ * Sets the maximum number of tries for each query.
+ *
+ * @return {@code this}
+ *
+ * @see #maxTriesPerQuery()
+ */
+ public DnsNameResolver setMaxTriesPerQuery(int maxTriesPerQuery) {
+ if (maxTriesPerQuery < 1) {
+ throw new IllegalArgumentException("maxTries: " + maxTriesPerQuery + " (expected: > 0)");
+ }
+
+ this.maxTriesPerQuery = maxTriesPerQuery;
+
+ return this;
+ }
+
+ /**
+ * Returns the list of the protocol families of the address resolved by {@link #resolve(SocketAddress)}
+ * in the order of preference.
+ * The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}.
+ *
+ * @see #setResolveAddressTypes(InternetProtocolFamily...)
+ */
+ public List resolveAddressTypes() {
+ return Arrays.asList(resolveAddressTypes);
+ }
+
+ InternetProtocolFamily[] resolveAddressTypesUnsafe() {
+ return resolveAddressTypes;
+ }
+
+ /**
+ * Sets the list of the protocol families of the address resolved by {@link #resolve(SocketAddress)}.
+ * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in the
+ * order of preference. To enforce the resolve to retrieve the address of a specific protocol family, specify
+ * only a single {@link InternetProtocolFamily}.
+ *
+ * @return {@code this}
+ *
+ * @see #resolveAddressTypes()
+ */
+ public DnsNameResolver setResolveAddressTypes(InternetProtocolFamily... resolveAddressTypes) {
+ if (resolveAddressTypes == null) {
+ throw new NullPointerException("resolveAddressTypes");
+ }
+
+ final List list =
+ new ArrayList(InternetProtocolFamily.values().length);
+
+ for (InternetProtocolFamily f: resolveAddressTypes) {
+ if (f == null) {
+ break;
+ }
+
+ // Avoid duplicate entries.
+ if (list.contains(f)) {
+ continue;
+ }
+
+ list.add(f);
+ }
+
+ if (list.isEmpty()) {
+ throw new IllegalArgumentException("no protocol family specified");
+ }
+
+ this.resolveAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]);
+
+ return this;
+ }
+
+ /**
+ * Sets the list of the protocol families of the address resolved by {@link #resolve(SocketAddress)}.
+ * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in the
+ * order of preference. To enforce the resolve to retrieve the address of a specific protocol family, specify
+ * only a single {@link InternetProtocolFamily}.
+ *
+ * @return {@code this}
+ *
+ * @see #resolveAddressTypes()
+ */
+ public DnsNameResolver setResolveAddressTypes(Iterable resolveAddressTypes) {
+ if (resolveAddressTypes == null) {
+ throw new NullPointerException("resolveAddressTypes");
+ }
+
+ final List list =
+ new ArrayList(InternetProtocolFamily.values().length);
+
+ for (InternetProtocolFamily f: resolveAddressTypes) {
+ if (f == null) {
+ break;
+ }
+
+ // Avoid duplicate entries.
+ if (list.contains(f)) {
+ continue;
+ }
+
+ list.add(f);
+ }
+
+ if (list.isEmpty()) {
+ throw new IllegalArgumentException("no protocol family specified");
+ }
+
+ this.resolveAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]);
+
+ return this;
+ }
+
+ /**
+ * Returns {@code true} if and only if this resolver sends a DNS query with the RD (recursion desired) flag set.
+ * The default value is {@code true}.
+ *
+ * @see #setRecursionDesired(boolean)
+ */
+ public boolean isRecursionDesired() {
+ return recursionDesired;
+ }
+
+ /**
+ * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set.
+ *
+ * @return {@code this}
+ *
+ * @see #isRecursionDesired()
+ */
+ public DnsNameResolver setRecursionDesired(boolean recursionDesired) {
+ this.recursionDesired = recursionDesired;
+ return this;
+ }
+
+ /**
+ * Returns the maximum allowed number of DNS queries to send when resolving a host name.
+ * The default value is {@code 8}.
+ *
+ * @see #setMaxQueriesPerResolve(int)
+ */
+ public int maxQueriesPerResolve() {
+ return maxQueriesPerResolve;
+ }
+
+ /**
+ * Sets the maximum allowed number of DNS queries to send when resolving a host name.
+ *
+ * @return {@code this}
+ *
+ * @see #maxQueriesPerResolve()
+ */
+ public DnsNameResolver setMaxQueriesPerResolve(int maxQueriesPerResolve) {
+ if (maxQueriesPerResolve <= 0) {
+ throw new IllegalArgumentException("maxQueriesPerResolve: " + maxQueriesPerResolve + " (expected: > 0)");
+ }
+
+ this.maxQueriesPerResolve = maxQueriesPerResolve;
+
+ return this;
+ }
+
+ /**
+ * Returns the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes.
+ *
+ * @see #setMaxPayloadSize(int)
+ */
+ public int maxPayloadSize() {
+ return maxPayloadSize;
+ }
+
+ /**
+ * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes.
+ *
+ * @return {@code this}
+ *
+ * @see #maxPayloadSize()
+ */
+ public DnsNameResolver setMaxPayloadSize(int maxPayloadSize) {
+ if (maxPayloadSize <= 0) {
+ throw new IllegalArgumentException("maxPayloadSize: " + maxPayloadSize + " (expected: > 0)");
+ }
+
+ if (this.maxPayloadSize == maxPayloadSize) {
+ // Same value; no need to instantiate DnsClass and RecvByteBufAllocator again.
+ return this;
+ }
+
+ this.maxPayloadSize = maxPayloadSize;
+ maxPayloadSizeClass = DnsClass.valueOf(maxPayloadSize);
+ ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize));
+
+ return this;
+ }
+
+ DnsClass maxPayloadSizeClass() {
+ return maxPayloadSizeClass;
+ }
+
+ /**
+ * Clears all the DNS resource records cached by this resolver.
+ *
+ * @return {@code this}
+ *
+ * @see #clearCache(DnsQuestion)
+ */
+ public DnsNameResolver clearCache() {
+ for (Iterator> i = queryCache.entrySet().iterator(); i.hasNext();) {
+ Entry e = i.next();
+ i.remove();
+ e.getValue().release();
+ }
+
+ return this;
+ }
+
+ /**
+ * Clears the DNS resource record of the specified DNS question from the cache of this resolver.
+ */
+ public boolean clearCache(DnsQuestion question) {
+ DnsCacheEntry e = queryCache.remove(question);
+ if (e != null) {
+ e.release();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Closes the internal datagram channel used for sending and receiving DNS messages, and clears all DNS resource
+ * records from the cache. Attempting to send a DNS query or to resolve a domain name will fail once this method
+ * has been called.
+ */
+ @Override
+ public void close() {
+ ch.close();
+ }
+
+ @Override
+ protected EventLoop executor() {
+ return (EventLoop) super.executor();
+ }
+
+ @Override
+ protected boolean doIsResolved(InetSocketAddress address) {
+ return !address.isUnresolved();
+ }
+
+ @Override
+ protected void doResolve(InetSocketAddress unresolvedAddress, Promise promise) throws Exception {
+ final String hostname = IDN.toASCII(unresolvedAddress.getHostString());
+ final int port = unresolvedAddress.getPort();
+
+ final DnsNameResolverContext ctx = new DnsNameResolverContext(this, hostname, port, promise);
+
+ ctx.resolve();
+ }
+
+ /**
+ * Sends a DNS query with the specified question.
+ */
+ public Future query(DnsQuestion question) {
+ return query(nameServerAddresses, question);
+ }
+
+ /**
+ * Sends a DNS query with the specified question.
+ */
+ public Future query(DnsQuestion question, Promise promise) {
+ return query(nameServerAddresses, question, promise);
+ }
+
+ /**
+ * Sends a DNS query with the specified question using the specified name server list.
+ */
+ public Future query(Iterable nameServerAddresses, DnsQuestion question) {
+ if (nameServerAddresses == null) {
+ throw new NullPointerException("nameServerAddresses");
+ }
+ if (question == null) {
+ throw new NullPointerException("question");
+ }
+
+ final EventLoop eventLoop = ch.eventLoop();
+ final DnsCacheEntry cachedResult = queryCache.get(question);
+ if (cachedResult != null) {
+ if (cachedResult.response != null) {
+ return eventLoop.newSucceededFuture(cachedResult.response.retain());
+ } else {
+ return eventLoop.newFailedFuture(cachedResult.cause);
+ }
+ } else {
+ return query0(nameServerAddresses, question, eventLoop.newPromise());
+ }
+ }
+
+ /**
+ * Sends a DNS query with the specified question using the specified name server list.
+ */
+ public Future query(
+ Iterable nameServerAddresses, DnsQuestion question, Promise promise) {
+
+ if (nameServerAddresses == null) {
+ throw new NullPointerException("nameServerAddresses");
+ }
+ if (question == null) {
+ throw new NullPointerException("question");
+ }
+ if (promise == null) {
+ throw new NullPointerException("promise");
+ }
+
+ final DnsCacheEntry cachedResult = queryCache.get(question);
+ if (cachedResult != null) {
+ if (cachedResult.response != null) {
+ return promise.setSuccess(cachedResult.response.retain());
+ } else {
+ return promise.setFailure(cachedResult.cause);
+ }
+ } else {
+ return query0(nameServerAddresses, question, promise);
+ }
+ }
+
+ private Future query0(
+ Iterable nameServerAddresses, DnsQuestion question, Promise promise) {
+
+ try {
+ new DnsQueryContext(this, nameServerAddresses, question, promise).query();
+ return promise;
+ } catch (Exception e) {
+ return promise.setFailure(e);
+ }
+ }
+
+ void cache(final DnsQuestion question, DnsCacheEntry entry, long delaySeconds) {
+ queryCache.put(question, entry);
+ boolean scheduled = false;
+ try {
+ entry.expirationFuture = ch.eventLoop().schedule(new OneTimeTask() {
+ @Override
+ public void run() {
+ Object response = queryCache.remove(question);
+ ReferenceCountUtil.safeRelease(response);
+ }
+ }, delaySeconds, TimeUnit.SECONDS);
+
+ scheduled = true;
+ } finally {
+ if (!scheduled) {
+ // If failed to schedule the expiration task,
+ // remove the entry from the cache so that it does not leak.
+ queryCache.remove(question);
+ entry.release();
+ }
+ }
+ }
+
+ private final class DnsResponseHandler extends ChannelInboundHandlerAdapter {
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ boolean success = false;
+ try {
+ final DnsResponse res = (DnsResponse) msg;
+ final int queryId = res.header().id();
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId, res.sender(), res);
+ }
+
+ final DnsQueryContext qCtx = promises.get(queryId);
+
+ if (qCtx == null) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Received a DNS response with an unknown ID: {}", queryId);
+ }
+ return;
+ }
+
+ final List questions = res.questions();
+ if (questions.size() != 1) {
+ logger.warn("Received a DNS response with invalid number of questions: {}", res);
+ return;
+ }
+
+ final DnsQuestion q = qCtx.question();
+ if (!q.equals(questions.get(0))) {
+ logger.warn("Received a mismatching DNS response: {}", res);
+ return;
+ }
+
+ // Cancel the timeout task.
+ final ScheduledFuture> timeoutFuture = qCtx.timeoutFuture();
+ if (timeoutFuture != null) {
+ timeoutFuture.cancel(false);
+ }
+
+ if (res.header().responseCode() == DnsResponseCode.NOERROR) {
+ cache(q, res);
+ promises.set(queryId, null);
+ qCtx.promise().trySuccess(res);
+ success = true;
+ } else {
+ qCtx.retry(res.sender(),
+ "response code: " + res.header().responseCode() +
+ " with " + res.answers().size() + " answer(s) and " +
+ res.authorityResources().size() + " authority resource(s)");
+ }
+ } finally {
+ if (!success) {
+ ReferenceCountUtil.safeRelease(msg);
+ }
+ }
+ }
+
+ private void cache(DnsQuestion question, DnsResponse res) {
+ final int maxTtl = maxTtl();
+ if (maxTtl == 0) {
+ return;
+ }
+
+ long ttl = Long.MAX_VALUE;
+ // Find the smallest TTL value returned by the server.
+ for (DnsResource r: res.answers()) {
+ long rTtl = r.timeToLive();
+ if (ttl > rTtl) {
+ ttl = rTtl;
+ }
+ }
+
+ // Ensure that the found TTL is between minTtl and maxTtl.
+ ttl = Math.max(minTtl(), Math.min(maxTtl, ttl));
+
+ res.retain();
+
+ DnsNameResolver.this.cache(question, new DnsCacheEntry(res), ttl);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ logger.warn("Unexpected exception: ", cause);
+ }
+ }
+
+ static final class DnsCacheEntry {
+ final DnsResponse response;
+ final Throwable cause;
+ volatile ScheduledFuture> expirationFuture;
+
+ DnsCacheEntry(DnsResponse response) {
+ this.response = response;
+ cause = null;
+ }
+
+ DnsCacheEntry(Throwable cause) {
+ this.cause = cause;
+ response = null;
+ }
+
+ void release() {
+ DnsResponse response = this.response;
+ if (response != null) {
+ ReferenceCountUtil.safeRelease(response);
+ }
+
+ ScheduledFuture> expirationFuture = this.expirationFuture;
+ if (expirationFuture != null) {
+ expirationFuture.cancel(false);
+ }
+ }
+ }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java
new file mode 100644
index 0000000000..538e2c9a3b
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver.dns;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.handler.codec.dns.DnsClass;
+import io.netty.handler.codec.dns.DnsQuestion;
+import io.netty.handler.codec.dns.DnsResource;
+import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.handler.codec.dns.DnsResponseDecoder;
+import io.netty.handler.codec.dns.DnsType;
+import io.netty.util.CharsetUtil;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
+import io.netty.util.concurrent.Promise;
+import io.netty.util.internal.StringUtil;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+final class DnsNameResolverContext {
+
+ private static final int INADDRSZ4 = 4;
+ private static final int INADDRSZ6 = 16;
+
+ private static final FutureListener RELEASE_RESPONSE = new FutureListener() {
+ @Override
+ public void operationComplete(Future future) {
+ if (future.isSuccess()) {
+ future.getNow().release();
+ }
+ }
+ };
+
+ private final DnsNameResolver parent;
+ private final Promise promise;
+ private final String hostname;
+ private final int port;
+ private final int maxAllowedQueries;
+ private final InternetProtocolFamily[] resolveAddressTypes;
+
+ private final Set> queriesInProgress =
+ Collections.newSetFromMap(new IdentityHashMap, Boolean>());
+ private List resolvedAddresses;
+ private StringBuilder trace;
+ private int allowedQueries;
+ private boolean triedCNAME;
+
+ DnsNameResolverContext(DnsNameResolver parent, String hostname, int port, Promise promise) {
+ this.parent = parent;
+ this.promise = promise;
+ this.hostname = hostname;
+ this.port = port;
+
+ maxAllowedQueries = parent.maxQueriesPerResolve();
+ resolveAddressTypes = parent.resolveAddressTypesUnsafe();
+ allowedQueries = maxAllowedQueries;
+ }
+
+ void resolve() {
+ for (InternetProtocolFamily f: resolveAddressTypes) {
+ final DnsType type;
+ switch (f) {
+ case IPv4:
+ type = DnsType.A;
+ break;
+ case IPv6:
+ type = DnsType.AAAA;
+ break;
+ default:
+ throw new Error();
+ }
+
+ query(parent.nameServerAddresses, new DnsQuestion(hostname, type));
+ }
+ }
+
+ private void query(Iterable nameServerAddresses, final DnsQuestion question) {
+ if (allowedQueries == 0 || promise.isCancelled()) {
+ return;
+ }
+
+ allowedQueries --;
+
+ final Future f = parent.query(nameServerAddresses, question);
+ queriesInProgress.add(f);
+
+ f.addListener(new FutureListener() {
+ @Override
+ public void operationComplete(Future future) throws Exception {
+ queriesInProgress.remove(future);
+
+ if (promise.isDone()) {
+ return;
+ }
+
+ try {
+ if (future.isSuccess()) {
+ onResponse(question, future.getNow());
+ } else {
+ addTrace(future.cause());
+ }
+ } finally {
+ tryToFinishResolve();
+ }
+ }
+ });
+ }
+
+ void onResponse(final DnsQuestion question, final DnsResponse response) {
+ final DnsType type = question.type();
+ try {
+ if (type == DnsType.A || type == DnsType.AAAA) {
+ onResponseAorAAAA(type, question, response);
+ } else if (type == DnsType.CNAME) {
+ onResponseCNAME(question, response);
+ }
+ } finally {
+ ReferenceCountUtil.safeRelease(response);
+ }
+ }
+
+ private void onResponseAorAAAA(DnsType qType, DnsQuestion question, DnsResponse response) {
+ // We often get a bunch of CNAMES as well when we asked for A/AAAA.
+ final Map cnames = buildAliasMap(response);
+
+ boolean found = false;
+ for (DnsResource r: response.answers()) {
+ final DnsType type = r.type();
+ if (type != DnsType.A && type != DnsType.AAAA) {
+ continue;
+ }
+
+ final String qName = question.name().toLowerCase(Locale.US);
+ final String rName = r.name().toLowerCase(Locale.US);
+
+ // Make sure the record is for the questioned domain.
+ if (!rName.equals(qName)) {
+ // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
+ String resolved = qName;
+ do {
+ resolved = cnames.get(resolved);
+ if (rName.equals(resolved)) {
+ break;
+ }
+ } while (resolved != null);
+
+ if (resolved == null) {
+ continue;
+ }
+ }
+
+ final ByteBuf content = r.content();
+ final int contentLen = content.readableBytes();
+ if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) {
+ continue;
+ }
+
+ final byte[] addrBytes = new byte[contentLen];
+ content.getBytes(content.readerIndex(), addrBytes);
+
+ try {
+ InetAddress resolved = InetAddress.getByAddress(hostname, addrBytes);
+ if (resolvedAddresses == null) {
+ resolvedAddresses = new ArrayList();
+ }
+ resolvedAddresses.add(resolved);
+ found = true;
+ } catch (UnknownHostException e) {
+ // Should never reach here.
+ throw new Error(e);
+ }
+ }
+
+ if (found) {
+ return;
+ }
+
+ addTrace(response.sender(), "no matching " + qType + " record found");
+
+ // We aked for A/AAAA but we got only CNAME.
+ if (!cnames.isEmpty()) {
+ onResponseCNAME(question, response, cnames, false);
+ }
+ }
+
+ private void onResponseCNAME(DnsQuestion question, DnsResponse response) {
+ onResponseCNAME(question, response, buildAliasMap(response), true);
+ }
+
+ private void onResponseCNAME(
+ DnsQuestion question, DnsResponse response, Map cnames, boolean trace) {
+
+ // Resolve the host name in the question into the real host name.
+ final String name = question.name().toLowerCase(Locale.US);
+ String resolved = name;
+ boolean found = false;
+ for (;;) {
+ String next = cnames.get(resolved);
+ if (next != null) {
+ found = true;
+ resolved = next;
+ } else {
+ break;
+ }
+ }
+
+ if (found) {
+ followCname(response.sender(), name, resolved);
+ } else if (trace) {
+ addTrace(response.sender(), "no matching CNAME record found");
+ }
+ }
+
+ private static Map buildAliasMap(DnsResponse response) {
+ Map cnames = null;
+ for (DnsResource r: response.answers()) {
+ final DnsType type = r.type();
+ if (type != DnsType.CNAME) {
+ continue;
+ }
+
+ String content = decodeDomainName(r.content());
+ if (content == null) {
+ continue;
+ }
+
+ if (cnames == null) {
+ cnames = new HashMap();
+ }
+
+ cnames.put(r.name().toLowerCase(Locale.US), content.toLowerCase(Locale.US));
+ }
+
+ return cnames != null? cnames : Collections.emptyMap();
+ }
+
+ void tryToFinishResolve() {
+ if (!queriesInProgress.isEmpty()) {
+ // There are still some queries we did not receive responses for.
+ if (gotPreferredAddress()) {
+ // But it's OK to finish the resolution process if we got a resolved address of the preferred type.
+ finishResolve();
+ }
+
+ // We did not get any resolved address of the preferred type, so we can't finish the resolution process.
+ return;
+ }
+
+ // There are no queries left to try.
+ if (resolvedAddresses == null) {
+ // .. and we could not find any A/AAAA records.
+ if (!triedCNAME) {
+ // As the last resort, try to query CNAME, just in case the name server has it.
+ triedCNAME = true;
+ query(parent.nameServerAddresses, new DnsQuestion(hostname, DnsType.CNAME, DnsClass.IN));
+ return;
+ }
+ }
+
+ // We have at least one resolved address or tried CNAME as the last resort..
+ finishResolve();
+ }
+
+ private boolean gotPreferredAddress() {
+ if (resolvedAddresses == null) {
+ return false;
+ }
+
+ final int size = resolvedAddresses.size();
+ switch (resolveAddressTypes[0]) {
+ case IPv4:
+ for (int i = 0; i < size; i ++) {
+ if (resolvedAddresses.get(i) instanceof Inet4Address) {
+ return true;
+ }
+ }
+ break;
+ case IPv6:
+ for (int i = 0; i < size; i ++) {
+ if (resolvedAddresses.get(i) instanceof Inet6Address) {
+ return true;
+ }
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ private void finishResolve() {
+ if (!queriesInProgress.isEmpty()) {
+ // If there are queries in progress, we should cancel it because we already finished the resolution.
+ for (Iterator> i = queriesInProgress.iterator(); i.hasNext();) {
+ Future f = i.next();
+ i.remove();
+
+ if (!f.cancel(false)) {
+ f.addListener(RELEASE_RESPONSE);
+ }
+ }
+ }
+
+ if (resolvedAddresses != null) {
+ // Found at least one resolved address.
+ for (InternetProtocolFamily f: resolveAddressTypes) {
+ switch (f) {
+ case IPv4:
+ if (finishResolveWithIPv4()) {
+ return;
+ }
+ break;
+ case IPv6:
+ if (finishResolveWithIPv6()) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ // No resolved address found.
+ int tries = maxAllowedQueries - allowedQueries;
+ UnknownHostException cause;
+ if (tries > 1) {
+ cause = new UnknownHostException(
+ "failed to resolve " + hostname + " after " + tries + " queries:" +
+ trace);
+ } else {
+ cause = new UnknownHostException("failed to resolve " + hostname + ':' + trace);
+ }
+
+ promise.tryFailure(cause);
+ }
+
+ private boolean finishResolveWithIPv4() {
+ final List resolvedAddresses = this.resolvedAddresses;
+ final int size = resolvedAddresses.size();
+
+ for (int i = 0; i < size; i ++) {
+ InetAddress a = resolvedAddresses.get(i);
+ if (a instanceof Inet4Address) {
+ promise.trySuccess(new InetSocketAddress(a, port));
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean finishResolveWithIPv6() {
+ final List resolvedAddresses = this.resolvedAddresses;
+ final int size = resolvedAddresses.size();
+
+ for (int i = 0; i < size; i ++) {
+ InetAddress a = resolvedAddresses.get(i);
+ if (a instanceof Inet6Address) {
+ promise.trySuccess(new InetSocketAddress(a, port));
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Adapted from {@link DnsResponseDecoder#readName(ByteBuf)}.
+ */
+ static String decodeDomainName(ByteBuf buf) {
+ buf.markReaderIndex();
+ try {
+ int position = -1;
+ int checked = 0;
+ int length = buf.writerIndex();
+ StringBuilder name = new StringBuilder(64);
+ for (int len = buf.readUnsignedByte(); buf.isReadable() && len != 0; len = buf.readUnsignedByte()) {
+ boolean pointer = (len & 0xc0) == 0xc0;
+ if (pointer) {
+ if (position == -1) {
+ position = buf.readerIndex() + 1;
+ }
+ buf.readerIndex((len & 0x3f) << 8 | buf.readUnsignedByte());
+ // check for loops
+ checked += 2;
+ if (checked >= length) {
+ // Name contains a loop; give up.
+ return null;
+ }
+ } else {
+ name.append(buf.toString(buf.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
+ buf.skipBytes(len);
+ }
+ }
+
+ if (position != -1) {
+ buf.readerIndex(position);
+ }
+
+ if (name.length() == 0) {
+ return null;
+ }
+
+ return name.substring(0, name.length() - 1);
+ } finally {
+ buf.resetReaderIndex();
+ }
+ }
+
+ private void followCname(
+ InetSocketAddress nameServerAddr, String name, String cname) {
+
+ if (trace == null) {
+ trace = new StringBuilder(128);
+ }
+
+ trace.append(StringUtil.NEWLINE);
+ trace.append("\tfrom ");
+ trace.append(nameServerAddr);
+ trace.append(": ");
+ trace.append(name);
+ trace.append(" CNAME ");
+ trace.append(cname);
+
+ query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.A, DnsClass.IN));
+ query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.AAAA, DnsClass.IN));
+ }
+
+ private void addTrace(InetSocketAddress nameServerAddr, String msg) {
+ if (trace == null) {
+ trace = new StringBuilder(128);
+ }
+
+ trace.append(StringUtil.NEWLINE);
+ trace.append("\tfrom ");
+ trace.append(nameServerAddr);
+ trace.append(": ");
+ trace.append(msg);
+ }
+
+ private void addTrace(Throwable cause) {
+ if (trace == null) {
+ trace = new StringBuilder(128);
+ }
+
+ trace.append(StringUtil.NEWLINE);
+ trace.append("Caused by: ");
+ trace.append(cause);
+ }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverGroup.java
new file mode 100644
index 0000000000..fcbcaeaed2
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverGroup.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver.dns;
+
+import io.netty.channel.ChannelFactory;
+import io.netty.channel.EventLoop;
+import io.netty.channel.ReflectiveChannelFactory;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.resolver.NameResolver;
+import io.netty.resolver.NameResolverGroup;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.internal.StringUtil;
+
+import java.net.InetSocketAddress;
+
+import static io.netty.resolver.dns.DnsNameResolver.*;
+
+/**
+ * A {@link NameResolverGroup} of {@link DnsNameResolver}s.
+ */
+public final class DnsNameResolverGroup extends NameResolverGroup {
+
+ private final ChannelFactory extends DatagramChannel> channelFactory;
+ private final InetSocketAddress localAddress;
+ private final Iterable nameServerAddresses;
+
+ public DnsNameResolverGroup(
+ Class extends DatagramChannel> channelType,
+ InetSocketAddress nameServerAddress) {
+ this(channelType, ANY_LOCAL_ADDR, nameServerAddress);
+ }
+
+ public DnsNameResolverGroup(
+ Class extends DatagramChannel> channelType,
+ InetSocketAddress localAddress, InetSocketAddress nameServerAddress) {
+ this(new ReflectiveChannelFactory(channelType), localAddress, nameServerAddress);
+ }
+
+ public DnsNameResolverGroup(
+ ChannelFactory extends DatagramChannel> channelFactory,
+ InetSocketAddress nameServerAddress) {
+ this(channelFactory, ANY_LOCAL_ADDR, nameServerAddress);
+ }
+
+ public DnsNameResolverGroup(
+ ChannelFactory extends DatagramChannel> channelFactory,
+ InetSocketAddress localAddress, InetSocketAddress nameServerAddress) {
+ this(channelFactory, localAddress, DnsServerAddresses.singleton(nameServerAddress));
+ }
+
+ public DnsNameResolverGroup(
+ Class extends DatagramChannel> channelType,
+ Iterable nameServerAddresses) {
+ this(channelType, ANY_LOCAL_ADDR, nameServerAddresses);
+ }
+
+ public DnsNameResolverGroup(
+ Class extends DatagramChannel> channelType,
+ InetSocketAddress localAddress, Iterable nameServerAddresses) {
+ this(new ReflectiveChannelFactory(channelType), localAddress, nameServerAddresses);
+ }
+
+ public DnsNameResolverGroup(
+ ChannelFactory extends DatagramChannel> channelFactory,
+ Iterable nameServerAddresses) {
+ this(channelFactory, ANY_LOCAL_ADDR, nameServerAddresses);
+ }
+
+ public DnsNameResolverGroup(
+ ChannelFactory extends DatagramChannel> channelFactory,
+ InetSocketAddress localAddress, Iterable nameServerAddresses) {
+ this.channelFactory = channelFactory;
+ this.localAddress = localAddress;
+ this.nameServerAddresses = nameServerAddresses;
+ }
+
+ @Override
+ protected NameResolver newResolver(EventExecutor executor) throws Exception {
+ if (!(executor instanceof EventLoop)) {
+ throw new IllegalStateException(
+ "unsupported executor type: " + StringUtil.simpleClassName(executor) +
+ " (expected: " + StringUtil.simpleClassName(EventLoop.class));
+ }
+
+ return new DnsNameResolver((EventLoop) executor, channelFactory, localAddress, nameServerAddresses);
+ }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java
new file mode 100644
index 0000000000..9add6b88bc
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver.dns;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.socket.DatagramChannel;
+import io.netty.handler.codec.dns.DnsQuery;
+import io.netty.handler.codec.dns.DnsQuestion;
+import io.netty.handler.codec.dns.DnsResource;
+import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.handler.codec.dns.DnsType;
+import io.netty.resolver.dns.DnsNameResolver.DnsCacheEntry;
+import io.netty.util.concurrent.Promise;
+import io.netty.util.concurrent.ScheduledFuture;
+import io.netty.util.internal.OneTimeTask;
+import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.ThreadLocalRandom;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+
+final class DnsQueryContext {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class);
+
+ private final DnsNameResolver parent;
+ private final Promise promise;
+ private final int id;
+ private final DnsQuestion question;
+ private final DnsResource optResource;
+ private final Iterator nameServerAddresses;
+
+ private final boolean recursionDesired;
+ private final int maxTries;
+ private int remainingTries;
+ private volatile ScheduledFuture> timeoutFuture;
+ private StringBuilder trace;
+
+ DnsQueryContext(DnsNameResolver parent,
+ Iterable nameServerAddresses,
+ DnsQuestion question, Promise promise) {
+
+ this.parent = parent;
+ this.promise = promise;
+ this.question = question;
+
+ id = allocateId();
+ recursionDesired = parent.isRecursionDesired();
+ maxTries = parent.maxTriesPerQuery();
+ remainingTries = maxTries;
+ optResource = new DnsResource("", DnsType.OPT, parent.maxPayloadSizeClass(), 0, Unpooled.EMPTY_BUFFER);
+
+ this.nameServerAddresses = nameServerAddresses.iterator();
+ }
+
+ private int allocateId() {
+ int id = ThreadLocalRandom.current().nextInt(parent.promises.length());
+ final int maxTries = parent.promises.length() << 1;
+ int tries = 0;
+ for (;;) {
+ if (parent.promises.compareAndSet(id, null, this)) {
+ return id;
+ }
+
+ id = id + 1 & 0xFFFF;
+
+ if (++ tries >= maxTries) {
+ throw new IllegalStateException("query ID space exhausted: " + question);
+ }
+ }
+ }
+
+ Promise promise() {
+ return promise;
+ }
+
+ DnsQuestion question() {
+ return question;
+ }
+
+ ScheduledFuture> timeoutFuture() {
+ return timeoutFuture;
+ }
+
+ void query() {
+ final DnsQuestion question = this.question;
+
+ if (remainingTries <= 0 || !nameServerAddresses.hasNext()) {
+ parent.promises.set(id, null);
+
+ int tries = maxTries - remainingTries;
+ UnknownHostException cause;
+ if (tries > 1) {
+ cause = new UnknownHostException(
+ "failed to resolve " + question + " after " + tries + " attempts:" +
+ trace);
+ } else {
+ cause = new UnknownHostException("failed to resolve " + question + ':' + trace);
+ }
+
+ cache(question, cause);
+ promise.tryFailure(cause);
+ return;
+ }
+
+ remainingTries --;
+
+ final InetSocketAddress nameServerAddr = nameServerAddresses.next();
+ final DnsQuery query = new DnsQuery(id, nameServerAddr);
+ query.addQuestion(question);
+ query.header().setRecursionDesired(recursionDesired);
+ query.addAdditionalResource(optResource);
+
+ final DatagramChannel ch = parent.ch;
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("{} WRITE: [{}: {}], {}", ch, id, nameServerAddr, question);
+ }
+
+ final ChannelFuture writeFuture = ch.writeAndFlush(query);
+ if (writeFuture.isDone()) {
+ onQueryWriteCompletion(writeFuture, nameServerAddr);
+ } else {
+ writeFuture.addListener(new ChannelFutureListener() {
+ @Override
+ public void operationComplete(ChannelFuture future) throws Exception {
+ onQueryWriteCompletion(writeFuture, nameServerAddr);
+ }
+ });
+ }
+ }
+
+ private void onQueryWriteCompletion(ChannelFuture writeFuture, final InetSocketAddress nameServerAddr) {
+ if (!writeFuture.isSuccess()) {
+ retry(nameServerAddr, "failed to send a query: " + writeFuture.cause());
+ return;
+ }
+
+ // Schedule a query timeout task if necessary.
+ final long queryTimeoutMillis = parent.queryTimeoutMillis();
+ if (queryTimeoutMillis > 0) {
+ timeoutFuture = parent.ch.eventLoop().schedule(new OneTimeTask() {
+ @Override
+ public void run() {
+ if (promise.isDone()) {
+ // Received a response before the query times out.
+ return;
+ }
+
+ retry(nameServerAddr, "query timed out after " + queryTimeoutMillis + " milliseconds");
+ }
+ }, queryTimeoutMillis, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ void retry(InetSocketAddress nameServerAddr, String message) {
+ if (promise.isCancelled()) {
+ return;
+ }
+
+ if (trace == null) {
+ trace = new StringBuilder(128);
+ }
+
+ trace.append(StringUtil.NEWLINE);
+ trace.append("\tfrom ");
+ trace.append(nameServerAddr);
+ trace.append(": ");
+ trace.append(message);
+ query();
+ }
+
+ private void cache(final DnsQuestion question, Throwable cause) {
+ final int negativeTtl = parent.negativeTtl();
+ if (negativeTtl == 0) {
+ return;
+ }
+
+ parent.cache(question, new DnsCacheEntry(cause), negativeTtl);
+ }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java
new file mode 100644
index 0000000000..c2cd6fe99a
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver.dns;
+
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.ThreadLocalRandom;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+/**
+ * Provides a sequence of DNS server addresses to {@link DnsNameResolver}. The {@link Iterator} created by the
+ * {@link Iterable}s returned by the factory methods of this class is infinite, which means {@link Iterator#hasNext()}
+ * will never return {@code false} and {@link Iterator#next()} will never raise a {@link NoSuchElementException}.
+ */
+@SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException")
+public final class DnsServerAddresses {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsServerAddresses.class);
+
+ private static final List DEFAULT_NAME_SERVER_LIST;
+ private static final InetSocketAddress[] DEFAULT_NAME_SERVER_ARRAY;
+
+ static {
+ final int DNS_PORT = 53;
+ final List defaultNameServers = new ArrayList(2);
+ 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 list = (List) nameservers.invoke(instance);
+ final int size = list.size();
+ for (int i = 0; i < size; i ++) {
+ String dnsAddr = list.get(i);
+ if (dnsAddr != null) {
+ defaultNameServers.add(new InetSocketAddress(InetAddress.getByName(dnsAddr), DNS_PORT));
+ }
+ }
+ } catch (Exception ignore) {
+ // Failed to get the system name server list.
+ // Will add the default name servers afterwards.
+ }
+
+ if (!defaultNameServers.isEmpty()) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(
+ "Default DNS servers: {} (sun.net.dns.ResolverConfiguration)", defaultNameServers);
+ }
+ } else {
+ Collections.addAll(
+ defaultNameServers,
+ new InetSocketAddress("8.8.8.8", DNS_PORT),
+ new InetSocketAddress("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()]);
+ }
+
+ /**
+ * Returns the list of the system DNS server addresses. If it failed to retrieve the list of the system DNS server
+ * addresses from the environment, it will return {@code "8.8.8.8"} and {@code "8.8.4.4"}, the addresses of the
+ * Google public DNS servers. Note that the {@code Iterator} of the returned list is not infinite unlike other
+ * factory methods in this class. To make the returned list infinite, pass it to the other factory method. e.g.
+ *
+ */
+ public static List defaultAddresses() {
+ return DEFAULT_NAME_SERVER_LIST;
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates
+ * the DNS server addresses in a sequential order.
+ */
+ public static Iterable sequential(Iterable extends InetSocketAddress> addresses) {
+ return sequential0(sanitize(addresses));
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates
+ * the DNS server addresses in a sequential order.
+ */
+ public static Iterable sequential(InetSocketAddress... addresses) {
+ return sequential0(sanitize(addresses));
+ }
+
+ private static Iterable sequential0(final InetSocketAddress[] addresses) {
+ return new Iterable() {
+ @Override
+ public Iterator iterator() {
+ return new SequentialAddressIterator(addresses, 0);
+ }
+ };
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates
+ * the DNS server addresses in a shuffled order.
+ */
+ public static Iterable shuffled(Iterable extends InetSocketAddress> addresses) {
+ return shuffled0(sanitize(addresses));
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates
+ * the DNS server addresses in a shuffled order.
+ */
+ public static Iterable shuffled(InetSocketAddress... addresses) {
+ return shuffled0(sanitize(addresses));
+ }
+
+ private static Iterable shuffled0(final InetSocketAddress[] addresses) {
+ if (addresses.length == 1) {
+ return singleton(addresses[0]);
+ }
+
+ return new Iterable() {
+ @Override
+ public Iterator iterator() {
+ return new ShuffledAddressIterator(addresses);
+ }
+ };
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates
+ * the DNS server addresses in a rotational order. It is similar to {@link #sequential(Iterable)}, but each
+ * {@link Iterator} starts from a different starting point. For example, the first {@link Iterable#iterator()}
+ * will iterate from the first DNS server address, the second one will iterate from the second DNS server address,
+ * and so on.
+ */
+ public static Iterable rotational(Iterable extends InetSocketAddress> addresses) {
+ return rotational0(sanitize(addresses));
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server addresses, whose {@link Iterator} iterates
+ * the DNS server addresses in a rotational order. It is similar to {@link #sequential(Iterable)}, but each
+ * {@link Iterator} starts from a different starting point. For example, the first {@link Iterable#iterator()}
+ * will iterate from the first DNS server address, the second one will iterate from the second DNS server address,
+ * and so on.
+ */
+ public static Iterable rotational(InetSocketAddress... addresses) {
+ return rotational0(sanitize(addresses));
+ }
+
+ private static Iterable rotational0(final InetSocketAddress[] addresses) {
+ return new RotationalAddresses(addresses);
+ }
+
+ /**
+ * Returns an infinite {@link Iterable} of the specified DNS server address, whose {@link Iterator} always
+ * return the same DNS server address.
+ */
+ public static Iterable singleton(final InetSocketAddress address) {
+ if (address == null) {
+ throw new NullPointerException("address");
+ }
+ if (address.isUnresolved()) {
+ throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + address);
+ }
+
+ return new Iterable() {
+
+ private final Iterator iterator = new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return true;
+ }
+
+ @Override
+ public InetSocketAddress next() {
+ return address;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ @Override
+ public Iterator iterator() {
+ return iterator;
+ }
+ };
+ }
+
+ private static InetSocketAddress[] sanitize(Iterable extends InetSocketAddress> addresses) {
+ if (addresses == null) {
+ throw new NullPointerException("addresses");
+ }
+
+ final List list;
+ if (addresses instanceof Collection) {
+ list = new ArrayList(((Collection>) addresses).size());
+ } else {
+ list = new ArrayList(4);
+ }
+
+ for (InetSocketAddress a : addresses) {
+ if (a == null) {
+ break;
+ }
+ if (a.isUnresolved()) {
+ throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + a);
+ }
+ list.add(a);
+ }
+
+ if (list.isEmpty()) {
+ return DEFAULT_NAME_SERVER_ARRAY;
+ }
+
+ return list.toArray(new InetSocketAddress[list.size()]);
+ }
+
+ private static InetSocketAddress[] sanitize(InetSocketAddress[] addresses) {
+ if (addresses == null) {
+ throw new NullPointerException("addresses");
+ }
+
+ List list = new ArrayList(addresses.length);
+ for (InetSocketAddress a: addresses) {
+ if (a == null) {
+ break;
+ }
+ if (a.isUnresolved()) {
+ throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + a);
+ }
+ list.add(a);
+ }
+
+ if (list.isEmpty()) {
+ return DEFAULT_NAME_SERVER_ARRAY;
+ }
+
+ return list.toArray(new InetSocketAddress[list.size()]);
+ }
+
+ private DnsServerAddresses() { }
+
+ private static final class SequentialAddressIterator implements Iterator {
+
+ private final InetSocketAddress[] addresses;
+ private int i;
+
+ SequentialAddressIterator(InetSocketAddress[] addresses, int startIdx) {
+ this.addresses = addresses;
+ i = startIdx;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return true;
+ }
+
+ @Override
+ public InetSocketAddress next() {
+ int i = this.i;
+ InetSocketAddress next = addresses[i];
+ if (++ i < addresses.length) {
+ this.i = i;
+ } else {
+ this.i = 0;
+ }
+ return next;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static final class ShuffledAddressIterator implements Iterator {
+
+ private final InetSocketAddress[] addresses;
+ private int i;
+
+ ShuffledAddressIterator(InetSocketAddress[] addresses) {
+ this.addresses = addresses.clone();
+
+ shuffle();
+ }
+
+ private void shuffle() {
+ final InetSocketAddress[] addresses = this.addresses;
+ final Random r = ThreadLocalRandom.current();
+
+ for (int i = addresses.length - 1; i >= 0; i --) {
+ InetSocketAddress tmp = addresses[i];
+ int j = r.nextInt(i + 1);
+ addresses[i] = addresses[j];
+ addresses[j] = tmp;
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return true;
+ }
+
+ @Override
+ public InetSocketAddress next() {
+ int i = this.i;
+ InetSocketAddress next = addresses[i];
+ if (++ i < addresses.length) {
+ this.i = i;
+ } else {
+ this.i = 0;
+ shuffle();
+ }
+ return next;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static final class RotationalAddresses implements Iterable {
+
+ private static final AtomicIntegerFieldUpdater startIdxUpdater;
+
+ static {
+ AtomicIntegerFieldUpdater updater =
+ PlatformDependent.newAtomicIntegerFieldUpdater(RotationalAddresses.class, "startIdx");
+
+ if (updater == null) {
+ updater = AtomicIntegerFieldUpdater.newUpdater(RotationalAddresses.class, "startIdx");
+ }
+
+ startIdxUpdater = updater;
+ }
+
+ private final InetSocketAddress[] addresses;
+ @SuppressWarnings("UnusedDeclaration")
+ private volatile int startIdx;
+
+ RotationalAddresses(InetSocketAddress[] addresses) {
+ this.addresses = addresses;
+ }
+
+ @Override
+ public Iterator iterator() {
+ for (;;) {
+ int curStartIdx = startIdx;
+ int nextStartIdx = curStartIdx + 1;
+ if (nextStartIdx >= addresses.length) {
+ nextStartIdx = 0;
+ }
+ if (startIdxUpdater.compareAndSet(this, curStartIdx, nextStartIdx)) {
+ return new SequentialAddressIterator(addresses, curStartIdx);
+ }
+ }
+ }
+ }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java
new file mode 100644
index 0000000000..63825ba878
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * An alternative to Java's built-in domain name lookup mechanism that resolves a domain name asynchronously,
+ * which supports the queries of an arbitrary DNS record type as well.
+ */
+package io.netty.resolver.dns;
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java
new file mode 100644
index 0000000000..eb6503743f
--- /dev/null
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver.dns;
+
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.handler.codec.dns.DnsQuestion;
+import io.netty.handler.codec.dns.DnsResource;
+import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.handler.codec.dns.DnsResponseCode;
+import io.netty.handler.codec.dns.DnsType;
+import io.netty.util.concurrent.Future;
+import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.ThreadLocalRandom;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+public class DnsNameResolverTest {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class);
+
+ private static final List SERVERS = Arrays.asList(
+ new InetSocketAddress("8.8.8.8", 53), // Google Public DNS
+ new InetSocketAddress("8.8.4.4", 53),
+ new InetSocketAddress("208.67.222.222", 53), // OpenDNS
+ new InetSocketAddress("208.67.220.220", 53),
+ new InetSocketAddress("37.235.1.174", 53), // FreeDNS
+ new InetSocketAddress("37.235.1.177", 53)
+ );
+
+ // Using the top-100 web sites ranked in Alexa.com (Oct 2014)
+ // Please use the following series of shell commands to get this up-to-date:
+ // $ curl -O http://s3.amazonaws.com/alexa-static/top-1m.csv.zip
+ // $ unzip -o top-1m.csv.zip top-1m.csv
+ // $ head -100 top-1m.csv | cut -d, -f2 | cut -d/ -f1 | while read L; do echo '"'"$L"'",'; done > topsites.txt
+ private static final String[] DOMAINS = {
+ "google.com",
+ "facebook.com",
+ "youtube.com",
+ "yahoo.com",
+ "baidu.com",
+ "wikipedia.org",
+ "amazon.com",
+ "twitter.com",
+ "qq.com",
+ "taobao.com",
+ "linkedin.com",
+ "google.co.in",
+ "live.com",
+ "hao123.com",
+ "sina.com.cn",
+ "blogspot.com",
+ "weibo.com",
+ "yahoo.co.jp",
+ "tmall.com",
+ "yandex.ru",
+ "sohu.com",
+ "bing.com",
+ "ebay.com",
+ "pinterest.com",
+ "vk.com",
+ "google.de",
+ "wordpress.com",
+ "apple.com",
+ "google.co.jp",
+ "google.co.uk",
+ "360.cn",
+ "instagram.com",
+ "google.fr",
+ "msn.com",
+ "ask.com",
+ "soso.com",
+ "google.com.br",
+ "tumblr.com",
+ "paypal.com",
+ "mail.ru",
+ "xvideos.com",
+ "microsoft.com",
+ "google.ru",
+ "reddit.com",
+ "google.it",
+ "imgur.com",
+ "163.com",
+ "google.es",
+ "imdb.com",
+ "aliexpress.com",
+ "t.co",
+ "go.com",
+ "adcash.com",
+ "craigslist.org",
+ "amazon.co.jp",
+ "alibaba.com",
+ "google.com.mx",
+ "stackoverflow.com",
+ "xhamster.com",
+ "fc2.com",
+ "google.ca",
+ "bbc.co.uk",
+ "espn.go.com",
+ "cnn.com",
+ "google.co.id",
+ "people.com.cn",
+ "gmw.cn",
+ "pornhub.com",
+ "blogger.com",
+ "huffingtonpost.com",
+ "flipkart.com",
+ "akamaihd.net",
+ "google.com.tr",
+ "amazon.de",
+ "netflix.com",
+ "onclickads.net",
+ "googleusercontent.com",
+ "kickass.to",
+ "google.com.au",
+ "google.pl",
+ "xinhuanet.com",
+ "ebay.de",
+ "wordpress.org",
+ "odnoklassniki.ru",
+ "google.com.hk",
+ "adobe.com",
+ "dailymotion.com",
+ "dailymail.co.uk",
+ "indiatimes.com",
+ "thepiratebay.se",
+ "amazon.co.uk",
+ "xnxx.com",
+ "rakuten.co.jp",
+ "dropbox.com",
+ "tudou.com",
+ "about.com",
+ "cnet.com",
+ "vimeo.com",
+ "redtube.com",
+ "blogspot.in",
+ };
+
+ /**
+ * The list of the domain names to exclude from {@link #testResolveAorAAAA()}.
+ */
+ private static final Set EXCLUSIONS_RESOLVE_A = new HashSet();
+ static {
+ Collections.addAll(
+ EXCLUSIONS_RESOLVE_A,
+ "akamaihd.net",
+ "googleusercontent.com",
+ "");
+ }
+
+ /**
+ * The list of the domain names to exclude from {@link #testResolveAAAA()}.
+ * Unfortunately, there are only handful of domain names with IPv6 addresses.
+ */
+ private static final Set EXCLUSIONS_RESOLVE_AAAA = new HashSet();
+ static {
+ EXCLUSIONS_RESOLVE_AAAA.addAll(EXCLUSIONS_RESOLVE_A);
+ Collections.addAll(EXCLUSIONS_RESOLVE_AAAA, DOMAINS);
+ EXCLUSIONS_RESOLVE_AAAA.removeAll(Arrays.asList(
+ "google.com",
+ "facebook.com",
+ "youtube.com",
+ "wikipedia.org",
+ "google.co.in",
+ "blogspot.com",
+ "vk.com",
+ "google.de",
+ "google.co.jp",
+ "google.co.uk",
+ "google.fr",
+ "google.com.br",
+ "google.ru",
+ "google.it",
+ "google.es",
+ "google.com.mx",
+ "xhamster.com",
+ "google.ca",
+ "google.co.id",
+ "blogger.com",
+ "flipkart.com",
+ "google.com.tr",
+ "google.com.au",
+ "google.pl",
+ "google.com.hk",
+ "blogspot.in"
+ ));
+ }
+
+ /**
+ * The list of the domain names to exclude from {@link #testQueryMx()}.
+ */
+ private static final Set EXCLUSIONS_QUERY_MX = new HashSet();
+ static {
+ Collections.addAll(
+ EXCLUSIONS_QUERY_MX,
+ "hao123.com",
+ "blogspot.com",
+ "t.co",
+ "espn.go.com",
+ "people.com.cn",
+ "googleusercontent.com",
+ "blogspot.in",
+ "");
+ }
+
+ private static final EventLoopGroup group = new NioEventLoopGroup(1);
+ private static final DnsNameResolver resolver = new DnsNameResolver(
+ group.next(), NioDatagramChannel.class, DnsServerAddresses.shuffled(SERVERS));
+
+ static {
+ resolver.setMaxTriesPerQuery(SERVERS.size());
+ }
+
+ @AfterClass
+ public static void destroy() {
+ group.shutdownGracefully();
+ }
+
+ @Before
+ public void reset() {
+ resolver.clearCache();
+ }
+
+ @Test
+ public void testResolveAorAAAA() throws Exception {
+ testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4, InternetProtocolFamily.IPv6);
+ }
+
+ @Test
+ public void testResolveAAAAorA() throws Exception {
+ testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv6, InternetProtocolFamily.IPv4);
+ }
+
+ @Test
+ public void testResolveA() throws Exception {
+
+ final int oldMinTtl = resolver.minTtl();
+ final int oldMaxTtl = resolver.maxTtl();
+
+ // Cache for eternity.
+ resolver.setTtl(Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+ try {
+ final Map resultA = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4);
+
+ // Now, try to resolve again to see if it's cached.
+ // This test works because the DNS servers usually randomizes the order of the records in a response.
+ // If cached, the resolved addresses must be always same, because we reuse the same response.
+
+ final Map resultB = testResolve0(EXCLUSIONS_RESOLVE_A, InternetProtocolFamily.IPv4);
+
+ assertThat(resultA, is(resultB));
+ } finally {
+ // Restore the TTL configuration.
+ resolver.setTtl(oldMinTtl, oldMaxTtl);
+ }
+ }
+
+ @Test
+ public void testResolveAAAA() throws Exception {
+ testResolve0(EXCLUSIONS_RESOLVE_AAAA, InternetProtocolFamily.IPv6);
+ }
+
+ private static Map testResolve0(
+ Set excludedDomains, InternetProtocolFamily... famililies) throws InterruptedException {
+
+ final List oldResolveAddressTypes = resolver.resolveAddressTypes();
+
+ assertThat(resolver.isRecursionDesired(), is(true));
+ assertThat(oldResolveAddressTypes.size(), is(InternetProtocolFamily.values().length));
+
+ resolver.setResolveAddressTypes(famililies);
+
+ final Map results = new HashMap();
+ try {
+ final Map> futures =
+ new LinkedHashMap>();
+
+ for (String name : DOMAINS) {
+ if (excludedDomains.contains(name)) {
+ continue;
+ }
+
+ resolve(futures, name);
+ }
+
+ for (Entry> e : futures.entrySet()) {
+ InetSocketAddress unresolved = e.getKey();
+ InetSocketAddress resolved = e.getValue().sync().getNow();
+
+ logger.info("{}: {}", unresolved.getHostString(), resolved.getAddress().getHostAddress());
+
+ assertThat(resolved.isUnresolved(), is(false));
+ assertThat(resolved.getHostString(), is(unresolved.getHostString()));
+ assertThat(resolved.getPort(), is(unresolved.getPort()));
+
+ boolean typeMatches = false;
+ for (InternetProtocolFamily f: famililies) {
+ Class> resolvedType = resolved.getAddress().getClass();
+ switch (f) {
+ case IPv4:
+ if (Inet4Address.class.isAssignableFrom(resolvedType)) {
+ typeMatches = true;
+ }
+ break;
+ case IPv6:
+ if (Inet6Address.class.isAssignableFrom(resolvedType)) {
+ typeMatches = true;
+ }
+ break;
+ }
+ }
+
+ assertThat(typeMatches, is(true));
+
+ results.put(resolved.getHostString(), resolved.getAddress());
+ }
+ } finally {
+ resolver.setResolveAddressTypes(oldResolveAddressTypes);
+ }
+
+ return results;
+ }
+
+ @Test
+ public void testQueryMx() throws Exception {
+ assertThat(resolver.isRecursionDesired(), is(true));
+
+ Map> futures =
+ new LinkedHashMap>();
+ for (String name: DOMAINS) {
+ if (EXCLUSIONS_QUERY_MX.contains(name)) {
+ continue;
+ }
+
+ queryMx(futures, name);
+ }
+
+ for (Entry> e: futures.entrySet()) {
+ String hostname = e.getKey();
+ DnsResponse response = e.getValue().sync().getNow();
+
+ assertThat(response.header().responseCode(), is(DnsResponseCode.NOERROR));
+ List mxList = new ArrayList();
+ for (DnsResource r: response.answers()) {
+ if (r.type() == DnsType.MX) {
+ mxList.add(r);
+ }
+ }
+
+ assertThat(mxList.size(), is(greaterThan(0)));
+ StringBuilder buf = new StringBuilder();
+ for (DnsResource r: mxList) {
+ buf.append(StringUtil.NEWLINE);
+ buf.append('\t');
+ buf.append(r.name());
+ buf.append(' ');
+ buf.append(r.type());
+ buf.append(' ');
+ buf.append(r.content().readUnsignedShort());
+ buf.append(' ');
+ buf.append(DnsNameResolverContext.decodeDomainName(r.content()));
+ }
+
+ logger.info("{} has the following MX records:{}", hostname, buf);
+ response.release();
+ }
+ }
+
+ private static void resolve(Map> futures, String hostname) {
+ InetSocketAddress unresolved =
+ InetSocketAddress.createUnresolved(hostname, ThreadLocalRandom.current().nextInt(65536));
+
+ futures.put(unresolved, resolver.resolve(unresolved));
+ }
+
+ private static void queryMx(Map> futures, String hostname) throws Exception {
+ futures.put(hostname, resolver.query(new DnsQuestion(hostname, DnsType.MX)));
+ }
+}
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java
new file mode 100644
index 0000000000..25a6190468
--- /dev/null
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver.dns;
+
+import io.netty.util.NetUtil;
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+public class DnsServerAddressesTest {
+
+ private static final InetSocketAddress ADDR1 = new InetSocketAddress(NetUtil.LOCALHOST, 1);
+ private static final InetSocketAddress ADDR2 = new InetSocketAddress(NetUtil.LOCALHOST, 2);
+ private static final InetSocketAddress ADDR3 = new InetSocketAddress(NetUtil.LOCALHOST, 3);
+
+ @Test
+ public void testDefaultAddresses() {
+ assertThat(DnsServerAddresses.defaultAddresses().size(), is(greaterThan(0)));
+ }
+
+ @Test
+ public void testSequential() {
+ Iterable seq = DnsServerAddresses.sequential(ADDR1, ADDR2, ADDR3);
+ assertThat(seq.iterator(), is(not(sameInstance(seq.iterator()))));
+
+ for (int j = 0; j < 2; j ++) {
+ Iterator i = seq.iterator();
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ }
+ }
+
+ @Test
+ public void testRotational() {
+ Iterable seq = DnsServerAddresses.rotational(ADDR1, ADDR2, ADDR3);
+
+ Iterator i = seq.iterator();
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+
+ i = seq.iterator();
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+
+ i = seq.iterator();
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+
+ i = seq.iterator();
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR2);
+ assertNext(i, ADDR3);
+ }
+
+ @Test
+ public void testShuffled() {
+ Iterable seq = DnsServerAddresses.shuffled(ADDR1, ADDR2, ADDR3);
+
+ // Ensure that all three addresses are returned by the iterator.
+ // In theory, this test can fail at extremely low chance, but we don't really care.
+ Set set = Collections.newSetFromMap(new IdentityHashMap());
+ Iterator i = seq.iterator();
+ for (int j = 0; j < 1048576; j ++) {
+ assertThat(i.hasNext(), is(true));
+ set.add(i.next());
+ }
+
+ assertThat(set.size(), is(3));
+ assertThat(seq.iterator(), is(not(sameInstance(seq.iterator()))));
+ }
+
+ @Test
+ public void testSingleton() {
+ Iterable seq = DnsServerAddresses.singleton(ADDR1);
+
+ // Should return the same iterator instance for least possible footprint.
+ assertThat(seq.iterator(), is(sameInstance(seq.iterator())));
+
+ Iterator i = seq.iterator();
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR1);
+ assertNext(i, ADDR1);
+ }
+
+ private static void assertNext(Iterator i, InetSocketAddress addr) {
+ assertThat(i.hasNext(), is(true));
+ assertThat(i.next(), is(sameInstance(addr)));
+ }
+}
diff --git a/resolver/pom.xml b/resolver/pom.xml
new file mode 100644
index 0000000000..13f67c363f
--- /dev/null
+++ b/resolver/pom.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ 4.0.0
+
+ io.netty
+ netty-parent
+ 4.1.0.Beta4-SNAPSHOT
+
+
+ netty-resolver
+ jar
+
+ Netty/Resolver
+
+
+
+ ${project.groupId}
+ netty-common
+ ${project.version}
+
+
+
+
diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java
new file mode 100644
index 0000000000..bdfb3b7cb4
--- /dev/null
+++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Promise;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+/**
+ * A {@link NameResolver} that resolves an {@link InetSocketAddress} using JDK's built-in domain name lookup mechanism.
+ * Note that this resolver performs a blocking name lookup from the caller thread.
+ */
+public class DefaultNameResolver extends SimpleNameResolver {
+
+ public DefaultNameResolver(EventExecutor executor) {
+ super(executor);
+ }
+
+ @Override
+ protected boolean doIsResolved(InetSocketAddress address) {
+ return !address.isUnresolved();
+ }
+
+ @Override
+ protected void doResolve(InetSocketAddress unresolvedAddress, Promise promise) throws Exception {
+ try {
+ promise.setSuccess(
+ new InetSocketAddress(
+ InetAddress.getByName(unresolvedAddress.getHostString()),
+ unresolvedAddress.getPort()));
+ } catch (UnknownHostException e) {
+ promise.setFailure(e);
+ }
+ }
+}
diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolverGroup.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolverGroup.java
new file mode 100644
index 0000000000..f5b76ae1e5
--- /dev/null
+++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolverGroup.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver;
+
+import io.netty.util.concurrent.EventExecutor;
+
+import java.net.InetSocketAddress;
+
+/**
+ * A {@link NameResolverGroup} of {@link DefaultNameResolver}s.
+ */
+public final class DefaultNameResolverGroup extends NameResolverGroup {
+
+ public static final DefaultNameResolverGroup INSTANCE = new DefaultNameResolverGroup();
+
+ private DefaultNameResolverGroup() { }
+
+ @Override
+ protected NameResolver newResolver(EventExecutor executor) throws Exception {
+ return new DefaultNameResolver(executor);
+ }
+}
diff --git a/resolver/src/main/java/io/netty/resolver/NameResolver.java b/resolver/src/main/java/io/netty/resolver/NameResolver.java
new file mode 100644
index 0000000000..7760d1f460
--- /dev/null
+++ b/resolver/src/main/java/io/netty/resolver/NameResolver.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver;
+
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.Promise;
+
+import java.io.Closeable;
+import java.net.SocketAddress;
+import java.nio.channels.UnsupportedAddressTypeException;
+
+/**
+ * Resolves an arbitrary string that represents the name of an endpoint into a {@link SocketAddress}.
+ */
+public interface NameResolver extends Closeable {
+
+ /**
+ * Returns {@code true} if and only if the specified address is supported by this resolved.
+ */
+ boolean isSupported(SocketAddress address);
+
+ /**
+ * Returns {@code true} if and only if the specified address has been resolved.
+ *
+ * @throws UnsupportedAddressTypeException if the specified address is not supported by this resolver
+ */
+ boolean isResolved(SocketAddress address);
+
+ /**
+ * Resolves the specified name into a {@link SocketAddress}.
+ *
+ * @param inetHost the name to resolve
+ * @param inetPort the port number
+ *
+ * @return the {@link SocketAddress} as the result of the resolution
+ */
+ Future resolve(String inetHost, int inetPort);
+
+ /**
+ * Resolves the specified name into a {@link SocketAddress}.
+ *
+ * @param inetHost the name to resolve
+ * @param inetPort the port number
+ * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished
+ *
+ * @return the {@link SocketAddress} as the result of the resolution
+ */
+ Future resolve(String inetHost, int inetPort, Promise promise);
+
+ /**
+ * Resolves the specified address. If the specified address is resolved already, this method does nothing
+ * but returning the original address.
+ *
+ * @param address the address to resolve
+ *
+ * @return the {@link SocketAddress} as the result of the resolution
+ */
+ Future resolve(SocketAddress address);
+
+ /**
+ * Resolves the specified address. If the specified address is resolved already, this method does nothing
+ * but returning the original address.
+ *
+ * @param address the address to resolve
+ * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished
+ *
+ * @return the {@link SocketAddress} as the result of the resolution
+ */
+ Future resolve(SocketAddress address, Promise promise);
+
+ /**
+ * Closes all the resources allocated and used by this resolver.
+ */
+ @Override
+ void close();
+}
diff --git a/resolver/src/main/java/io/netty/resolver/NameResolverGroup.java b/resolver/src/main/java/io/netty/resolver/NameResolverGroup.java
new file mode 100644
index 0000000000..58662a4ec5
--- /dev/null
+++ b/resolver/src/main/java/io/netty/resolver/NameResolverGroup.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.resolver;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.io.Closeable;
+import java.net.SocketAddress;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Creates and manages {@link NameResolver}s so that each {@link EventExecutor} has its own resolver instance.
+ */
+public abstract class NameResolverGroup implements Closeable {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(NameResolverGroup.class);
+
+ /**
+ * Note that we do not use a {@link ConcurrentMap} here because it is usually expensive to instantiate a resolver.
+ */
+ private final Map> resolvers =
+ new IdentityHashMap>();
+
+ protected NameResolverGroup() { }
+
+ /**
+ * Returns the {@link NameResolver} associated with the specified {@link EventExecutor}. If there's no associated
+ * resolved found, this method creates and returns a new resolver instance created by
+ * {@link #newResolver(EventExecutor)} so that the new resolver is reused on another
+ * {@link #getResolver(EventExecutor)} call with the same {@link EventExecutor}.
+ */
+ public NameResolver getResolver(final EventExecutor executor) {
+ if (executor == null) {
+ throw new NullPointerException("executor");
+ }
+
+ if (executor.isShuttingDown()) {
+ throw new IllegalStateException("executor not accepting a task");
+ }
+
+ NameResolver r;
+ synchronized (resolvers) {
+ r = resolvers.get(executor);
+ if (r == null) {
+ final NameResolver newResolver;
+ try {
+ newResolver = newResolver(executor);
+ } catch (Exception e) {
+ throw new IllegalStateException("failed to create a new resolver", e);
+ }
+
+ resolvers.put(executor, newResolver);
+ executor.terminationFuture().addListener(new FutureListener
+
+ ${project.groupId}
+ netty-resolver
+ ${project.version}
+
diff --git a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
index 48ad5da3d4..e6ece5a79f 100644
--- a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
+++ b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
@@ -17,7 +17,6 @@
package io.netty.bootstrap;
import io.netty.channel.Channel;
-import io.netty.channel.ChannelException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
@@ -26,6 +25,7 @@ import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ReflectiveChannelFactory;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.GlobalEventExecutor;
@@ -47,6 +47,7 @@ import java.util.Map;
public abstract class AbstractBootstrap, C extends Channel> implements Cloneable {
private volatile EventLoopGroup group;
+ @SuppressWarnings("deprecation")
private volatile ChannelFactory extends C> channelFactory;
private volatile SocketAddress localAddress;
private final Map, Object> options = new LinkedHashMap, Object>();
@@ -88,23 +89,20 @@ public abstract class AbstractBootstrap, C ext
/**
* The {@link Class} which is used to create {@link Channel} instances from.
- * You either use this or {@link #channelFactory(ChannelFactory)} if your
+ * You either use this or {@link #channelFactory(io.netty.channel.ChannelFactory)} if your
* {@link Channel} implementation has no no-args constructor.
*/
public B channel(Class extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
- return channelFactory(new BootstrapChannelFactory(channelClass));
+ return channelFactory(new ReflectiveChannelFactory(channelClass));
}
/**
- * {@link ChannelFactory} which is used to create {@link Channel} instances from
- * when calling {@link #bind()}. This method is usually only used if {@link #channel(Class)}
- * is not working for you because of some more complex needs. If your {@link Channel} implementation
- * has a no-args constructor, its highly recommend to just use {@link #channel(Class)} for
- * simplify your code.
+ * @deprecated Use {@link #channelFactory(io.netty.channel.ChannelFactory)} instead.
*/
+ @Deprecated
@SuppressWarnings("unchecked")
public B channelFactory(ChannelFactory extends C> channelFactory) {
if (channelFactory == null) {
@@ -118,9 +116,20 @@ public abstract class AbstractBootstrap, C ext
return (B) this;
}
+ /**
+ * {@link io.netty.channel.ChannelFactory} which is used to create {@link Channel} instances from
+ * when calling {@link #bind()}. This method is usually only used if {@link #channel(Class)}
+ * is not working for you because of some more complex needs. If your {@link Channel} implementation
+ * has a no-args constructor, its highly recommend to just use {@link #channel(Class)} for
+ * simplify your code.
+ */
+ @SuppressWarnings({ "unchecked", "deprecation" })
+ public B channelFactory(io.netty.channel.ChannelFactory extends C> channelFactory) {
+ return channelFactory((ChannelFactory) channelFactory);
+ }
+
/**
* The {@link SocketAddress} which is used to bind the local "end" to.
- *
*/
@SuppressWarnings("unchecked")
public B localAddress(SocketAddress localAddress) {
@@ -371,6 +380,7 @@ public abstract class AbstractBootstrap, C ext
return localAddress;
}
+ @SuppressWarnings("deprecation")
final ChannelFactory extends C> channelFactory() {
return channelFactory;
}
@@ -442,28 +452,6 @@ public abstract class AbstractBootstrap, C ext
return buf.toString();
}
- private static final class BootstrapChannelFactory implements ChannelFactory {
- private final Class extends T> clazz;
-
- BootstrapChannelFactory(Class extends T> clazz) {
- this.clazz = clazz;
- }
-
- @Override
- public T newChannel() {
- try {
- return clazz.newInstance();
- } catch (Throwable t) {
- throw new ChannelException("Unable to create Channel from class " + clazz, t);
- }
- }
-
- @Override
- public String toString() {
- return StringUtil.simpleClassName(clazz) + ".class";
- }
- }
-
private static final class PendingRegistrationPromise extends DefaultChannelPromise {
// Is set to the correct EventExecutor once the registration was successful. Otherwise it will
// stay null and so the GlobalEventExecutor.INSTANCE will be used for notifications.
diff --git a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java
index 687d717fc9..2cd4c3227f 100644
--- a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java
+++ b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java
@@ -21,7 +21,13 @@ import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
+import io.netty.channel.EventLoop;
+import io.netty.resolver.DefaultNameResolverGroup;
+import io.netty.resolver.NameResolver;
+import io.netty.resolver.NameResolverGroup;
import io.netty.util.AttributeKey;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -42,6 +48,10 @@ public final class Bootstrap extends AbstractBootstrap {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Bootstrap.class);
+ private static final NameResolverGroup> DEFAULT_RESOLVER = DefaultNameResolverGroup.INSTANCE;
+
+ @SuppressWarnings("unchecked")
+ private volatile NameResolverGroup resolver = (NameResolverGroup) DEFAULT_RESOLVER;
private volatile SocketAddress remoteAddress;
public Bootstrap() { }
@@ -51,6 +61,18 @@ public final class Bootstrap extends AbstractBootstrap {
remoteAddress = bootstrap.remoteAddress;
}
+ /**
+ * Sets the {@link NameResolver} which will resolve the address of the unresolved named address.
+ */
+ @SuppressWarnings("unchecked")
+ public Bootstrap resolver(NameResolverGroup> resolver) {
+ if (resolver == null) {
+ throw new NullPointerException("resolver");
+ }
+ this.resolver = (NameResolverGroup) resolver;
+ return this;
+ }
+
/**
* The {@link SocketAddress} to connect to once the {@link #connect()} method
* is called.
@@ -64,7 +86,7 @@ public final class Bootstrap extends AbstractBootstrap {
* @see {@link #remoteAddress(SocketAddress)}
*/
public Bootstrap remoteAddress(String inetHost, int inetPort) {
- remoteAddress = new InetSocketAddress(inetHost, inetPort);
+ remoteAddress = InetSocketAddress.createUnresolved(inetHost, inetPort);
return this;
}
@@ -86,14 +108,14 @@ public final class Bootstrap extends AbstractBootstrap {
throw new IllegalStateException("remoteAddress not set");
}
- return doConnect(remoteAddress, localAddress());
+ return doResolveAndConnect(remoteAddress, localAddress());
}
/**
* Connect a {@link Channel} to the remote peer.
*/
public ChannelFuture connect(String inetHost, int inetPort) {
- return connect(new InetSocketAddress(inetHost, inetPort));
+ return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
/**
@@ -112,7 +134,7 @@ public final class Bootstrap extends AbstractBootstrap {
}
validate();
- return doConnect(remoteAddress, localAddress());
+ return doResolveAndConnect(remoteAddress, localAddress());
}
/**
@@ -123,52 +145,94 @@ public final class Bootstrap extends AbstractBootstrap {
throw new NullPointerException("remoteAddress");
}
validate();
- return doConnect(remoteAddress, localAddress);
+ return doResolveAndConnect(remoteAddress, localAddress);
}
/**
* @see {@link #connect()}
*/
- private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
+ private ChannelFuture doResolveAndConnect(SocketAddress remoteAddress, final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
- final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
- final ChannelPromise promise = channel.newPromise();
+ final Channel channel = regFuture.channel();
+ final EventLoop eventLoop = channel.eventLoop();
+ final NameResolver resolver = this.resolver.getResolver(eventLoop);
+
+ if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
+ // Resolver has no idea about what to do with the specified remote address or it's resolved already.
+ return doConnect(remoteAddress, localAddress, regFuture, channel.newPromise());
+ }
+
+ final Future resolveFuture = resolver.resolve(remoteAddress);
+ final Throwable resolveFailureCause = resolveFuture.cause();
+
+ if (resolveFailureCause != null) {
+ // Failed to resolve immediately
+ channel.close();
+ return channel.newFailedFuture(resolveFailureCause);
+ }
+
+ if (resolveFuture.isDone()) {
+ // Succeeded to resolve immediately; cached? (or did a blocking lookup)
+ return doConnect(resolveFuture.getNow(), localAddress, regFuture, channel.newPromise());
+ }
+
+ // Wait until the name resolution is finished.
+ final ChannelPromise connectPromise = channel.newPromise();
+ resolveFuture.addListener(new FutureListener() {
+ @Override
+ public void operationComplete(Future future) throws Exception {
+ if (future.cause() != null) {
+ channel.close();
+ connectPromise.setFailure(future.cause());
+ } else {
+ doConnect(future.getNow(), localAddress, regFuture, connectPromise);
+ }
+ }
+ });
+
+ return connectPromise;
+ }
+
+ private static ChannelFuture doConnect(
+ final SocketAddress remoteAddress, final SocketAddress localAddress,
+ final ChannelFuture regFuture, final ChannelPromise connectPromise) {
if (regFuture.isDone()) {
- doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
+ doConnect0(remoteAddress, localAddress, regFuture, connectPromise);
} else {
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
- doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
+ doConnect0(remoteAddress, localAddress, regFuture, connectPromise);
}
});
}
- return promise;
+ return connectPromise;
}
private static void doConnect0(
- final ChannelFuture regFuture, final Channel channel,
- final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
+ final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelFuture regFuture,
+ final ChannelPromise connectPromise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
+ final Channel channel = connectPromise.channel();
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (regFuture.isSuccess()) {
if (localAddress == null) {
- channel.connect(remoteAddress, promise);
+ channel.connect(remoteAddress, connectPromise);
} else {
- channel.connect(remoteAddress, localAddress, promise);
+ channel.connect(remoteAddress, localAddress, connectPromise);
}
- promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
+ connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
- promise.setFailure(regFuture.cause());
+ connectPromise.setFailure(regFuture.cause());
}
}
});
diff --git a/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java b/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java
index 292eb6efa6..6d12b99864 100644
--- a/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java
+++ b/transport/src/main/java/io/netty/bootstrap/ChannelFactory.java
@@ -18,9 +18,9 @@ package io.netty.bootstrap;
import io.netty.channel.Channel;
/**
- * Factory that creates a new {@link Channel} on {@link Bootstrap#bind()}, {@link Bootstrap#connect()}, and
- * {@link ServerBootstrap#bind()}.
+ * @deprecated Use {@link io.netty.channel.ChannelFactory} instead.
*/
+@Deprecated
public interface ChannelFactory {
/**
* Creates a new channel.
diff --git a/transport/src/main/java/io/netty/channel/ChannelFactory.java b/transport/src/main/java/io/netty/channel/ChannelFactory.java
new file mode 100644
index 0000000000..dddea4eb45
--- /dev/null
+++ b/transport/src/main/java/io/netty/channel/ChannelFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.channel;
+
+/**
+ * Creates a new {@link Channel}.
+ */
+@SuppressWarnings({ "ClassNameSameAsAncestorName", "deprecation" })
+public interface ChannelFactory extends io.netty.bootstrap.ChannelFactory {
+ /**
+ * Creates a new channel.
+ */
+ @Override
+ T newChannel();
+}
diff --git a/transport/src/main/java/io/netty/channel/ReflectiveChannelFactory.java b/transport/src/main/java/io/netty/channel/ReflectiveChannelFactory.java
new file mode 100644
index 0000000000..0db431bbb0
--- /dev/null
+++ b/transport/src/main/java/io/netty/channel/ReflectiveChannelFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2014 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.netty.channel;
+
+import io.netty.util.internal.StringUtil;
+
+/**
+ * A {@link ChannelFactory} that instantiates a new {@link Channel} by invoking its default constructor reflectively.
+ */
+public class ReflectiveChannelFactory implements ChannelFactory {
+
+ private final Class extends T> clazz;
+
+ public ReflectiveChannelFactory(Class extends T> clazz) {
+ if (clazz == null) {
+ throw new NullPointerException("clazz");
+ }
+ this.clazz = clazz;
+ }
+
+ @Override
+ public T newChannel() {
+ try {
+ return clazz.newInstance();
+ } catch (Throwable t) {
+ throw new ChannelException("Unable to create Channel from class " + clazz, t);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return StringUtil.simpleClassName(clazz) + ".class";
+ }
+}
diff --git a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java
index 12964e3ae5..39d5864221 100644
--- a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java
+++ b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java
@@ -17,124 +17,124 @@
package io.netty.bootstrap;
import io.netty.channel.Channel;
+import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ServerChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
+import io.netty.resolver.NameResolver;
+import io.netty.resolver.NameResolverGroup;
+import io.netty.resolver.SimpleNameResolver;
+import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
-import org.junit.Assert;
+import io.netty.util.concurrent.Promise;
+import org.junit.AfterClass;
import org.junit.Test;
import java.net.SocketAddress;
import java.net.SocketException;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
public class BootstrapTest {
+ private static final EventLoopGroup groupA = new DefaultEventLoopGroup(1);
+ private static final EventLoopGroup groupB = new DefaultEventLoopGroup(1);
+ private static final ChannelInboundHandler dummyHandler = new DummyHandler();
+
+ @AfterClass
+ public static void destroy() {
+ groupA.shutdownGracefully();
+ groupB.shutdownGracefully();
+ groupA.terminationFuture().syncUninterruptibly();
+ groupB.terminationFuture().syncUninterruptibly();
+ }
+
@Test(timeout = 10000)
public void testBindDeadLock() throws Exception {
- EventLoopGroup groupA = new DefaultEventLoopGroup(1);
- EventLoopGroup groupB = new DefaultEventLoopGroup(1);
- try {
- ChannelInboundHandler dummyHandler = new DummyHandler();
+ final Bootstrap bootstrapA = new Bootstrap();
+ bootstrapA.group(groupA);
+ bootstrapA.channel(LocalChannel.class);
+ bootstrapA.handler(dummyHandler);
- final Bootstrap bootstrapA = new Bootstrap();
- bootstrapA.group(groupA);
- bootstrapA.channel(LocalChannel.class);
- bootstrapA.handler(dummyHandler);
+ final Bootstrap bootstrapB = new Bootstrap();
+ bootstrapB.group(groupB);
+ bootstrapB.channel(LocalChannel.class);
+ bootstrapB.handler(dummyHandler);
- final Bootstrap bootstrapB = new Bootstrap();
- bootstrapB.group(groupB);
- bootstrapB.channel(LocalChannel.class);
- bootstrapB.handler(dummyHandler);
+ List> bindFutures = new ArrayList>();
- List> bindFutures = new ArrayList>();
+ // Try to bind from each other.
+ for (int i = 0; i < 1024; i ++) {
+ bindFutures.add(groupA.next().submit(new Runnable() {
+ @Override
+ public void run() {
+ bootstrapB.bind(LocalAddress.ANY);
+ }
+ }));
- // Try to bind from each other.
- for (int i = 0; i < 1024; i ++) {
- bindFutures.add(groupA.next().submit(new Runnable() {
- @Override
- public void run() {
- bootstrapB.bind(LocalAddress.ANY);
- }
- }));
+ bindFutures.add(groupB.next().submit(new Runnable() {
+ @Override
+ public void run() {
+ bootstrapA.bind(LocalAddress.ANY);
+ }
+ }));
+ }
- bindFutures.add(groupB.next().submit(new Runnable() {
- @Override
- public void run() {
- bootstrapA.bind(LocalAddress.ANY);
- }
- }));
- }
-
- for (Future> f: bindFutures) {
- f.sync();
- }
- } finally {
- groupA.shutdownGracefully();
- groupB.shutdownGracefully();
- groupA.terminationFuture().sync();
- groupB.terminationFuture().sync();
+ for (Future> f: bindFutures) {
+ f.sync();
}
}
@Test(timeout = 10000)
public void testConnectDeadLock() throws Exception {
- EventLoopGroup groupA = new DefaultEventLoopGroup(1);
- EventLoopGroup groupB = new DefaultEventLoopGroup(1);
- try {
- ChannelInboundHandler dummyHandler = new DummyHandler();
+ final Bootstrap bootstrapA = new Bootstrap();
+ bootstrapA.group(groupA);
+ bootstrapA.channel(LocalChannel.class);
+ bootstrapA.handler(dummyHandler);
- final Bootstrap bootstrapA = new Bootstrap();
- bootstrapA.group(groupA);
- bootstrapA.channel(LocalChannel.class);
- bootstrapA.handler(dummyHandler);
+ final Bootstrap bootstrapB = new Bootstrap();
+ bootstrapB.group(groupB);
+ bootstrapB.channel(LocalChannel.class);
+ bootstrapB.handler(dummyHandler);
- final Bootstrap bootstrapB = new Bootstrap();
- bootstrapB.group(groupB);
- bootstrapB.channel(LocalChannel.class);
- bootstrapB.handler(dummyHandler);
+ List> bindFutures = new ArrayList>();
- List> bindFutures = new ArrayList>();
+ // Try to connect from each other.
+ for (int i = 0; i < 1024; i ++) {
+ bindFutures.add(groupA.next().submit(new Runnable() {
+ @Override
+ public void run() {
+ bootstrapB.connect(LocalAddress.ANY);
+ }
+ }));
- // Try to connect from each other.
- for (int i = 0; i < 1024; i ++) {
- bindFutures.add(groupA.next().submit(new Runnable() {
- @Override
- public void run() {
- bootstrapB.connect(LocalAddress.ANY);
- }
- }));
+ bindFutures.add(groupB.next().submit(new Runnable() {
+ @Override
+ public void run() {
+ bootstrapA.connect(LocalAddress.ANY);
+ }
+ }));
+ }
- bindFutures.add(groupB.next().submit(new Runnable() {
- @Override
- public void run() {
- bootstrapA.connect(LocalAddress.ANY);
- }
- }));
- }
-
- for (Future> f: bindFutures) {
- f.sync();
- }
- } finally {
- groupA.shutdownGracefully();
- groupB.shutdownGracefully();
- groupA.terminationFuture().sync();
- groupB.terminationFuture().sync();
+ for (Future> f: bindFutures) {
+ f.sync();
}
}
@@ -148,7 +148,7 @@ public class BootstrapTest {
bootstrap.childHandler(new DummyHandler());
bootstrap.localAddress(new LocalAddress("1"));
ChannelFuture future = bootstrap.bind();
- Assert.assertFalse(future.isDone());
+ assertFalse(future.isDone());
group.promise.setSuccess();
final BlockingQueue queue = new LinkedBlockingQueue();
future.addListener(new ChannelFutureListener() {
@@ -158,8 +158,8 @@ public class BootstrapTest {
queue.add(future.isSuccess());
}
});
- Assert.assertTrue(queue.take());
- Assert.assertTrue(queue.take());
+ assertTrue(queue.take());
+ assertTrue(queue.take());
} finally {
group.shutdownGracefully();
group.terminationFuture().sync();
@@ -197,7 +197,7 @@ public class BootstrapTest {
bootstrap.childHandler(new DummyHandler());
bootstrap.localAddress(new LocalAddress("1"));
ChannelFuture future = bootstrap.bind();
- Assert.assertFalse(future.isDone());
+ assertFalse(future.isDone());
group.promise.setSuccess();
final BlockingQueue queue = new LinkedBlockingQueue();
future.addListener(new ChannelFutureListener() {
@@ -207,16 +207,61 @@ public class BootstrapTest {
queue.add(future.isSuccess());
}
});
- Assert.assertTrue(queue.take());
- Assert.assertFalse(queue.take());
+ assertTrue(queue.take());
+ assertFalse(queue.take());
} finally {
group.shutdownGracefully();
group.terminationFuture().sync();
}
}
+ @Test
+ public void testAsyncResolutionSuccess() throws Exception {
+
+ final Bootstrap bootstrapA = new Bootstrap();
+ bootstrapA.group(groupA);
+ bootstrapA.channel(LocalChannel.class);
+ bootstrapA.resolver(new TestNameResolverGroup(true));
+ bootstrapA.handler(dummyHandler);
+
+ final ServerBootstrap bootstrapB = new ServerBootstrap();
+ bootstrapB.group(groupB);
+ bootstrapB.channel(LocalServerChannel.class);
+ bootstrapB.childHandler(dummyHandler);
+ SocketAddress localAddress = bootstrapB.bind(LocalAddress.ANY).sync().channel().localAddress();
+
+ // Connect to the server using the asynchronous resolver.
+ bootstrapA.connect(localAddress).sync();
+ }
+
+ @Test
+ public void testAsyncResolutionFailure() throws Exception {
+
+ final Bootstrap bootstrapA = new Bootstrap();
+ bootstrapA.group(groupA);
+ bootstrapA.channel(LocalChannel.class);
+ bootstrapA.resolver(new TestNameResolverGroup(false));
+ bootstrapA.handler(dummyHandler);
+
+ final ServerBootstrap bootstrapB = new ServerBootstrap();
+ bootstrapB.group(groupB);
+ bootstrapB.channel(LocalServerChannel.class);
+ bootstrapB.childHandler(dummyHandler);
+ SocketAddress localAddress = bootstrapB.bind(LocalAddress.ANY).sync().channel().localAddress();
+
+ // Connect to the server using the asynchronous resolver.
+ ChannelFuture connectFuture = bootstrapA.connect(localAddress);
+
+ // Should fail with the UnknownHostException.
+ assertThat(connectFuture.await(10000), is(true));
+ assertThat(connectFuture.cause(), is(instanceOf(UnknownHostException.class)));
+ assertThat(connectFuture.channel().isOpen(), is(false));
+ }
+
private static final class TestEventLoopGroup extends DefaultEventLoopGroup {
+
ChannelPromise promise;
+
TestEventLoopGroup() {
super(1);
}
@@ -236,4 +281,39 @@ public class BootstrapTest {
@Sharable
private static final class DummyHandler extends ChannelInboundHandlerAdapter { }
+
+ private static final class TestNameResolverGroup extends NameResolverGroup {
+
+ private final boolean success;
+
+ TestNameResolverGroup(boolean success) {
+ this.success = success;
+ }
+
+ @Override
+ protected NameResolver newResolver(EventExecutor executor) throws Exception {
+ return new SimpleNameResolver(executor) {
+
+ @Override
+ protected boolean doIsResolved(SocketAddress address) {
+ return false;
+ }
+
+ @Override
+ protected void doResolve(
+ final SocketAddress unresolvedAddress, final Promise promise) {
+ executor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (success) {
+ promise.setSuccess(unresolvedAddress);
+ } else {
+ promise.setFailure(new UnknownHostException());
+ }
+ }
+ });
+ }
+ };
+ }
+ }
}