Android SDK接入

前言

本章节介绍HTTPDNS Android SDK的接入方法。

  • 推荐使用Gradle管理依赖的Android Studio项目。

  • 支持Android 4.0及以上版本。

  • targetSdkVersion支持到Android 14(34)。

  • 支持arm64-v8a/armeabi-v7a/x86/x86_64架构。

SDK已开源,如有特殊需求,可以自行更改源码进行使用,请参见httpdns-android-sdk

准备工作

第一步:将SDK添加到您的应用

我们提供了Maven依赖和本地依赖两种集成方式,方便您根据需要将SDK添加到您的应用中。

说明

建议开发者采用Maven依赖方式进行集成,配置简单,不容易出问题,后续更新方便。

1 Maven依赖方式

1.1 配置Maven仓库

下面分别介绍Gradle 7.0及以上推荐的dependencyResolutionManagement配置方式和Gradle 7.0之下推荐的allprojects配置方式。

1.1.1 dependencyResolutionManagement方式

在您的根级(项目级)Gradle 文件(<project>/settings.gradle)中,在dependencyResolutionManagementrepositories中添加Maven仓库地址。

dependencyResolutionManagement {
  repositories {
    maven {
      url 'https://maven.aliyun.com/nexus/content/repositories/releases/'
    }
  }
}
1.1.2 allprojects方式

在您的根级(项目级)Gradle 文件(<project>/build.gradle)中,在allprojectsrepositories中添加Maven仓库地址。

allprojects {
  repositories {
    maven {
      url 'https://maven.aliyun.com/nexus/content/repositories/releases/'
    }
  }
}

1.2 添加SDK依赖

在您的模块(应用级)Gradle 文件(通常是<project>/<app-module>/build.gradle)中,在dependencies中添加SDK依赖。

dependencies {
  implementation 'com.aliyun.ams:alicloud-android-httpdns:${httpdnsVersion}'
}
重要

httpdnsVersion请从Android SDK发布说明中获取。

2 本地依赖方式

2.1 下载SDK

EMAS SDK列表选择HTTPDNS进行下载,将SDK包内所有文件拷贝至您的模块(应用级)<project>/<app-module>/libs目录下。

2.2 添加SDK依赖

2.2.1 配置本地SDK目录

在您的模块(应用级)Gradle 文件(通常是<project>/<app-module>/build.gradle)中,添加本地SDK文件目录地址。

repositories {
  flatDir {
    dirs 'libs'
  }
}

2.2.2 添加SDK依赖

在您的模块(应用级)Gradle 文件(通常是<project>/<app-module>/build.gradle)中,的dependencies中添加SDK依赖。

dependencies {
  implementation (name: 'alicloud-android-httpdns-${httpdnsVersion}', ext: 'aar')
  implementation (name: 'alicloud-android-logger-${loggerVersion}', ext: 'aar')
  implementation (name: 'alicloud-android-crashdefend-${crashDefendVersion}', ext: 'jar')
  implementation (name: 'alicloud-android-ipdetector-${ipdetectorVersion}', ext: 'aar')
  implementation (name: 'alicloud-android-utdid-${utdidVersion}', ext: 'jar')
}
重要
  • 示例依赖中的SDK版本号请根据下载产物的文件名中的版本号为准。

  • 如果编译报类冲突,请确认dependencies下是否已经有 implementation fileTree(dir: 'libs', include: ['*.jar'])

第二步:配置使用SDK

1 配置HTTPDNS

//初始化配置,调用即可,不必处理返回值。
InitConfig config = InitConfig.Builder()
    // 配置初始的region
    .setRegion(currentRegion)
    // 配置是否启用https,默认http
    .setEnableHttps(enableHttps)
    // 配置服务请求的超时时长,毫秒,默认2秒,最大5秒
    .setTimeout(2 * 1000)
    // 配置是否启用本地缓存,默认不启用
    .setEnableCacheIp(true)
    // 配置是否允许返回过期IP,默认允许
    .setEnableExpiredIp(true)
    // 配置ipv4探测域名
    .setIPRankingList(ipRankingItemJson.toIPRankingList())
    // 配置接口来自定义缓存的ttl时间
    .configCacheTtlChanger(ttlChanger)
    // 配置固定IP的域名列表,优化SDK的内部逻辑,减少解析频次
    .configHostWithFixedIp(hostListWithFixedIp)
    // 配置不使用HttpDns解析的域名策略
    .setNotUseHttpDnsFilter(notUseHttpDnsFilter)
    // 针对哪一个account配置
    .build()

