前言

本文转自feshfans的博客 Android 在 4G 下访问 IPV6 慢的解决方案,仅用于记录解决方式,如有侵权,烦请告知。


起因

因公司iOS上架应用必须要使用ipv6协议,服务端这边新增了ipv6协议支持。
Android刚开始在wifi网络环境下访问一切正常,后来在切换到4G网络状态下时,发下每次请求的时间相对之前都延长了好多,经过搜索参阅上文后,发现是每次解析都会收到两个ip地址(ipv6、ipv4),ipv6总是排在前面,会被优先使用ipv6地址访问解析,所以决定采用文中方案,使用自定义DNS方式将ipv4优先级放在第一位,杜绝此类问题的发生。

这里盗用一下排查图

验证方式

使用下面的代码,验证DNS解析到的IP地址:

try {
    InetAddress[] inetAddresses = InetAddress.getAllByName("server.xxxx.cn");
    for (InetAddress inetAddress : inetAddresses) {
        Log.d(TAG, inetAddress.getHostAddress());
    }
} catch (UnknownHostException e) {
    e.printStackTrace();
}

解决方案

通过上面的验证,基本断定为 4G 网络下,Android 端通过 ipv6 连接的服务地址。客户端的Retrofit是基于okHttp3为核心实现的网络请求 ,在查看 javadoc 后,发现其提供了 DNS 接口,代码如下:

/**
 * A domain name service that resolves IP addresses for host names. Most applications will use the
 * {@linkplain #SYSTEM system DNS service}, which is the default. Some applications may provide
 * their own implementation to use a different DNS server, to prefer IPv6 addresses, to prefer IPv4
 * addresses, or to force a specific known IP address.
 *
 * <p>Implementations of this interface must be safe for concurrent use.
 */
public interface Dns {
  /**
   * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
   * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
   */
  Dns SYSTEM = hostname -> {
    if (hostname == null) throw new UnknownHostException("hostname == null");
    try {
      return Arrays.asList(InetAddress.getAllByName(hostname));
    } catch (NullPointerException e) {
      UnknownHostException unknownHostException =
          new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
      unknownHostException.initCause(e);
      throw unknownHostException;
    }
  };

  /**
   * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp. If
   * a connection to an address fails, OkHttp will retry the connection with the next address until
   * either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded.
   */
  List<InetAddress> lookup(String hostname) throws UnknownHostException;
}

下面我们通过实现此接口,将解析到的 ip 顺序调整一下,如果是 ipv4 则将其放到数据的第一个,其它保持不变,如下图:

package com.dohenes.common.data.remote;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import okhttp3.Dns;
import retrofit2.internal.EverythingIsNonNull;

/**
 * ClassName Ipv4Dns
 * Describe TODO<ipv4访问优先的DNS--解决ipv6环境下,4G网络访问太慢的问题>
 * Author zihao
 * Date 2020/2/27 14:59
 * Version v1.0
 */
@EverythingIsNonNull
public class Ipv4PriorityDns implements Dns {

    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        try {
            // 获取指定主机-hostname的IP地址数组(Android解析指定域名获取到的IP)
            List<InetAddress> inetAddressList = new ArrayList<>();
            InetAddress[] inetAddresses = InetAddress.getAllByName(hostname);

            // 遍历获取到的地址,并将得到的ip顺序调整一下,使ipv4放在第一位(即优先使用ipv4协议访问)
            for (InetAddress inetAddress : inetAddresses) {
                if (inetAddress instanceof Inet4Address) {
                    inetAddressList.add(0, inetAddress);
                } else {
                    inetAddressList.add(inetAddress);
                }
            }

            return inetAddressList;
        } catch (NullPointerException exception) {
            UnknownHostException unknownHostException = new
                    UnknownHostException("Broken system behavior");
            unknownHostException.initCause(exception);
            throw unknownHostException;
        }
    }
}

然后设置Retrofit的OkHttpClient,修改其DNS解析类:

OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
okHttpClientBuilder.dns(new MyDns());
OkHttpClient okHttpClient = okHttpClientBuilder.build();