data) {
42 | this.data = data;
43 | }
44 |
45 | public static class DataBean {
46 |
47 | /**
48 | * id : 1001
49 | * animate : 1
50 | * duration : 3
51 | * start_time : 1515254400
52 | * end_time : 1515340740
53 | * image : http://i0.hdslb.com/bfs/archive/fedf1d0b2a88b7f33cfbea31877ed122d75d360c.jpg
54 | * key : 3a28f7cc6f8eb95e8fe92f795e9e6c01
55 | * times : 5
56 | * type : 1
57 | * param : https://bangumi.bilibili.com/anime/21421
58 | * skip : 1
59 | */
60 |
61 | private int id;
62 | private int animate;
63 | private int duration;
64 | private int start_time;
65 | private int end_time;
66 | private String image;
67 | private String key;
68 | private int times;
69 | private int type;
70 | private String param;
71 | private int skip;
72 |
73 | public int getId() {
74 | return id;
75 | }
76 |
77 | public void setId(int id) {
78 | this.id = id;
79 | }
80 |
81 | public int getAnimate() {
82 | return animate;
83 | }
84 |
85 | public void setAnimate(int animate) {
86 | this.animate = animate;
87 | }
88 |
89 | public int getDuration() {
90 | return duration;
91 | }
92 |
93 | public void setDuration(int duration) {
94 | this.duration = duration;
95 | }
96 |
97 | public int getStart_time() {
98 | return start_time;
99 | }
100 |
101 | public void setStart_time(int start_time) {
102 | this.start_time = start_time;
103 | }
104 |
105 | public int getEnd_time() {
106 | return end_time;
107 | }
108 |
109 | public void setEnd_time(int end_time) {
110 | this.end_time = end_time;
111 | }
112 |
113 | public String getImage() {
114 | return image;
115 | }
116 |
117 | public void setImage(String image) {
118 | this.image = image;
119 | }
120 |
121 | public String getKey() {
122 | return key;
123 | }
124 |
125 | public void setKey(String key) {
126 | this.key = key;
127 | }
128 |
129 | public int getTimes() {
130 | return times;
131 | }
132 |
133 | public void setTimes(int times) {
134 | this.times = times;
135 | }
136 |
137 | public int getType() {
138 | return type;
139 | }
140 |
141 | public void setType(int type) {
142 | this.type = type;
143 | }
144 |
145 | public String getParam() {
146 | return param;
147 | }
148 |
149 | public void setParam(String param) {
150 | this.param = param;
151 | }
152 |
153 | public int getSkip() {
154 | return skip;
155 | }
156 |
157 | public void setSkip(int skip) {
158 | this.skip = skip;
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # http dns
2 |
3 | > 当移动app遇到区域性出现 `unsolved host name xxx` 的时候才会想起来吧。。。
4 |
5 | 使用HTTP请求和HTTPDNS服务,(在高可用情况下)替代传统DNS协议,解析DNS。
6 |
7 | **大部分内容节选至以下参考文章**:
8 |
9 | [移动互联网时代,如何优化你的网络 —— 域名解析篇](https://yq.aliyun.com/articles/58967?spm=5176.doc30102.2.4.Hv1CZk)
10 |
11 | [【鹅厂网事】全局精确流量调度新思路-HttpDNS服务详解](https://mp.weixin.qq.com/s?__biz=MzA3ODgyNzcwMw==&mid=201837080&idx=1&sn=b2a152b84df1c7dbd294ea66037cf262&scene=2&from=timeline&isappinstalled=0&utm_source=tuicool)
12 |
13 | [腾讯云:移动解析HttpDNS](https://cloud.tencent.com/document/product/379/3523)
14 |
15 | ## Todo List
16 |
17 | - [ ] 同步请求自动重试
18 | - [ ] 网络切换更新缓存
19 | - [ ] 允许HTTP DNS返回TTL过期的域名
20 | - [ ] 使用代理情况
21 |
22 |
23 | # 基础概念
24 |
25 | DNS:
26 |
27 | ISP:
28 |
29 | localDNS:
30 |
31 | IDC:
32 |
33 | ICP:
34 |
35 | GSLB: 全局负载均衡
36 |
37 | DHCP:
38 |
39 | NAT:
40 |
41 | 根域、顶级域、二级域:
42 |
43 | 权威DNS:
44 |
45 | 递归DNS:
46 |
47 | 
48 |
49 | 公共DNS:
50 |
51 | HTTPS
52 |
53 |
54 | # 传统的域名解析面临的问题
55 |
56 | 问题根源:ISP的LocalDNS解析域名异常
57 |
58 | ## 域名缓存
59 |
60 | LocalDNS缓存了腾讯的域名的解析结果,不向腾讯权威DNS发起递归。
61 |
62 | 
63 |
64 | 为何LocalDNS要把域名解析结果进行缓存呢?原因有以下几个:
65 |
66 | - 保证用户访问流量在本网内消化:国内的各互联网接入运营商的带宽资源、网间结算费用、IDC机房分布、网内ICP资源分布等存在较大差异。为了保证网内用户的访问质量,同时减少跨网结算,运营商在网内搭建了内容缓存服务器,通过把域名强行指向内容缓存服务器的IP地址,就实现了把本地本网流量完全留在了本地的目的。
67 |
68 | - 推送广告:有部分LocalDNS会把部分域名解析结果的所指向的内容缓存,并替换成第三方广告联盟的广告。
69 |
70 | 这种类型的行为就是我们常说的域名缓存,域名缓存会导致用户产生以下的访问异常:
71 |
72 | - A. 仅对80端口的http服务做了缓存,如果域名是通过https协议或其它端口提供服务的,用户访问就会出现失败。比如支付服务、游戏通过指定端口连接connect server服务等。
73 |
74 | - B. 缓存服务器的运维水平参差不齐,时有出现缓存服务器故障导致用户访问异常的问题。
75 |
76 | ## 解析转发
77 |
78 | 除了域名缓存以外,运营商的LocalDNS还存在解析转发的现象。解析转发是指运营商自身不进行域名递归解析,而是把域名解析请求转发到其它运营商的递归DNS上的行为。正常的LocalDNS递归解析过程是这样的:
79 |
80 | 
81 |
82 | 而部分小运营商为了节省资源,就直接将解析请求转发到了其它运营的递归LocalDNS上去了
83 |
84 | 
85 |
86 | 这样的直接后果就是腾讯权威DNS收到的域名解析请求的来源IP就成了其它运营商的IP,最终导致用户流量被导向了错误的IDC,用户访问变慢。
87 |
88 | ## LocalDNS递归出口NAT
89 |
90 | LocalDNS递归出口NAT指的是运营商的LocalDNS按照标准的DNS协议进行递归,但是因为在网络上存在多出口且配置了目标路由NAT,结果导致LocalDNS最终进行递归解析的时候的出口IP就有概率不为本网的IP地址
91 |
92 | 
93 |
94 | 这样的直接后果就是GSLB DNS收到的域名解析请求的来源IP还是成了其它运营商的IP,最终导致用户流量被导向了错误的IDC,用户访问变慢。
95 |
96 | ## 解析生效滞后
97 |
98 | 部分业务场景下开发者对域名解析结果变更的生效时间非常敏感(这部分变更操作是开发者在权威DNS上完成的),比如当业务服务器受到攻击时,我们需要最快速地将业务IP切换到另一组集群上,这样的诉求在传统域名解析体系下是无法完成的。
99 |
100 | Local DNS的部署是由各个地区的各个运营商独立部署的,因此各个Local DNS的服务质量参差不齐。在对域名解析缓存的处理上,各个独立节点的实现策略也有区别,比如部分节点为了节省开支忽略了域名解析结果的TTL时间限制,导致用户在权威DNS变更的解析结果全网生效的周期非常漫长(我们已知的最长生效时间甚至高达48小时)。这类延迟生效可能直接导致用户业务访问的异常。
101 |
102 |
103 |
104 | # HTTPDNS
105 |
106 | ## 基本原理
107 |
108 | HttpDNS是为移动客户端量身定做的基于Http协议和域名解析的流量调度解决方案,专治LocalDNS解析异常以及流量调度不准。详细介绍如下:
109 |
110 | 
111 |
112 | HttpDNS的原理非常简单,主要有两步:
113 |
114 | 1. 客户端直接访问HttpDNS接口,获取业务在域名配置管理系统上配置的访问延迟最优的IP。(基于容灾考虑,还是保留次选使用运营商LocalDNS解析域名的方式)
115 |
116 | 2. 客户端向获取到的IP后就向直接往此IP发送业务协议请求。以Http请求为例,通过在header中指定host字段,向HttpDNS返回的IP发送标准的Http请求即可。
117 |
118 | ## 优点
119 |
120 | [`SLA 服务级别协议`](https://zh.wikipedia.org/zh-hans/%E6%9C%8D%E5%8A%A1%E7%BA%A7%E5%88%AB%E5%8D%8F%E8%AE%AE) 保证
121 |
122 | ## 缺点
123 |
124 | 延迟:HTTP DNS基于TCP协议请求响应时间更长,但可以使用缓存解析结果、预加载、懒加载等方式,异步处理httpDNS的请求时间。
125 |
126 | # 实践方案
127 |
128 | 移动APP的域名解析机制,新的流程参考如下:
129 |
130 | 
131 |
132 |
133 | 需要注意的是,发起网络请求时,在本地无缓存,或缓存已过期的情况下,直接使用localDNS解析,并同时异步更新本地DNS缓存。
134 |
135 | ## Failed over策略「降级」
136 |
137 | 虽然HttpDNS已经接入BGP Anycast,并实现了多地跨机房容灾。但为了保证在最坏的情况下客户端域名解析依然不受影响。建议采用以下的fail over策略:
138 |
139 | - 第一步先向HttpDNS发起域名查询请求
140 | - 如果HttpDNS查询返回的结果不是一个IP地址(结果为空、结果非IP、连接超时等),则通过本地 LocalDNS进行域名解析。超时时间建议为5s。
141 |
142 | 不管是因为什么原因,当通过HTTPDNS服务无法获得域名对应的IP时,都必须降级:使用标准的DNS解析,通过Local DNS去解析域名。
143 |
144 | ## 缓存策略
145 |
146 | 移动互联网用户的网络环境比较复杂,为了尽可能地减少由于域名解析导致的延迟,建议在本地进行缓存。缓存规则如下:
147 |
148 | - 缓存时间
149 | 缓存时间建议设置为120s至600s,不可低于60s。
150 |
151 | - 缓存更新
152 | 缓存更新应在以下两种情形下进行:
153 | **用户网络状态发生变化时**:
154 | 移动互联网的用户的网络状态由3G切Wi-Fi,Wi-Fi切3G的情况下,其接入点的网络归属可能发生变化。所以用户的网络状态发生变化时,需要重新向HttpDNS发起域名解析请求,以获得用户当前网络归属下的最优指向。
155 |
156 | **缓存过期时**:
157 | 当域名解析的结果缓存时间到期时,客户端应该向HttpDNS重新发起域名解析请求以获取最新的域名对应的IP。为了减少用户在缓存过期后重新进行域名解析时的等待时间,建议在75%TTL时就开始进行域名解析。如本地缓存的TTL为600s,那么在第600*0.75=450s时刻,客户端就应该进行域名解析。
158 |
159 |
160 | 除了以上几点建议外,减少域名解析的次数也能有效的减少网络交互,提升用户访问体验。建议在业务允许的情况下,尽量减少域名的数量。如需区分不同的资源,建议通过url来进行区分。
161 |
162 | ## 异步请求、懒加载
163 |
164 | 异步请求策略:解析域名时,如果当前缓存中有TTL未过期的IP,可直接使用;如果没有,则立刻让此次请求降级走原生LocalDNS解析,同时另起线程异步地发起HTTPDNS请求进行解析,更新缓存,这样后续解析域名时就能命中缓存。
165 |
166 | 1. 查询注册的DNS解析列表,若未注册返回null
167 | 2. 查询缓存,若存在且未过期则返回结果,若不存在返回null并且进行异步域名解析更新缓存。
168 | 3. 若接口返回null,降级到local dns解析策略。
169 |
170 | ## 重试
171 |
172 | 访问HTTPDNS服务解析域名时,如果请求HTTPDNS服务端失败,即HTTP请求没有返回,可以进行重试。
173 | 大部分情况下,这种访问失败是由于网络原因引起的,重试可以解决。
174 |
175 |
176 | ## 预解析
177 |
178 | 在初始化阶段针对业务的热点域名在后台发起异步的HTTPDNS解析请求。这部分预解析结果在后续的业务请求中可以直接使用,进而消除首次业务请求的DNS解析开销,提升APP首页的加载速度。
179 |
180 |
181 | ## HTTPS情况
182 |
183 | [HTTPS(含SNI)业务场景“IP直连”方案说明](https://help.aliyun.com/document_detail/30143.html?spm=5176.doc30140.6.562.9duHZH)
184 |
185 | ## webview
186 |
187 | [Android Webview + HttpDns最佳实践](https://help.aliyun.com/document_detail/60181.html?spm=5176.doc30144.6.565.1qauiM)
188 |
189 | ## 代理情况
190 |
191 | 当存在中间HTTP代理时,客户端发起的请求中请求行会使用绝对路径的URL,在开启HTTPDNS并采用IP URL进行访问时,中间代理将识别您的IP信息并将其作为真实访问的HOST信息传递给目标服务器,这时目标服务器将无法处理这类无真实HOST信息的HTTP请求。
192 |
193 | 绝大多数场景下,在代理模式下关闭HTTPDNS功能。
194 |
195 | ## 注意事项
196 |
197 | 1. 设置的缓存TTL值不可太低(不可低于60s),防止频繁进行HtppDNS请求。
198 | 2. 接入HttpDNS的业务需要保留用户本地LocalDNS作为容灾通道,当HttpDNS无法正常服务时(移动网络不稳定或HttpDNS服务出现问题),可以使用LocalDNS进行解析。
199 | 3. https问题,需在客户端hook客户端检查证书的domain域和扩展域看是否包含本次请求的host的过程,将IP直接替换成原来的域名,再执行证书验证。或者忽略证书认证,类似于curl -k参数。
200 |
201 | 4. HttpDNS请求建议超时时间2-5s左右。
202 | 5. 在网络类型变化时,如4G切换到wifi,不同wifi间切换等,需要重新执行HttpDNS请求刷新本地缓存。
203 |
--------------------------------------------------------------------------------
/app/src/main/java/io/github/haohaozaici/httpdns/network/httpdns/HttpDnsCache.java:
--------------------------------------------------------------------------------
1 | package io.github.haohaozaici.httpdns.network.httpdns;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.util.Log;
5 |
6 | import com.google.gson.Gson;
7 |
8 | import java.io.IOException;
9 | import java.util.HashMap;
10 | import java.util.Map;
11 | import java.util.concurrent.TimeUnit;
12 |
13 | import okhttp3.Call;
14 | import okhttp3.Callback;
15 | import okhttp3.OkHttpClient;
16 | import okhttp3.Request;
17 | import okhttp3.Response;
18 |
19 | import static io.github.haohaozaici.httpdns.network.httpdns.HttpDnsRes.DnsBean;
20 |
21 | /**
22 | * Created by haoyuan on 2018/1/23.
23 | *
24 | * accountID 错误返回404
25 | * host错误返回empty ips
26 | *
27 | * todo 1.同步请求自动重试
28 | * todo 2.网络切换更新缓存
29 | * todo 3.允许HTTP DNS返回TTL过期的域名
30 | * todo 4.使用代理情况
31 | * todo 5.webview
32 | */
33 |
34 | public class HttpDnsCache {
35 |
36 | private static HttpDnsCache instance;
37 |
38 | private HttpDnsCache() {
39 |
40 | }
41 |
42 | public static synchronized HttpDnsCache getInstance() {
43 | if (instance == null) {
44 | instance = new HttpDnsCache();
45 | }
46 | return instance;
47 | }
48 |
49 | private static final String TAG = "HttpDnsCache";
50 |
51 | private String[] mHosts;
52 | private Map mDnsCaches = new HashMap<>();
53 |
54 | private OkHttpClient client;
55 |
56 | private String HttpDnsServerIp = "203.107.1.33";
57 | private int account_id = 191607;
58 |
59 | private static final int RETRY_TIMES = 2;
60 | private int retryTimes = 0;
61 |
62 | private boolean preLoadSync = false;
63 |
64 |
65 | /**
66 | * 初始化accountID,域名解析列表
67 | *
68 | * @param accountID ali dns accountID
69 | * @param hosts httpDNS 解析域名列表
70 | */
71 | public void init(int accountID, String... hosts) {
72 | account_id = accountID;
73 | mHosts = hosts;
74 |
75 | client = new OkHttpClient.Builder()
76 | .connectTimeout(2, TimeUnit.SECONDS)
77 | .readTimeout(2, TimeUnit.SECONDS)
78 | .build();
79 | }
80 |
81 |
82 | /**
83 | * 首次同步加载
84 | *
85 | * maxSize = 5
86 | */
87 | public boolean loadDnsCache() {
88 |
89 | if (mHosts == null || mHosts.length == 0)
90 | throw new NullPointerException("preload dns, hostname can not be null or empty");
91 |
92 | //选择http 或 https
93 | StringBuilder http_dns_url = new StringBuilder("http://" + HttpDnsServerIp + "/" + account_id);
94 |
95 | if (mHosts.length == 1) {
96 | http_dns_url.append("/d?host=").append(mHosts[0]);
97 | } else {
98 | http_dns_url.append("/resolve?host=");
99 | for (String hostname : mHosts) {
100 | http_dns_url.append(hostname).append(",");
101 | }
102 | http_dns_url.deleteCharAt(http_dns_url.length() - 1);
103 | }
104 |
105 | Request request = new Request.Builder()
106 | .url(http_dns_url.toString())
107 | .build();
108 |
109 | if (preLoadSync) {
110 | return requestCacheSync(request);
111 | } else {
112 | requestCache(request);
113 | return true;
114 | }
115 |
116 | }
117 |
118 | private boolean requestCacheSync(Request request) {
119 |
120 | try (Response response = client.newCall(request).execute()) {
121 | return handleResponse(request, response);
122 |
123 | } catch (IOException | NullPointerException e) {
124 | e.printStackTrace();
125 | Log.d(TAG, "requestCacheSync: " + e.getMessage());
126 | return false;
127 | }
128 | }
129 |
130 | private boolean handleResponse(Request request, Response response) throws IOException {
131 | if (response.isSuccessful()) {
132 | String res = response.body().string();
133 |
134 | if (request.url().encodedPath().contains("d")) {
135 | DnsBean dnsBean = new Gson().fromJson(res, DnsBean.class);
136 | saveCache(dnsBean);
137 | } else if (request.url().encodedPath().contains("resolve")) {
138 | HttpDnsRes dnsRes = new Gson().fromJson(res, HttpDnsRes.class);
139 | if (dnsRes.getDns().size() == 0)
140 | throw new NullPointerException("dns resolved return null");
141 |
142 | for (DnsBean bean : dnsRes.getDns()) {
143 | saveCache(bean);
144 | }
145 | }
146 |
147 | return true;
148 | } else {
149 | return false;
150 | }
151 | }
152 |
153 | /**
154 | * 异步解析接口
155 | * 1. 查询注册的DNS解析列表,若未注册返回null
156 | * 2. 查询缓存,若存在且未过期则返回结果,若不存在返回null并且进行异步域名解析更新缓存。
157 | * 3. 若接口返回null,,为避免影响业务请降级到local dns解析策略。
158 | *
159 | * @param hostname 域名(如www.aliyun.com)
160 | * @return 域名对应的解析结果
161 | */
162 | public String getIpByHostAsync(String hostname) {
163 |
164 | if (mHosts == null || mHosts.length == 0) return null;
165 |
166 | for (String host : mHosts) {
167 | if (host.equals(hostname)) {
168 | if (mDnsCaches == null || mDnsCaches.isEmpty()) {
169 | updateIpByHostAsync(hostname);
170 | //无缓存
171 | return null;
172 | } else {
173 | DnsBean dnsBean = mDnsCaches.get(hostname);
174 | if (dnsBean != null) {
175 | long timeSpan = Math.abs(System.currentTimeMillis() - dnsBean.getTime());
176 | if (timeSpan < dnsBean.getTtl() * 1000) {
177 | if (!dnsBean.getIps().isEmpty()) {
178 | //正确返回缓存ip
179 | return dnsBean.getIps().get(0);
180 | } else {
181 | //无缓存
182 | return null;
183 | }
184 | } else {
185 | updateIpByHostAsync(dnsBean.getHost());
186 | //缓存过期
187 | return null;
188 | }
189 | } else {
190 | updateIpByHostAsync(hostname);
191 | //无缓存
192 | return null;
193 | }
194 | }
195 | }
196 | }
197 |
198 | return null;
199 | }
200 |
201 | /**
202 | * 更新DNS缓存
203 | */
204 |
205 | private void updateIpByHostAsync(String hostname) {
206 |
207 | //选择http 或 https
208 | Request request = new Request.Builder()
209 | .url("http://" + HttpDnsServerIp + "/" + account_id + "/d?host=" + hostname)
210 | .build();
211 |
212 | retryTimes = 0;
213 | requestCache(request);
214 |
215 | }
216 |
217 |
218 | private void requestCache(Request request) {
219 | retryTimes++;
220 | client.newCall(request).enqueue(new Callback() {
221 | @Override
222 | public void onFailure(@NonNull Call call, @NonNull IOException e) {
223 | e.printStackTrace();
224 | if (retryTimes < RETRY_TIMES) {
225 | requestCache(request);
226 | }
227 | }
228 |
229 | @Override
230 | public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
231 | handleResponse(request, response);
232 |
233 | }
234 |
235 |
236 | });
237 | }
238 |
239 |
240 | private void saveCache(DnsBean dnsBean) {
241 | mDnsCaches.put(dnsBean.getHost(), dnsBean);
242 | }
243 |
244 | /**
245 | * 设置自定义请求超时时间,默认为2S
246 | *
247 | * @param timeoutInterval 单位是毫秒(ms)
248 | */
249 | public void setTimeoutInterval(int timeoutInterval) {
250 | client = new OkHttpClient.Builder()
251 | .connectTimeout(timeoutInterval, TimeUnit.SECONDS)
252 | .readTimeout(timeoutInterval, TimeUnit.SECONDS)
253 | .build();
254 | }
255 |
256 |
257 | public void setPreLoadSync(boolean preLoadSync) {
258 | this.preLoadSync = preLoadSync;
259 | }
260 |
261 | public String getHttpDnsServerIp() {
262 | return HttpDnsServerIp;
263 | }
264 |
265 | public void setHttpDnsServerIp(String httpDnsServerIp) {
266 | HttpDnsServerIp = httpDnsServerIp;
267 | }
268 | }
269 |
--------------------------------------------------------------------------------