HttpDns.init(accountID, config);
//初始化配置,调用即可,不必处理返回值。
new InitConfig.Builder()
    // 配置初始的region
    .setRegion(currentRegion)
    // 配置是否启用https,默认http
    .setEnableHttps(enableHttps)
    // 配置服务请求的超时时长,毫秒,默认2秒,最大5秒
    .setTimeout(2 * 1000)
    // 配置是否启用本地缓存,默认不启用
    .setEnableCacheIp(true)
    // 配置是否允许返回过期IP,默认允许
    .setEnableExpiredIp(true)
    // 配置ipv4探测域名
    .setIPRankingList(list)
    // 配置接口来自定义缓存的ttl时间
    .configCacheTtlChanger(ttlChanger)
    // 配置固定IP的域名列表,优化SDK的内部逻辑,减少解析频次
    .configHostWithFixedIp(hostListWithFixedIp)
    // 配置不使用HttpDns解析的域名策略
    .setNotUseHttpDnsFilter(notUseHttpDnsFilter)
    // 针对哪一个account配置
    .buildFor(accountID);
重要
  • 如果初始化的时候setEnableHttps没有设置成true,需要在应用级的AndroidManifest.xml文件下的application节点下添加配置android:usesCleartextTraffic="true",否则域名解析请求在高版本系统上会失败。

具体配置接口请查看基础配置接口高阶配置接口

2 获取服务实例

HTTPDNS Android SDK以全局service实例的方式提供域名解析服务,您可以通过以下方式获取实例。

2.1 普通方式

val httpdns = HttpDns.getService(applicationContext, accountID)
HttpDnsService httpdns = HttpDns.getService(applicationContext, accountID);

2.2 鉴权方式

val httpdns = HttpDns.getService(applicationContext, accountID, secretKey)
HttpDnsService httpdns = HttpDns.getService(applicationContext, accountID, secretKey);
说明
  • HTTPDNS SDK V1.1.3及以上版本支持鉴权方式,该方式需要在HTTPDNS控制台进行关闭非鉴权接口(请谨慎操作)。详情请参见鉴权解析接口

  • 鉴权默认过期时间为10分钟。

  • 为避免在日志中泄漏参数accountID/secretKey或App运行过程中产生的数据,建议线上版本关闭SDK调试日志。

  • 由于所有用户使用统一的SDK接入,在接入过程中需要在代码中设accountID/secretKey参数,而此类参数与计量计费密切相关,为防止恶意反编译获取参数造成信息泄漏,建议您开启混淆,并进行App加固后再发布上线。

3 进行域名解析

HTTPDNS提供了多种域名解析方式,包括预解析/同步解析/异步解析/同步非阻塞解析。下面以同步解析接口作为例子。

val httpDnsResult = dnsService?.getHttpDnsResultForHostSync("www.aliyun.com", RequestIpType.auto)
HTTPDNSResult httpDnsResult = httpdns.getHttpDnsResultForHostSync("www.aliyun.com", RequestIpType.auto);

请根据您的实际使用场景选择合适的域名解析接口。具体接口请查看域名解析接口自定义解析接口

4 使用域名解析结果

上一步解析成功后,可以拿到域名解析结果,数据结构请查看HTTPDNSResult

此处以okhttp网络库的解析过程为例,示例代码如下:

