From 999429c3171f362177ec626d2f88e9d13aabca51 Mon Sep 17 00:00:00 2001
From: Arturo Bernal
+ * This connection operator maintains a connection pool for each unique route (combination of target host and
+ * target port) and selects the next connection from the pool to establish a new connection or reuse an
+ * existing connection. The connection pool uses a First-In-First-Out (FIFO) queue and has a configurable limit
+ * on the maximum number of connections that can be kept alive in the pool. Once the maximum number of connections
+ * has been reached, the oldest connection in the pool is closed to make room for a new one.
+ *
+ * This class is thread-safe and can be used in a multi-threaded environment.
+ *
+ * The HEV2 algorithm is configurable through the following parameters:
+ *
+ *
+ *
+ * This class can be used with any {@link org.apache.hc.core5.http.nio.AsyncClientEndpoint} implementation + * that supports HTTP/1.1 or HTTP/2 protocols. + *
+ * + * @since 5.3 + */ +public class HappyEyeballsV2AsyncClientConnectionOperator implements AsyncClientConnectionOperator { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncClientConnectionOperator.class); + + + /** + * The default delay used between subsequent DNS resolution attempts, in milliseconds. + */ + private final Timeout DEFAULT_RESOLUTION_DELAY = Timeout.ofMilliseconds(50); + /** + * The default timeout duration for establishing a connection, in milliseconds. + */ + private final Timeout DEFAULT_TIMEOUT = Timeout.ofMilliseconds(250); + + /** + * The default minimum delay between connection attempts. + * This delay is used to prevent the connection operator from spamming connection attempts and to provide a reasonable + * delay between attempts for the user. + */ + private final Timeout DEFAULT_MINIMUM_CONNECTION_ATTEMPT_DELAY = Timeout.ofMilliseconds(100); + + /** + * The default maximum delay between connection attempts. + * This delay is used to prevent the connection operator from spamming connection attempts and to provide a reasonable + * delay between attempts for the user. This value is used to cap the delay between attempts to prevent the delay from becoming + * too long and causing unnecessary delays in the application's processing. + */ + private final Timeout DEFAULT_MAXIMUM_CONNECTION_ATTEMPT_DELAY = Timeout.ofMilliseconds(2000); + + /** + * The default delay before attempting to establish a connection. + * This delay is used to provide a reasonable amount of time for the underlying transport to be ready before attempting + * to establish a connection. This can help to improve the likelihood of successful connection attempts and reduce + * unnecessary delays in the application's processing. + */ + private final Timeout DEFAULT_CONNECTION_ATTEMPT_DELAY = Timeout.ofMilliseconds(250); + + + /** + * The {@link ScheduledExecutorService} used by this connection operator to execute delayed tasks, such as DNS resolution and connection attempts. + * This executor is used to control the timing of tasks in order to optimize the performance of connection attempts. By default, a single thread is used + * to execute tasks sequentially, but this can be adjusted depending on the application's workload and number of instances of the connection operator. + * If multiple instances of the connection operator are being used in the same application, it may be more efficient to use a {@link java.util.concurrent.ThreadPoolExecutor} + * with a fixed number of threads instead of a single thread executor. This will allow tasks to be executed in parallel, which can improve the overall + * performance of the application. + * If the scheduler provided to the constructor is null, a new instance of {@link Executors#newSingleThreadScheduledExecutor()} will be used as the default. + */ + private final ScheduledExecutorService scheduler; + + /** + * The underlying {@link AsyncClientConnectionOperator} that is used to establish connections + * to the target server. + */ + private final AsyncClientConnectionOperator connectionOperator; + + /** + * The DNS resolver used to resolve hostnames to IP addresses. + */ + private final DnsResolver dnsResolver; + + /** + * A lookup table used to determine the {@link TlsStrategy} to use for a given connection route. + */ + private final Lookup+ * connections using the Happy Eyeballs V2 algorithm. + * + *
+ * The Happy Eyeballs V2 algorithm allows for concurrent connection attempts to be made to different IP addresses, + *
+ * so this enum specifies whether connections should be attempted using IPv4 or IPv6 addresses. + * + *
+ */ + public enum AddressFamily { + IPv4, IPv6 + } + + /** + * Constructs a new {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters. + * + * @param tlsStrategyLookup the lookup object used to retrieve a {@link TlsStrategy} for a given {@link Route} + * @param connectionOperator the underlying {@link AsyncClientConnectionOperator} to use for establishing connections + * @param dnsResolver the {@link DnsResolver} to use for resolving target hostnames + * @param timeout the timeout duration for establishing a connection + * @param resolution_delay the configurable delay before subsequent DNS resolution attempts + * @param minimumConnectionAttemptDelay the minimum configurable delay between connection attempts + * @param maximumConnectionAttemptDelay the maximum configurable delay between connection attempts + * @param connectionAttemptDelay the configurable delay before attempting to establish a connection + * @param firstAddressFamilyCount the number of initial address families to use for establishing a connection + * @param addressFamily the preferred address family to use for establishing a connection + * @param scheduler the {@link ScheduledExecutorService} to use for scheduling tasks + * @throws IllegalArgumentException if {@code firstAddressFamilyCount} is not positive + */ + public HappyEyeballsV2AsyncClientConnectionOperator(final Lookup+ * The constructor internally creates a new instance of {@link DefaultAsyncClientConnectionOperator} with the + * specified {@link Lookup} for {@link TlsStrategy}, {@link SchemePortResolver} and {@link DnsResolver}. The + * created {@link AsyncClientConnectionOperator} is then passed to the main constructor along with default values + * for other parameters. + *
+ * + * @param tlsStrategyLookup The {@link Lookup} for {@link TlsStrategy}. + * @param schemePortResolver The {@link SchemePortResolver} to use for resolving scheme ports. + * @param dnsResolver The {@link DnsResolver} to use for resolving hostnames to IP addresses. + * @throws IllegalArgumentException if the {@code tlsStrategyLookup} or {@code schemePortResolver} or {@code dnsResolver} parameter is {@code null}. + */ + public HappyEyeballsV2AsyncClientConnectionOperator( + final Lookup
+ * attempt delay parameters.
+ *
+ * @param attemptIndex the index of the connection attempt, starting from 0
+ * @return the duration to wait before the next connection attempt
+ */
+ private Duration calculateDelay(final int attemptIndex) {
+ final Duration delay;
+ final Duration attemptDelay = connectionAttemptDelay.toDuration();
+ final Duration maximumAttemptDelay = maximumConnectionAttemptDelay.toDuration();
+ final Duration minimumAttemptDelay = minimumConnectionAttemptDelay.toDuration();
+
+ if (attemptIndex == 0) {
+ delay = attemptDelay;
+ } else {
+ delay = attemptDelay.multipliedBy(2).compareTo(maximumAttemptDelay) <= 0 ?
+ attemptDelay.multipliedBy(2) : maximumAttemptDelay;
+ }
+ return delay.compareTo(minimumAttemptDelay) >= 0 ? delay : minimumAttemptDelay;
+ }
+
+ /**
+ * Shuts down this operator and releases any system resources associated with it.
+ * If the operator is already shutdown then invoking this method has no effect.
+ */
+ public void shutdown() {
+ scheduler.shutdown();
+ }
+}
+
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java
new file mode 100644
index 000000000..c8c6f6d38
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/HappyEyeballsV2AsyncClientConnectionOperatorBuilder.java
@@ -0,0 +1,277 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ *
This builder provides a fluent API for configuring various options of the + * {@link HappyEyeballsV2AsyncClientConnectionOperator}. Once all the desired options have been set, + * the {@link #build()} method can be called to create an instance of the connection operator. + * + *
The following options can be configured using this builder: + *
If no options are explicitly set using this builder, default options will be used for each option. + * + *
This class is not thread-safe.
+ *
+ * @see HappyEyeballsV2AsyncClientConnectionOperator
+ * @since 5.3
+ */
+public class HappyEyeballsV2AsyncClientConnectionOperatorBuilder {
+
+ private AsyncClientConnectionOperator connectionOperator;
+ private DnsResolver dnsResolver;
+ private TlsStrategy tlsStrategy;
+ private Timeout timeout;
+ private Timeout minimumConnectionAttemptDelay;
+ private Timeout maximumConnectionAttemptDelay;
+ private Timeout connectionAttemptDelay;
+ private Timeout resolutionDelay;
+ private int firstAddressFamilyCount;
+ private HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamily;
+ private ScheduledExecutorService scheduler;
+
+
+ public static HappyEyeballsV2AsyncClientConnectionOperatorBuilder create() {
+ return new HappyEyeballsV2AsyncClientConnectionOperatorBuilder();
+ }
+
+
+ HappyEyeballsV2AsyncClientConnectionOperatorBuilder() {
+ super();
+ }
+
+
+ private boolean systemProperties;
+
+ /**
+ * Use system properties when creating and configuring default
+ * implementations.
+ */
+ public final HappyEyeballsV2AsyncClientConnectionOperatorBuilder useSystemProperties() {
+ this.systemProperties = true;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AsyncClientConnectionOperator} to use for establishing connections.
+ *
+ * @param connectionOperator the {@link AsyncClientConnectionOperator} to use
+ * @return this {@link HappyEyeballsV2AsyncClientConnectionOperatorBuilder} instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withConnectionOperator(
+ final AsyncClientConnectionOperator connectionOperator) {
+ this.connectionOperator = connectionOperator;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DnsResolver} to use for resolving host names to IP addresses.
+ *
+ * @param dnsResolver the {@link DnsResolver} to use
+ * @return this builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withDnsResolver(final DnsResolver dnsResolver) {
+ this.dnsResolver = dnsResolver;
+ return this;
+ }
+
+ /**
+ * Sets the {@link TlsStrategy} to use for creating TLS connections.
+ *
+ * @param tlsStrategy the {@link TlsStrategy} to use
+ * @return this {@link HappyEyeballsV2AsyncClientConnectionOperatorBuilder} instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withTlsStrategyLookup(final TlsStrategy tlsStrategy) {
+ this.tlsStrategy = tlsStrategy;
+ return this;
+ }
+
+ /**
+ * Set the timeout to use for connection attempts.
+ *
+ * @param timeout the timeout to use for connection attempts
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withTimeout(final Timeout timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Sets the minimum delay between connection attempts. The actual delay may be longer if a resolution delay has been
+ * specified, in which case the minimum connection attempt delay is added to the resolution delay.
+ *
+ * @param minimumConnectionAttemptDelay the minimum delay between connection attempts
+ * @return this builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withMinimumConnectionAttemptDelay(
+ final Timeout minimumConnectionAttemptDelay) {
+ this.minimumConnectionAttemptDelay = minimumConnectionAttemptDelay;
+ return this;
+ }
+
+ /**
+ * Sets the maximum delay between two connection attempts.
+ *
+ * @param maximumConnectionAttemptDelay the maximum delay between two connection attempts
+ * @return the builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withMaximumConnectionAttemptDelay(
+ final Timeout maximumConnectionAttemptDelay) {
+ this.maximumConnectionAttemptDelay = maximumConnectionAttemptDelay;
+ return this;
+ }
+
+ /**
+ * Sets the delay between two connection attempts.
+ *
+ * @param connectionAttemptDelay the delay between two connection attempts
+ * @return the builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withConnectionAttemptDelay(
+ final Timeout connectionAttemptDelay) {
+ this.connectionAttemptDelay = connectionAttemptDelay;
+ return this;
+ }
+
+ /**
+ * Sets the delay before attempting to resolve the next address in the list.
+ *
+ * @param resolutionDelay the delay before attempting to resolve the next address in the list
+ * @return the builder instance
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withResolutionDelay(final Timeout resolutionDelay) {
+ this.resolutionDelay = resolutionDelay;
+ return this;
+ }
+
+ /**
+ * Sets the number of first address families to try before falling back to the other address families.
+ *
+ * @param firstAddressFamilyCount the number of first address families to try
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withFirstAddressFamilyCount(
+ final int firstAddressFamilyCount) {
+ this.firstAddressFamilyCount = firstAddressFamilyCount;
+ return this;
+ }
+
+ /**
+ * Sets the preferred address family to use for connections.
+ *
+ * @param addressFamily the preferred address family
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withAddressFamily(final HappyEyeballsV2AsyncClientConnectionOperator.AddressFamily addressFamily) {
+ this.addressFamily = addressFamily;
+ return this;
+ }
+
+ /**
+ * Sets the {@link ScheduledExecutorService} for the {@link HappyEyeballsV2AsyncClientConnectionOperator}.
+ *
+ * @param scheduler The ScheduledExecutorService to set.
+ * @return this builder
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperatorBuilder withScheduler(final ScheduledExecutorService scheduler) {
+ this.scheduler = scheduler;
+ return this;
+ }
+
+ /**
+ * Builds a {@link HappyEyeballsV2AsyncClientConnectionOperator} with the specified parameters.
+ *
+ * @return the {@link HappyEyeballsV2AsyncClientConnectionOperator} instance built with the specified parameters.
+ * @throws IllegalArgumentException if the connection operator is null.
+ */
+ public HappyEyeballsV2AsyncClientConnectionOperator build() {
+ final TlsStrategy tlsStrategyCopy;
+ if (tlsStrategy != null) {
+ tlsStrategyCopy = tlsStrategy;
+ } else {
+ if (ReflectionUtils.determineJRELevel() <= 8 && ConscryptClientTlsStrategy.isSupported()) {
+ if (systemProperties) {
+ tlsStrategyCopy = ConscryptClientTlsStrategy.getSystemDefault();
+ } else {
+ tlsStrategyCopy = ConscryptClientTlsStrategy.getDefault();
+ }
+ } else {
+ if (systemProperties) {
+ tlsStrategyCopy = DefaultClientTlsStrategy.getSystemDefault();
+ } else {
+ tlsStrategyCopy = DefaultClientTlsStrategy.getDefault();
+ }
+ }
+ }
+
+
+ connectionOperator = Args.notNull(connectionOperator, "Connection operator");
+
+ return new HappyEyeballsV2AsyncClientConnectionOperator(
+ RegistryBuilder.
+ * The comparator is used to sort a list of IP addresses based on their reachability and preference.
+ *
+ *
+ * The Happy Eyeballs algorithm is a mechanism for reducing connection latency when connecting to IPv6-capable
+ *
+ * servers over networks where both IPv6 and IPv4 are available. The algorithm attempts to establish connections
+ *
+ * using IPv6 and IPv4 in parallel, and selects the first connection to complete successfully.
+ *
+ *
+ * This comparator implements the Happy Eyeballs V2 rules defined in RFC 8305. The following rules are used for
+ *
+ * comparing two IP addresses:
+ *
+ *
+ * bash
+ * Copy code
+ * Concurrency
+ * The method first orders the addresses based on their precedence, and then compares them based on other rules,
+ *
+ * including avoiding unusable destinations, preferring matching scope, preferring global scope, preferring
+ *
+ * IPv6 addresses, and preferring smaller address prefixes.
+ *
+ * @param addr1 the first address to be compared
+ * @param addr2 the second address to be compared
+ * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater
+ *
+ * than the second
+ */
+ @Override
+ public int compare(final InetAddress addr1, final InetAddress addr2) {
+ if (addr1 == null && addr2 == null) {
+ return 0;
+ }
+ if (addr1 == null) {
+ return -1;
+ }
+ if (addr2 == null) {
+ return 1;
+ }
+
+ // Rule 1: Avoid unusable destinations.
+ final boolean add1IsReachable;
+ final boolean add2IsReachable;
+ try {
+ add1IsReachable = addr1.isReachable(500);
+ } catch (final IOException e) {
+ return -1;
+ }
+ try {
+
+ add2IsReachable = addr2.isReachable(500);
+ } catch (final IOException e) {
+ return 1;
+ }
+
+ if (add1IsReachable && !add2IsReachable) {
+ return -1;
+ } else if (!add1IsReachable && add2IsReachable) {
+ return 1;
+ }
+
+
+ // Rule 2: Prefer matching scope.
+ final int addr1Scope = getScope(addr1);
+ final int addr2Scope = getScope(addr2);
+ final int srcScope;
+ try {
+ srcScope = getScope(getLocalAddress());
+ } catch (final IOException e) {
+ return 0;
+ }
+
+ if (addr1Scope == srcScope && addr2Scope != srcScope) {
+ return -1;
+ } else if (addr1Scope != srcScope && addr2Scope == srcScope) {
+ return 1;
+ }
+
+ //Rule 3: Avoid deprecated addresses.
+ final boolean add1IsDeprecated = isDeprecated(addr1);
+ final boolean add2IsDeprecated = isDeprecated(addr2);
+
+ if (add1IsDeprecated && !add2IsDeprecated) {
+ return 1;
+ } else if (!add1IsDeprecated && add2IsDeprecated) {
+ return -1;
+ }
+
+
+ // Rule 4: Prefer home addresses.
+ final boolean add1IsLocal = addr1.isLinkLocalAddress() || addr1.isSiteLocalAddress();
+ final boolean add2IsLocal = addr1.isLinkLocalAddress() || addr1.isSiteLocalAddress();
+
+ if (add1IsLocal && !add2IsLocal) {
+ return -1;
+ } else if (!add1IsLocal && add2IsLocal) {
+ return 1;
+ }
+
+ // Rule 5: Avoid deprecated addresses.
+ final String label1;
+ try {
+ label1 = getLabel(addr1);
+ } catch (final SocketException e) {
+ return -1;
+ }
+ final String label2;
+ try {
+ label2 = getLabel(addr2);
+ } catch (final SocketException e) {
+ return 1;
+ }
+
+ if (label1.equals(label2)) {
+ return 0;
+ } else if (label1.isEmpty()) {
+ return 1;
+ } else if (label2.isEmpty()) {
+ return -1;
+ }
+
+ // Rule 6 rule: Prefer the smaller address.
+ final int add1Precedence = getPrecedence(addr1);
+ final int add2Precedence = getPrecedence(addr2);
+
+ if (add1Precedence > add2Precedence) {
+ return -1;
+ } else if (add1Precedence < add2Precedence) {
+ return 1;
+ }
+
+ // Rule 7: Prefer native transport.
+ final boolean add1IsIPv4 = addr1 instanceof Inet4Address;
+ final boolean add2IsIPv4 = addr2 instanceof Inet4Address;
+
+ if (add1IsIPv4 && !add2IsIPv4) {
+ return -1;
+ } else if (!add1IsIPv4 && add2IsIPv4) {
+ return 1;
+ } else if (addr1 instanceof Inet6Address && addr2 instanceof Inet6Address) {
+ final Inet6Address ipv6Addr1 = (Inet6Address) addr1;
+ final Inet6Address ipv6Addr2 = (Inet6Address) addr2;
+
+ if (ipv6Addr1.isIPv4CompatibleAddress() && !ipv6Addr2.isIPv4CompatibleAddress()) {
+ return -1;
+ } else if (!ipv6Addr1.isIPv4CompatibleAddress() && ipv6Addr2.isIPv4CompatibleAddress()) {
+ return 1;
+ }
+ }
+
+
+ // Rule 8: Prefer smaller scope.
+ final int add1Scope = addr1 instanceof Inet6Address ? ((Inet6Address) addr1).getScopeId() : -1;
+ final int add2Scope = addr2 instanceof Inet6Address ? ((Inet6Address) addr2).getScopeId() : -1;
+
+ if (add1Scope < add2Scope) {
+ return -1;
+ } else if (add1Scope > add2Scope) {
+ return 1;
+ }
+
+ // Rule 9: Use longest matching prefix.
+ final int prefixLen1 = getMatchingPrefixLength(addr1, addr1);
+ final int prefixLen2 = getMatchingPrefixLength(addr2, addr1);
+
+ if (prefixLen1 > prefixLen2) {
+ return -1;
+ } else if (prefixLen1 < prefixLen2) {
+ return 1;
+ }
+
+
+ // Rule 9: Use longest matching prefix.
+ final byte[] ba1 = addr1.getAddress();
+ final byte[] ba2 = addr2.getAddress();
+ int prefixLen = 0;
+ for (int i = 0; i < ba1.length; i++) {
+ if (ba1[i] == ba2[i]) {
+ prefixLen += 8;
+ } else {
+ final int xor = ba1[i] ^ ba2[i];
+ // Count the number of leading zeroes in the XOR result
+ final int zeroes = Integer.numberOfLeadingZeros(xor) - 24;
+ prefixLen += zeroes;
+ break;
+ }
+ }
+ if (prefixLen == 128) {
+ return 0;
+ } else if ((ba1.length == 4 && prefixLen >= 24) || (ba1.length == 16 && prefixLen >= 64)) {
+ return 1;
+ } else if ((ba2.length == 4 && prefixLen >= 24) || (ba2.length == 16 && prefixLen >= 64)) {
+ return -1;
+ }
+
+
+ // Rule 10: Otherwise, leave the order unchanged.
+ return 0;
+ }
+
+
+ /**
+ * Returns the scope of the given address. For IPv6 addresses, this is the identifier of the
+ * scope the address is associated with. For IPv4 addresses, this always returns -1.
+ *
+ * @param address the address to get the scope for.
+ * @return the scope of the given address.
+ */
+ private int getScope(final InetAddress address) {
+ if (address instanceof Inet6Address) {
+ final Inet6Address ipv6Addr = (Inet6Address) address;
+ final int scope = ipv6Addr.getScopeId();
+ if (scope > 0) {
+ return scope;
+ }
+ }
+ return -1;
+ }
+
+
+ /**
+ * Returns the local address of the machine running this code. This method tries to use a UDP
+ * socket to connect to Google's DNS server at 8.8.8.8 on port 10002 to determine the local address.
+ * If that fails, it falls back to using the InetAddress.getLocalHost() method.
+ *
+ * @return the local address of the machine running this code.
+ * @throws IOException if there was an error determining the local address.
+ */
+ private InetAddress getLocalAddress() throws IOException {
+ InetAddress localAddress;
+
+ try (final DatagramSocket socket = new DatagramSocket()) {
+ socket.connect(InetAddress.getByName("8.8.8.8"), 10002);
+ localAddress = socket.getLocalAddress();
+ } catch (final SocketException e) {
+ // fallback to getLocalHost() method
+ localAddress = InetAddress.getLocalHost();
+ }
+
+ return localAddress;
+ }
+
+ /**
+ * Checks whether the given IPv6 address is a deprecated address.
+ *
+ * @param addr the IPv6 address to check.
+ * @return {@code true} if the given address is deprecated, {@code false} otherwise.
+ */
+ private boolean isDeprecated(final InetAddress addr) {
+ if (addr instanceof Inet4Address) {
+ return false;
+ } else if (addr instanceof Inet6Address) {
+ final Inet6Address ipv6Addr = (Inet6Address) addr;
+ final byte[] addressBytes = ipv6Addr.getAddress();
+
+ // Check if the IPv6 address is IPv4-mapped
+ if (addressBytes[0] == 0 && addressBytes[1] == 0 && addressBytes[2] == 0 && addressBytes[3] == 0 &&
+ addressBytes[4] == 0 && addressBytes[5] == 0 && addressBytes[6] == 0 && addressBytes[7] == 0 &&
+ addressBytes[8] == 0 && addressBytes[9] == 0 && addressBytes[10] == (byte) 0xFF && addressBytes[11] == (byte) 0xFF) {
+ return true;
+ }
+
+ // Check if the IPv6 address is a link-local address
+ if ((addressBytes[0] & 0xFF) == 0xFE && (addressBytes[1] & 0xC0) == 0x80) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Gets a label for the given InetAddress, used for sorting addresses.
+ * If the address is an IPv6 address, the label is the scope ID.
+ * If the address is an IPv4 address, the label is the name of the associated NetworkInterface.
+ * If the address is not associated with any NetworkInterface, the label is "default".
+ *
+ * @param addr the InetAddress to get the label for.
+ * @return the label for the given InetAddress.
+ * @throws SocketException if there is an error getting the NetworkInterface for the address.
+ */
+ private String getLabel(final InetAddress addr) throws SocketException {
+ if (addr instanceof Inet6Address) {
+ return ((Inet6Address) addr).getScopeId() + "";
+ } else {
+ final NetworkInterface netif = NetworkInterface.getByInetAddress(addr);
+ if (netif == null) {
+ // Address not associated with any network interface
+ return "default";
+ } else {
+ return netif.getName();
+ }
+ }
+ }
+
+ /**
+ * Returns the precedence of the given IP address based on its type and properties. The lower the
+ * precedence, the higher the priority.
+ *
+ * @param addr the IP address to evaluate.
+ * @return the precedence value.
+ */
+ private int getPrecedence(final InetAddress addr) {
+ if (addr instanceof Inet6Address) {
+ final byte[] addrBytes = addr.getAddress();
+ if (addrBytes[0] == (byte) 0xFF) {
+ return 1; // multicast
+ } else if (isIPv4MappedIPv6Address(addrBytes)) {
+ return 4; // IPv4-mapped IPv6 address
+ } else if (isULA(addrBytes)) {
+ return 5; // unique local address
+ } else if (isLinkLocal(addrBytes)) {
+ return 6; // link-local address
+ } else if (isSiteLocal(addrBytes)) {
+ return 7; // site-local address
+ } else {
+ return 3; // global address
+ }
+ } else {
+ return 2; // IPv4 address
+ }
+ }
+
+ /**
+ * Checks whether the given byte array represents an IPv4-mapped IPv6 address.
+ *
+ * @param addr the byte array representing the IP address
+ * @return {@code true} if the byte array represents an IPv4-mapped IPv6 address, {@code false} otherwise
+ */
+ private static boolean isIPv4MappedIPv6Address(final byte[] addr) {
+ return addr.length == 16 && addr[0] == 0x00 && addr[1] == 0x00 && addr[2] == 0x00
+ && addr[3] == 0x00 && addr[4] == 0x00 && addr[5] == 0x00 && addr[6] == 0x00
+ && addr[7] == 0x00 && addr[8] == 0x00 && addr[9] == 0x00 && addr[10] == (byte) 0xFF
+ && addr[11] == (byte) 0xFF;
+ }
+
+ /**
+ * Determines whether a given IPv6 address is a Unique Local Address (ULA) or not.
+ *
+ * @param addr the IPv6 address as a byte array
+ * @return true if the address is a ULA, false otherwise
+ */
+ private static boolean isULA(final byte[] addr) {
+ return addr.length == 16 && ((addr[0] & 0xFE) == (byte) 0xFC);
+ }
+
+ /**
+ * Determines whether a given IPv6 address is a Link-Local Address (LLA) or not.
+ *
+ * @param addr the IPv6 address as a byte array
+ * @return true if the address is a LLA, false otherwise
+ */
+ private boolean isLinkLocal(final byte[] addr) {
+ return addr.length == 16 && (addr[0] & 0xFF) == 0xFE && (addr[1] & 0xC0) == 0x80;
+ }
+
+ /**
+ * Determines whether a given IPv6 address is a Site-Local Address (SLA) or not.
+ *
+ * @param addr the IPv6 address as a byte array
+ * @return true if the address is a SLA, false otherwise
+ */
+ private boolean isSiteLocal(final byte[] addr) {
+ return addr.length == 16 && (addr[0] & 0xFF) == 0xFE && (addr[1] & 0xC0) == 0xC0;
+ }
+
+ /**
+ * Calculates the length of the matching prefix between two IP addresses.
+ *
+ * @param addr1 the first IP address to compare.
+ * @param addr2 the second IP address to compare.
+ * @return the length of the matching prefix between the two IP addresses.
+ */
+ private int getMatchingPrefixLength(final InetAddress addr1, final InetAddress addr2) {
+ final byte[] bytes1 = addr1.getAddress();
+ final byte[] bytes2 = addr2.getAddress();
+ final int len = Math.min(bytes1.length, bytes2.length);
+ int prefixLen = 0;
+ for (int i = 0; i < len; i++) {
+ int bits = 8;
+ for (int j = 7; j >= 0; j--) {
+ if ((bytes1[i] & (1 << j)) == (bytes2[i] & (1 << j))) {
+ prefixLen++;
+ } else {
+ bits--;
+ }
+ }
+ if (bits != 8) {
+ break;
+ }
+ }
+ return prefixLen;
+ }
+
+
+}
\ No newline at end of file
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2AsyncClientConnectionOperatorExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2AsyncClientConnectionOperatorExample.java
new file mode 100644
index 000000000..507c7c484
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/HappyEyeballsV2AsyncClientConnectionOperatorExample.java
@@ -0,0 +1,219 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ *
+ *
+ *
+ * @see RFC 8305 - Happy Eyeballs Version 2: Better Connectivity Using
+ *