功能说明
HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题。
功能 | 说明 |
---|---|
防劫持 | 绕过运营商Local DNS,避免域名劫持,让每一次访问都畅通无阻。 |
精准调度 | 基于访问的来源IP,获得最精准的解析结果,让客户端就近接入业务节点。 |
0ms解析延迟 | 通过热点域名预解析、缓存DNS解析结果、解析结果懒更新策略等方式实现0解析延迟 |
快速生效 | 避免Local DNS不遵循权威TTL,解析结果长时间无法更新的问题 |
降低解析失败率 | 有效降低无线场景下解析失败的比率 |
防劫持
HTTPDNS代替了传统的LocalDNS完成递归解析的功能,基于HTTP协议的设计可以适用于几乎所有的网络环境,同时保留了鉴权、HTTPS等更高安全性的扩展能力,避免恶意攻击劫持行为。
精准调度
传统域名解析的调度精准性问题,本质根源在于Local DNS的部署和分配机制上。由于碎片化的管理方式,这些环节的服务质量同样很难得到保障。HTTPDNS在递归解析实现上优化了与权威DNS的交互,通过<a name="https://datatracker.ietf.org/doc/rfc7871">edns-client-subnet</a>协议将终端用户的IP信息直接交付给权威DNS,这样权威DNS就可以忽略Local DNS IP信息,根据终端用户的IP信息进行精准调度,避免Local DNS的坐标干扰
DNS解析0延迟:
-
构建客户端DNS缓存;
通过合理的DNS缓存,我们确保每次网络交互的DNS解析都是从内存中获取IP信息,从而大幅降低DNS解析开销。根据业务的不同,我们可以 制订更丰富的缓存策略,如根据运营商缓存,可以在网络切换的场景下复用已缓存的不同运营商线路的域名IP信息,避免网络切换后进行链 路重选择引入的DNS网络解析开销。另外,我们还可以引入IP本地化离线存储,在客户端重启时快速从本地读取域名IP信息,大幅提升首页 载入效率。
-
热点域名预解析;
在客户端启动过程中,我们可以通过热点域名的预解析完成热点域名的缓存载入。当真正的业务请求发生时,直接由内存中读取目标域名的IP 信息,避免传统DNS的网络开销。
-
懒更新策略;
绝大多数场景下业务域名的IP信息变更并不频繁,特别是在单次APP的使用周期内,域名解析获取的IP往往是相同的(特殊业务场景除外)。 因此我们可以利用DNS懒更新策略来实现TTL过期后的DNS快速解析。所谓DNS懒更新策略即客户端不主动探测域名对应IP的TTL时间,当业务 请求需要访问某个业务域名时,查询内存缓存并返回该业务域名对应的IP解析结果。如果IP解析结果的TTL已过期,则在后台进行异步DNS网 络解析与缓存结果更新。通过上述策略,用户的所有DNS解析都在与内存交互,避免了网络交互引入的延迟。
实现方案
服务端:
服务端提供API接口,app端直接通过ip地址访问,ip地址可以有多个
请求方式:HTTP GET
URL参数说明:
名称 | 是否必须 | 描述 |
---|---|---|
host | 必须 | 要解析的域名 |
ip | 可选 | 用户的来源IP,如果没指定这个参数,默认使用请求连接的源IP |
请求示例:
-
示例1(默认来源IP):http://x.x.x.x/d?host=www.suning.com
-
示例2(指定来源IP):http://x.x.x.x/d?host=www.suning.com&ip=185.53.179.6
考虑到服务IP防攻击之类的安全风险,为保障服务可用性,HTTPDNS同时提供多个服务IP,当某个服务IP在异常情况下不可用时,可以使用其它服务IP进行重试。
请求成功时,HTTP响应的状态码为200,响应结果用JSON格式表示,示例如下:
{
"host": "www.suning.com",
"ips": [
"112.84.104.48"
],
"ttl": 57,
"origin_ttl": 120
}
请求失败的响应示例:
{
"code": "MissingArgument"
}
错误码列表如下:
错误码 | HTTP状态码 | 描述 |
---|---|---|
MissingArgument | 400 | 缺少必要参数 |
InvalidHost | 400 | 域名格式不合法 |
MethodNotAllowed | 405 | 不支持的HTTP方法 |
InternalError | 500 | 服务端内部错误 |
错误处理
异常下的出错兼容逻辑,主要包括异步请求,重试,降级
异步请求
访问HTTPDNS服务时,应该使用异步请求的策略,避免解析延迟太大而对业务造成影响,特别是在网络环境异常或HTTPDNS服务IP异常不可
用时,如果用同步访问,需要等待网络超时后才会返回解析失败,这个超时时间较大,可能对业务的使用体验造成很大影响。
异步请求策略:解析域名时,如果当前缓存中有TTL未过期的IP,可直接使用;如果没有,则立刻让此次请求降级走原生LocalDNS解析,同
时另起线程异步地发起HTTPDNS请求进行解析,更新缓存,这样后续解析域名时就能命中缓存。
重试
访问HTTPDNS服务解析域名时,如果请求HTTPDNS服务端失败,即HTTP请求没有返回,可以进行重试。
大部分情况下,这种访问失败是由于网络原因引起的,重试可以解决。
降级
不管是因为什么原因,当通过HTTPDNS服务无法获得域名对应的IP时,都必须降级:使用标准的DNS解析,通过Local DNS去解析域名。
Android端:
OkHttp默认使用系统DNS服务InetAddress进行域名解析,但同时也暴露了自定义DNS服务的接口,通过该接口我们可以优雅地使用HttpDns。
- 自定义DNS接口
OkHttp暴露了一个Dns接口,通过实现该接口,我们可以自定义Dns服务:
public class OkHttpDns implements Dns {
private static final Dns SYSTEM = Dns.SYSTEM;
HttpDnsService httpdns;//httpdns 解析服务
private static OkHttpDns instance = null;
private OkHttpDns(Context context) {
this.httpdns = HttpDns.getService(context, "account id");
}
public static OkHttpDns getInstance(Context context) {
if(instance == null) {
instance = new OkHttpDns(context);
}
return instance;
}
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
//通过异步解析接口获取ip
String ip = httpdns.getIpByHostAsync(hostname);
if(ip != null) {
//如果ip不为null,直接使用该ip进行网络请求
List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
Log.e("OkHttpDns", "inetAddresses:" + inetAddresses);
return inetAddresses;
}
//如果返回null,走系统DNS服务解析域名
return Dns.SYSTEM.lookup(hostname);
}
}
- 创建OkHttpClient
创建OkHttpClient对象,传入OkHttpDns对象代替默认Dns服务:
private void okhttpDnsRequest() {
OkHttpClient client = new OkHttpClient.Builder()
.dns(OkHttpDns.getInstance(getApplicationContext()))
.build();
Request request = new Request.Builder()
.url("http://www.aliyun.com")
.build();
Response response = null;
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
DataInputStream dis = new DataInputStream(response.body().byteStream());
int len;
byte[] buff = new byte[4096];
StringBuilder result = new StringBuilder();
while ((len = dis.read(buff)) != -1) {
result.append(new String(buff, 0, len));
}
Log.d("OkHttpDns", "Response: " + result.toString());
}
});
}
- 总结
相比于通用方案,OkHttp+HttpDns有以下两个主要优势:
实现简单,只需通过实现Dns接口即可接入HttpDns服务
通用性强,该方案在HTTPS,SNI以及设置Cookie等场景均适用。规避了证书校验,域名检查等环节
IOS端:
基于NSURLProtocol可拦截iOS系统上基于上层网络库NSURLConnection/NSURLSession发出的网络请求;
通过以下接口注册自定义NSURLProtocol,用于拦截上层网络请求,并创建新的网络请求接管数据发送、接收、重定向等处理逻辑,将结果反馈给原始请求。
[NSURLProtocol registerClass:[CustomProtocol class]];
自定义NSURLProtocol处理过程概述:
- 在canInitWithRequest中过滤要需要做HTTPDNS域名解析的请求;
- 请求拦截后,做HTTPDNS域名解析;
- 解析完成后,同普通请求一样,替换URL.host字段,替换HTTP Header Host域,并接管该请求的数据发送、接收、重定向等处理;