object : Dns {
    @Throws(UnknownHostException::class)
    override fun lookup(host: String): List<InetAddress> {
        val httpdnsResult: HTTPDNSResult = HttpDns.getService(context)
            .getHttpDnsResultForHostSync(host, RequestIpType.auto)
        val inetAddresses: MutableList<InetAddress> = ArrayList()
        var address: InetAddress
        try {
            if (httpdnsResult.ips != null) {
                //处理IPv4地址
                for (ipv4 in httpdnsResult.ips) {
                    address = InetAddress.getByName(ipv4)
                    inetAddresses.add(address)
                }
            }
            if (httpdnsResult.ipv6s != null) {
                //处理IPv6地址
                for (ipv6 in httpdnsResult.ipv6s) {
                    address = InetAddress.getByName(ipv6)
                    inetAddresses.add(address)
                }
            }
        } catch (e: UnknownHostException) {
        }
        return if (!inetAddresses.isEmpty()) {
            inetAddresses
        } else Dns.SYSTEM.lookup(host)
    }
}
new Dns() {
    @Override
    public List<InetAddress> lookup(String host) throws UnknownHostException {
        HTTPDNSResult httpdnsResult = HttpDns.getService(context, accountID).getHttpDnsResultForHostSync(host, RequestIpType.auto);
        List<InetAddress> inetAddresses = new ArrayList<>();
        InetAddress address;
        try {
            if (httpdnsResult.getIps() != null) {
                //处理IPv4地址
                for (String ipv4 : httpdnsResult.getIps()) {
                    address = InetAddress.getByName(ipv4);
                    inetAddresses.add(address);
                }
            }

            if (httpdnsResult.getIpv6s() != null) {
                //处理IPv6地址
                for (String ipv6 : httpdnsResult.getIpv6s()) {
                    address = InetAddress.getByName(ipv6);
                    inetAddresses.add(address);
                }
            }
        } catch (UnknownHostException e) {

        }

        if (!inetAddresses.isEmpty()) {
            return inetAddresses;
        }

        return Dns.SYSTEM.lookup(host);
    }
};

5 混淆配置

如果您的项目做了代码混淆,请保留以下:4 混淆配置

-keep class com.alibaba.sdk.android.**{*;}

第三步:接入验证

1 打开SDK日志

按照是否允许HTTPDNS打印Log打开HTTPDNS Sdk的日志。

2 分析日志

# 发起同步请求
D  sync request host www.aliyun.com with type both extras : null cacheKey null
# 缓存查询结果
D  host www.aliyun.com result in cache is null
# 触发云端解析
I  sync start request for www.aliyun.com both
D  ip detector type is 3
D  ipdetector type is both
D  start resolve ip request for www.aliyun.com both
# 解析超时时长
D  the httpDnsConfig timeout is: 2000
D  final timeout is: 2000
D  wait for request finish
# 发起解析请求
D  request url http://xx.xx.xx.xx:80/xxxx/sign_d?host=www.aliyun.com&sdk=android_2.4.0&query=4,6&sid=CaZk1vTyI3hy&s=b4a34694b7215b4cd6a10376b3425a8e&t=1715772172
# 解析成功
D  request success {"ipsv6":[],"host":"www.aliyun.com","ips":[],"ttl":60,"origin_ttl":60}
# 更新缓存
D  save host www.aliyun.com for type v4 ttl is 60
D  save host www.aliyun.com for type v6 ttl is 60
D  sync resolve time is: 224
# 返回解析结果
I  request host www.aliyun.com for both and return host:www.aliyun.com, ips:[], ipv6s:[], extras:{}, expired:false, fromDB:false after request

请参考日志,分析域名解析是否成功。

