Android SDK接入
前言
本章節(jié)介紹HTTPDNS Android SDK的接入方法。
推薦使用Gradle管理依賴的Android Studio項(xiàng)目。
支持Android 4.0及以上版本。
targetSdkVersion支持到Android 14(34)。
支持arm64-v8a/armeabi-v7a/x86/x86_64架構(gòu)。
SDK已開源,如有特殊需求,可以自行更改源碼進(jìn)行使用,請(qǐng)參見httpdns-android-sdk。
準(zhǔn)備工作
已創(chuàng)建項(xiàng)目和應(yīng)用。具體操作請(qǐng)參見創(chuàng)建項(xiàng)目和應(yīng)用。
第一步:將SDK添加到您的應(yīng)用
我們提供了Maven依賴和本地依賴兩種集成方式,方便您根據(jù)需要將SDK添加到您的應(yīng)用中。
建議開發(fā)者采用Maven依賴方式進(jìn)行集成,配置簡(jiǎn)單,不容易出問題,后續(xù)更新方便。
1 Maven依賴方式
1.1 配置Maven倉(cāng)庫(kù)
下面分別介紹Gradle 7.0及以上推薦的dependencyResolutionManagement
配置方式和Gradle 7.0之下推薦的allprojects
配置方式。
1.1.1 dependencyResolutionManagement方式
在您的根級(jí)(項(xiàng)目級(jí))Gradle 文件(<project>/settings.gradle
)中,在dependencyResolutionManagement
的repositories
中添加Maven倉(cāng)庫(kù)地址。
dependencyResolutionManagement {
repositories {
maven {
url 'https://maven.aliyun.com/nexus/content/repositories/releases/'
}
}
}
1.1.2 allprojects方式
在您的根級(jí)(項(xiàng)目級(jí))Gradle 文件(<project>/build.gradle
)中,在allprojects
的repositories
中添加Maven倉(cāng)庫(kù)地址。
allprojects {
repositories {
maven {
url 'https://maven.aliyun.com/nexus/content/repositories/releases/'
}
}
}
1.2 添加SDK依賴
在您的模塊(應(yīng)用級(jí))Gradle 文件(通常是<project>/<app-module>/build.gradle
)中,在dependencies
中添加SDK依賴。
dependencies {
implementation 'com.aliyun.ams:alicloud-android-httpdns:${httpdnsVersion}'
}
httpdnsVersion
請(qǐng)從Android SDK發(fā)布說明中獲取。
2 本地依賴方式
2.1 下載SDK
從EMAS SDK列表選擇HTTPDNS進(jìn)行下載,將SDK包內(nèi)所有文件拷貝至您的模塊(應(yīng)用級(jí))的<project>/<app-module>/libs
目錄下。
2.2 添加SDK依賴
2.2.1 配置本地SDK目錄
在您的模塊(應(yīng)用級(jí))Gradle 文件(通常是<project>/<app-module>/build.gradle
)中,添加本地SDK文件目錄地址。
repositories {
flatDir {
dirs 'libs'
}
}
2.2.2 添加SDK依賴
在您的模塊(應(yīng)用級(jí))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版本號(hào)請(qǐng)根據(jù)下載產(chǎn)物的文件名中的版本號(hào)為準(zhǔn)。
如果編譯報(bào)類沖突,請(qǐng)確認(rèn)dependencies下是否已經(jīng)有
implementation fileTree(dir: 'libs', include: ['*.jar'])
第二步:配置使用SDK
1 配置HTTPDNS
//初始化配置,調(diào)用即可,不必處理返回值。
InitConfig config = InitConfig.Builder()
// 配置初始的region
.setRegion(currentRegion)
// 配置是否啟用https,默認(rèn)http
.setEnableHttps(enableHttps)
// 配置服務(wù)請(qǐng)求的超時(shí)時(shí)長(zhǎng),毫秒,默認(rèn)2秒,最大5秒
.setTimeout(2 * 1000)
// 配置是否啟用本地緩存,默認(rèn)不啟用
.setEnableCacheIp(true)
// 配置是否允許返回過期IP,默認(rèn)允許
.setEnableExpiredIp(true)
// 配置ipv4探測(cè)域名
.setIPRankingList(ipRankingItemJson.toIPRankingList())
// 配置接口來自定義緩存的ttl時(shí)間
.configCacheTtlChanger(ttlChanger)
// 配置固定IP的域名列表,優(yōu)化SDK的內(nèi)部邏輯,減少解析頻次
.configHostWithFixedIp(hostListWithFixedIp)
// 配置不使用HttpDns解析的域名策略
.setNotUseHttpDnsFilter(notUseHttpDnsFilter)
// 針對(duì)哪一個(gè)account配置
.build()
HttpDns.init(accountID, config);
//初始化配置,調(diào)用即可,不必處理返回值。
new InitConfig.Builder()
// 配置初始的region
.setRegion(currentRegion)
// 配置是否啟用https,默認(rèn)http
.setEnableHttps(enableHttps)
// 配置服務(wù)請(qǐng)求的超時(shí)時(shí)長(zhǎng),毫秒,默認(rèn)2秒,最大5秒
.setTimeout(2 * 1000)
// 配置是否啟用本地緩存,默認(rèn)不啟用
.setEnableCacheIp(true)
// 配置是否允許返回過期IP,默認(rèn)允許
.setEnableExpiredIp(true)
// 配置ipv4探測(cè)域名
.setIPRankingList(list)
// 配置接口來自定義緩存的ttl時(shí)間
.configCacheTtlChanger(ttlChanger)
// 配置固定IP的域名列表,優(yōu)化SDK的內(nèi)部邏輯,減少解析頻次
.configHostWithFixedIp(hostListWithFixedIp)
// 配置不使用HttpDns解析的域名策略
.setNotUseHttpDnsFilter(notUseHttpDnsFilter)
// 針對(duì)哪一個(gè)account配置
.buildFor(accountID);
如果初始化的時(shí)候
setEnableHttps
沒有設(shè)置成true
,需要在應(yīng)用級(jí)的AndroidManifest.xml
文件下的application
節(jié)點(diǎn)下添加配置android:usesCleartextTraffic="true"
,否則域名解析請(qǐng)求在高版本系統(tǒng)上會(huì)失敗。
具體配置接口請(qǐng)查看基礎(chǔ)配置接口和高階配置接口。
2 獲取服務(wù)實(shí)例
HTTPDNS Android SDK以全局service實(shí)例的方式提供域名解析服務(wù),您可以通過以下方式獲取實(shí)例。
2.1 普通方式
val httpdns = HttpDns.getService(applicationContext, accountID)
HttpDnsService httpdns = HttpDns.getService(applicationContext, accountID);
2.2 鑒權(quán)方式
val httpdns = HttpDns.getService(applicationContext, accountID, secretKey)
HttpDnsService httpdns = HttpDns.getService(applicationContext, accountID, secretKey);
HTTPDNS SDK V1.1.3及以上版本支持鑒權(quán)方式,該方式需要在HTTPDNS控制臺(tái)進(jìn)行關(guān)閉非鑒權(quán)接口(請(qǐng)謹(jǐn)慎操作)。詳情請(qǐng)參見鑒權(quán)解析接口。
鑒權(quán)默認(rèn)過期時(shí)間為10分鐘。
為避免在日志中泄漏參數(shù)accountID/secretKey或App運(yùn)行過程中產(chǎn)生的數(shù)據(jù),建議線上版本關(guān)閉SDK調(diào)試日志。
由于所有用戶使用統(tǒng)一的SDK接入,在接入過程中需要在代碼中設(shè)accountID/secretKey參數(shù),而此類參數(shù)與計(jì)量計(jì)費(fèi)密切相關(guān),為防止惡意反編譯獲取參數(shù)造成信息泄漏,建議您開啟混淆,并進(jìn)行App加固后再發(fā)布上線。
3 進(jìn)行域名解析
HTTPDNS提供了多種域名解析方式,包括預(yù)解析/同步解析/異步解析/同步非阻塞解析。下面以同步解析接口作為例子。
val httpDnsResult = dnsService?.getHttpDnsResultForHostSync("www.aliyun.com", RequestIpType.auto)
HTTPDNSResult httpDnsResult = httpdns.getHttpDnsResultForHostSync("www.aliyun.com", RequestIpType.auto);
請(qǐng)根據(jù)您的實(shí)際使用場(chǎng)景選擇合適的域名解析接口。具體接口請(qǐng)查看域名解析接口和自定義解析接口。
4 使用域名解析結(jié)果
上一步解析成功后,可以拿到域名解析結(jié)果,數(shù)據(jù)結(jié)構(gòu)請(qǐng)查看HTTPDNSResult。
此處以okhttp網(wǎng)絡(luò)庫(kù)的解析過程為例,示例代碼如下:
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 混淆配置
如果您的項(xiàng)目做了代碼混淆,請(qǐng)保留以下:4 混淆配置
-keep class com.alibaba.sdk.android.**{*;}
第三步:接入驗(yàn)證
1 打開SDK日志
按照是否允許HTTPDNS打印Log打開HTTPDNS Sdk的日志。
2 分析日志
# 發(fā)起同步請(qǐng)求
D sync request host www.aliyun.com with type both extras : null cacheKey null
# 緩存查詢結(jié)果
D host www.aliyun.com result in cache is null
# 觸發(fā)云端解析
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
# 解析超時(shí)時(shí)長(zhǎng)
D the httpDnsConfig timeout is: 2000
D final timeout is: 2000
D wait for request finish
# 發(fā)起解析請(qǐng)求
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
# 返回解析結(jié)果
I request host www.aliyun.com for both and return host:www.aliyun.com, ips:[], ipv6s:[], extras:{}, expired:false, fromDB:false after request
請(qǐng)參考日志,分析域名解析是否成功。
注意事項(xiàng)
升級(jí)SDK后編譯失敗
如果您升級(jí)SDK版本后,出現(xiàn)編譯失敗的情況,有可能是因?yàn)镾DK的API有調(diào)整,請(qǐng)查看Android SDK API使用替換的新接口。
務(wù)必編寫降級(jí)代碼
降級(jí)代碼指的是當(dāng)HTTPDNS無法獲取期望結(jié)果時(shí),需要降級(jí)使用Local Dns去完成域名解析。
記錄從HTTPDNS獲取的IP及sessionId
我們提供了用于解析問題排查的解決方案,需要您將從HTTPDNS獲取的IP及sessionId記錄到日志中,詳情請(qǐng)參見如何使用“會(huì)話追蹤方案”排查解析異常。
設(shè)置HTTP請(qǐng)求頭HOST字段
標(biāo)準(zhǔn)的HTTP協(xié)議中服務(wù)端會(huì)將HTTP請(qǐng)求頭HOST字段的值作為請(qǐng)求的域名信息進(jìn)行解析。
使用HTTPDNS后,您可能需要將HTTP請(qǐng)求URL中的HOST字段替換為HTTPDNS解析獲得的IP,這時(shí)標(biāo)準(zhǔn)的網(wǎng)絡(luò)庫(kù)會(huì)將您的IP賦值給HTTP請(qǐng)求頭的HOST字段,進(jìn)而導(dǎo)致服務(wù)端的解析異常(服務(wù)端認(rèn)可的是您的域名信息,而非IP信息)。
為了解決這個(gè)問題,您可以主動(dòng)設(shè)置HTTP請(qǐng)求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成功,進(jìn)行URL替換和HOST頭設(shè)置 url = URL(originalUrl.replaceFirst(originalHost.toRegex(), result.ips[0])) conn = url.openConnection() as HttpURLConnection // 設(shè)置請(qǐng)求HOST字段 conn.setRequestProperty("Host", originalHost) } else if (result.ipv6s != null && result.ipv6s.isNotEmpty()) { // 通過HTTPDNS獲取IPv4成功,進(jìn)行URL替換和HOST頭設(shè)置 url = URL(originalUrl.replaceFirst(originalHost.toRegex(), result.ipv6s[0])) conn = url.openConnection() as HttpURLConnection // 設(shè)置請(qǐng)求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成功,進(jìn)行URL替換和HOST頭設(shè)置 url = new URL(originalUrl.replaceFirst(originalHost, result.getIps()[0])); conn = (HttpURLConnection) url.openConnection(); // 設(shè)置請(qǐng)求HOST字段 conn.setRequestProperty("Host", originalHost); } else if (result.getIpv6s() != null && result.getIpv6s().length > 0) { // 通過HTTPDNS獲取IPv4成功,進(jìn)行URL替換和HOST頭設(shè)置 url = new URL(originalUrl.replaceFirst(originalHost, result.getIpv6s()[0])); conn = (HttpURLConnection) url.openConnection(); // 設(shè)置請(qǐng)求HOST字段 conn.setRequestProperty("Host", originalHost); } else { conn = (HttpURLConnection) url.openConnection(); }
Cookie字段
部分網(wǎng)絡(luò)庫(kù)支持Cookie的自動(dòng)存儲(chǔ)管理,當(dāng)您使用HTTPDNS進(jìn)行IP URL請(qǐng)求時(shí),部分網(wǎng)絡(luò)庫(kù)會(huì)將您URL中的IP信息作為Cookie對(duì)應(yīng)的域名信息進(jìn)行存儲(chǔ)管理(而非HTTP請(qǐng)求頭HOST字段信息),進(jìn)而造成Cookie管理與使用上的困擾,因此您需要關(guān)閉Cookie的自動(dòng)管理功能(默認(rèn)關(guān)閉)。
HTTPS/WebView/SNI場(chǎng)景
HTTPS場(chǎng)景,參考Android端HTTPS(含SNI)業(yè)務(wù)場(chǎng)景:IP直連方案。
WebView場(chǎng)景,參考Android端HTTPDNS+Webview最佳實(shí)踐。
代理情況下的使用
當(dāng)存在中間HTTP代理時(shí),客戶端發(fā)起的請(qǐng)求中請(qǐng)求行會(huì)使用絕對(duì)路徑的URL,在您開啟HTTPDNS并采用IP URL進(jìn)行訪問時(shí),中間代理將識(shí)別您的IP信息并將其作為真實(shí)訪問的HOST信息傳遞給目標(biāo)服務(wù)器,這時(shí)目標(biāo)服務(wù)器將無法處理這類無真實(shí)HOST信息的HTTP請(qǐng)求。
移動(dòng)網(wǎng)關(guān)提供了
X-Online-Host
的私有協(xié)議字段來解決這個(gè)問題,比如:目標(biāo)URL:http://www.example.com/product/oss/ 通過HTTPDNS解析出來的www.example.com的IP:192.168.XX.XX 代理:10.0.XX.XX:XX 您的HTTP請(qǐng)求頭: GET http://192.168.XX.XX/product/oss/ HTTP/1.1 # 通過代理發(fā)起的HTTP請(qǐng)求頭,請(qǐng)求行是一個(gè)絕對(duì)路徑 Host: www.example.com # 這個(gè)Header會(huì)被代理網(wǎng)關(guān)忽略,代理網(wǎng)關(guān)會(huì)使用請(qǐng)求行絕對(duì)路徑中的host字段作為源站的host,即192.168.XX.XX X-Online-Host: www.example.com # 這個(gè)Header就是移動(dòng)網(wǎng)關(guān)為了傳遞真實(shí)Host添加的私有頭部,源站需要配置識(shí)別該私有頭部以獲取真實(shí)的Host信息
同樣您可以通過
setRequestProperty
方法進(jìn)行X-Online-Host
請(qǐng)求頭域的設(shè)置,并在服務(wù)端設(shè)置對(duì)該私有頭域的解析。說明在絕大多數(shù)場(chǎng)景下,我們建議您在代理模式下關(guān)閉HTTPDNS功能。
集成常見問題
UTDID沖突,可參考:阿里云-云產(chǎn)品SDK UTDID沖突解決方案