注意事项

  1. 升级SDK后编译失败

    如果您升级SDK版本后,出现编译失败的情况,有可能是因为SDK的API有调整,请查看Android SDK API使用替换的新接口。

  2. 务必编写降级代码

    降级代码指的是当HTTPDNS无法获取期望结果时,需要降级使用Local Dns去完成域名解析。

  3. 记录从HTTPDNS获取的IP及sessionId

    我们提供了用于解析问题排查的解决方案,需要您将从HTTPDNS获取的IP及sessionId记录到日志中,详情请参见如何使用“会话追踪方案”排查解析异常

  4. 设置HTTP请求头HOST字段

    标准的HTTP协议中服务端会将HTTP请求头HOST字段的值作为请求的域名信息进行解析。

    使用HTTPDNS后,您可能需要将HTTP请求URL中的HOST字段替换为HTTPDNS解析获得的IP,这时标准的网络库会将您的IP赋值给HTTP请求头的HOST字段,进而导致服务端的解析异常(服务端认可的是您的域名信息,而非IP信息)。

    为了解决这个问题,您可以主动设置HTTP请求HOST字段的值,如:

    val originalUrl = "http://www.aliyun.com/"
    var url = URL(originalUrl)
    val originalHost = url.host
    // 同步非阻塞方式获取IP
    val httpdns = HttpDns.getService(applicationContext, accountID)
    val result: HTTPDNSResult =
        httpdns.getHttpDnsResultForHostSyncNonBlocking(originalHost, RequestIpType.auto)
    val conn: HttpURLConnection
    if (result.ips != null && result.ips.isNotEmpty()) {
        // 通过HTTPDNS获取IPv4成功,进行URL替换和HOST头设置
        url = URL(originalUrl.replaceFirst(originalHost.toRegex(), result.ips[0]))
        conn = url.openConnection() as HttpURLConnection
        // 设置请求HOST字段
        conn.setRequestProperty("Host", originalHost)
    } else if (result.ipv6s != null && result.ipv6s.isNotEmpty()) {
        // 通过HTTPDNS获取IPv4成功,进行URL替换和HOST头设置
        url = URL(originalUrl.replaceFirst(originalHost.toRegex(), result.ipv6s[0]))
        conn = url.openConnection() as HttpURLConnection
        // 设置请求HOST字段
        conn.setRequestProperty("Host", originalHost)
    } else {
        conn = url.openConnection() as HttpURLConnection
    }
    String originalUrl = "http://www.aliyun.com/";
    URL url = new URL(originalUrl);
    String originalHost = url.getHost();
    // 同步非阻塞方式获取IP
    HttpDnsService httpdns = HttpDns.getService(applicationContext, accountID);
    HTTPDNSResult result = httpdns.getHttpDnsResultForHostSyncNonBlocking(originalHost, RequestIpType.auto);
    HttpURLConnection conn;
    if (result.getIps() != null && result.getIps().length > 0) {
      // 通过HTTPDNS获取IPv4成功,进行URL替换和HOST头设置
      url = new URL(originalUrl.replaceFirst(originalHost, result.getIps()[0]));
      conn = (HttpURLConnection) url.openConnection();
      // 设置请求HOST字段
      conn.setRequestProperty("Host", originalHost);
    } else if (result.getIpv6s() != null && result.getIpv6s().length > 0) {
      // 通过HTTPDNS获取IPv4成功,进行URL替换和HOST头设置
      url = new URL(originalUrl.replaceFirst(originalHost, result.getIpv6s()[0]));
      conn = (HttpURLConnection) url.openConnection();
      // 设置请求HOST字段
      conn.setRequestProperty("Host", originalHost);
    } else {
      conn = (HttpURLConnection) url.openConnection();
    }
  5. Cookie字段

    部分网络库支持Cookie的自动存储管理,当您使用HTTPDNS进行IP URL请求时,部分网络库会将您URL中的IP信息作为Cookie对应的域名信息进行存储管理(而非HTTP请求头HOST字段信息),进而造成Cookie管理与使用上的困扰,因此您需要关闭Cookie的自动管理功能(默认关闭)。

  6. HTTPS/WebView/SNI场景

    1. HTTPS场景,参考Android端HTTPS(含SNI)业务场景:IP直连方案

    2. WebView场景,参考Android端HTTPDNS+Webview最佳实践

  7. 代理情况下的使用

    当存在中间HTTP代理时,客户端发起的请求中请求行会使用绝对路径的URL,在您开启HTTPDNS并采用IP URL进行访问时,中间代理将识别您的IP信息并将其作为真实访问的HOST信息传递给目标服务器,这时目标服务器将无法处理这类无真实HOST信息的HTTP请求。

    移动网关提供了X-Online-Host的私有协议字段来解决这个问题,比如:

    目标URL:http://www.example.com/product/oss/
    通过HTTPDNS解析出来的www.example.com的IP:192.168.XX.XX
    代理:10.0.XX.XX:XX
    您的HTTP请求头:
    GET http://192.168.XX.XX/product/oss/ HTTP/1.1     # 通过代理发起的HTTP请求头,请求行是一个绝对路径
    Host: www.example.com                         # 这个Header会被代理网关忽略,代理网关会使用请求行绝对路径中的host字段作为源站的host,即192.168.XX.XX
    X-Online-Host: www.example.com                # 这个Header就是移动网关为了传递真实Host添加的私有头部,源站需要配置识别该私有头部以获取真实的Host信息

    同样您可以通过setRequestProperty方法进行X-Online-Host请求头域的设置,并在服务端设置对该私有头域的解析。

    说明

    在绝大多数场景下,我们建议您在代理模式下关闭HTTPDNS功能。

集成常见问题

UTDID冲突,可参考:阿里云-云产品SDK UTDID冲突解决方案