阿里巴巴是这样说的
HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题。
分析demo
https://github.com/aliyun/alicloud-android-demo.git
普通场景 就是普通的http请求
sni场景 就是 server name Indication 场景
选择里面的httpdns_android_demo打开MainActivity。
private static final String APPLE_URL = "www.apple.com";private static final String TAOBAO_URL = "m.taobao.com";private static final String DOUBAN_URL = "dou.bz";private static final String ALIYUN_URL = "aliyun.com";private static final String HTTP_SCHEMA = "http://";private static final String HTTPS_SCHEMA = "https://";private static final String TAG = "httpdns_android_demo"; 先看看普通请求,
/*** 通过IP直连方式发起普通请求示例* 1. 通过IP直连时,为绕开服务域名检查,需要在Http报头中设置Host字段*/private void normalReqeust() {pool.execute(new Runnable() {@Overridepublic void run() {try {// 发送网络请求String originalUrl = HTTP_SCHEMA + TAOBAO_URL;URL url = new URL(originalUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();// 异步接口获取IPString ip = httpdns.getIpByHostAsync(url.getHost());if (ip != null) {// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");sendConsoleMessage("Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");String newUrl = originalUrl.replaceFirst(url.getHost(), ip);conn = (HttpURLConnection) new URL(newUrl).openConnection();// 设置HTTP请求头Host域conn.setRequestProperty("Host", url.getHost());}DataInputStream dis = new DataInputStream(conn.getInputStream());int len;byte[] buff = new byte[4096];StringBuilder response = new StringBuilder();while ((len = dis.read(buff)) != -1) {response.append(new String(buff, 0, len));}Log.d(TAG, "Response: " + response.toString());sendConsoleMessage("Get response from " + url.getHost() + ". Please check response detail from log.");} catch (Throwable throwable) {Log.e(TAG, "normal request failed.", throwable);sendConsoleMessage("Normal request failed." + " Please check error detail from log.");}}});} 从这例子不难看出,阿里的demo首先是创建一个url连接,获取host
host就是不包含http 的域名(比如s.taobao.com)然后调用通过sdk中的String ip = httpdns.getIpByHostAsync(url.getHost()); 也就是通过阿里自己的http请求查询这个host对应的ip地址,如果查询成功,那么HttpURLConnection会被重新创建,而且是通过ip进行创建,另外为了不丢失域名,所以这里做了一个操作就是设置请求头"Host"
也就是调用conn.setRequestProperty("Host", url.getHost());
整个过程就是通过 域名查询ip,然后通过ip进行请求的操作,
但是这个业务逻辑本身是dns自身做的事情,现在已经绕过了,直接交给阿里的http dns服务器进行操作。
不过不管怎么操作,这httpdns自身还是得通过运营商的dns进行请求,当然他们自己的也可以做缓存,或者ip地址可靠也可以直接访问比如http://203.107.1.33/100000/d?host=www.aliyun.com
他们是这样说的
考虑到服务IP防攻击之类的安全风险,为保障服务可用性,HTTPDNS同时提供多个服务IP,当某个服务IP在异常情况下不可用时,可以使用其它服务IP进行重试。上述文档中使用的203.107.1.33是其中一个服务IP。
HTTPDNS提供Android SDK和iOS SDK,两个平台的SDK中已经做了多IP轮转和出错重试的策略,通常情况下,建议开发者直接集成SDK即可,不需要自己手动调用HTTP API接口。
如果使用场景特殊,无法使用SDK,需要直接访问HTTP API接口,请提工单联系我们,我们将根据您的具体使用场景,为您提供多个服务IP和相关的安全建议。
具体参考https://help.aliyun.com/document_detail/52679.html?spm=a2c4g.11186623.2.21.11321d22lF9Vbp#1.1 访问方式
再看看https
/*** 通过IP直连方式发起https请求示例* 1. 通过IP直连时,为绕开服务域名检查,需要在Http报头中设置Host字段* 2. 为通过证书检查,需要自定义证书验证逻辑*/private void httpsRequest() {pool.execute(new Runnable() {@Overridepublic void run() {try {String originalUrl = HTTPS_SCHEMA + TAOBAO_URL + "/?sprefer=sypc00";URL url = new URL(originalUrl);HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();// 同步接口获取IPString ip = httpdns.getIpByHostAsync(url.getHost());if (ip != null) {// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");sendConsoleMessage("Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");String newUrl = originalUrl.replaceFirst(url.getHost(), ip);conn = (HttpsURLConnection) new URL(newUrl).openConnection();// 设置HTTP请求头Host域conn.setRequestProperty("Host", url.getHost());}final HttpsURLConnection finalConn = conn;conn.setHostnameVerifier(new HostnameVerifier() {/** 关于这个接口的说明,官方有文档描述:* This is an extended verification option that implementers can provide.* It is to be used during a handshake if the URL's hostname does not match the* peer's identification hostname.** 使用HTTPDNS后URL里设置的hostname不是远程的主机名(如:m.taobao.com),与证书颁发的域不匹配,* Android HttpsURLConnection提供了回调接口让用户来处理这种定制化场景。* 在确认HTTPDNS返回的源站IP与Session携带的IP信息一致后,您可以在回调方法中将待验证域名替换为原来的真实域名进行验证。**/@Overridepublic boolean verify(String hostname, SSLSession session) {String host = finalConn.getRequestProperty("Host");if (null == host) {host = finalConn.getURL().getHost();}return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);}});DataInputStream dis = new DataInputStream(conn.getInputStream());int len;byte[] buff = new byte[4096];StringBuilder response = new StringBuilder();while ((len = dis.read(buff)) != -1) {response.append(new String(buff, 0, len));}Log.d(TAG, "Response: " + response.toString());sendConsoleMessage("Get reponse from " + url.getHost() + ". Please check response detail from log.");} catch (Exception e) {e.printStackTrace();sendConsoleMessage("Get reponse failed. Please check error detail from log.");}}});} 处理重定向
private void recursiveRequest(String path, String reffer) {URL url;HttpsURLConnection conn = null;try {url = new URL(path);conn = (HttpsURLConnection) url.openConnection();// 同步接口获取IPString ip = httpdns.getIpByHostAsync(url.getHost());if (ip != null) {// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");sendConsoleMessage("Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");String newUrl = path.replaceFirst(url.getHost(), ip);conn = (HttpsURLConnection) new URL(newUrl).openConnection();// 设置HTTP请求头Host域conn.setRequestProperty("Host", url.getHost());}conn.setConnectTimeout(30000);conn.setReadTimeout(30000);conn.setInstanceFollowRedirects(false);HttpDnsTLSSniSocketFactory sslSocketFactory = new HttpDnsTLSSniSocketFactory(conn);conn.setSSLSocketFactory(sslSocketFactory);final HttpsURLConnection finalConn = conn;conn.setHostnameVerifier(new HostnameVerifier() {/** 关于这个接口的说明,官方有文档描述:* This is an extended verification option that implementers can provide.* It is to be used during a handshake if the URL's hostname does not match the* peer's identification hostname.** 使用HTTPDNS后URL里设置的hostname不是远程的主机名(如:m.taobao.com),与证书颁发的域不匹配,* Android HttpsURLConnection提供了回调接口让用户来处理这种定制化场景。* 在确认HTTPDNS返回的源站IP与Session携带的IP信息一致后,您可以在回调方法中将待验证域名替换为原来的真实域名进行验证。**/@Overridepublic boolean verify(String hostname, SSLSession session) {String host = finalConn.getRequestProperty("Host");if (null == host) {host = finalConn.getURL().getHost();}return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);}});int code = conn.getResponseCode();// Network blockif (needRedirect(code)) {//临时重定向和永久重定向location的大小写有区分String location = conn.getHeaderField("Location");if (location == null) {location = conn.getHeaderField("location");}if (!(location.startsWith(HTTP_SCHEMA) || location.startsWith(HTTPS_SCHEMA))) {//某些时候会省略host,只返回后面的path,所以需要补全urlURL originalUrl = new URL(path);location = originalUrl.getProtocol() + "://"+ originalUrl.getHost() + location;}recursiveRequest(location, path);} else {// redirect finish.DataInputStream dis = new DataInputStream(conn.getInputStream());int len;byte[] buff = new byte[4096];StringBuilder response = new StringBuilder();while ((len = dis.read(buff)) != -1) {response.append(new String(buff, 0, len));}Log.d(TAG, "Response: " + response.toString());sendConsoleMessage("Get reponse from " + url.getHost() + ". Please check response detail from log.");}} catch (MalformedURLException e) {Log.w(TAG, "recursiveRequest MalformedURLException", e);} catch (IOException e) {Log.w(TAG, "recursiveRequest IOException");} catch (Exception e) {Log.w(TAG, "unknow exception");} finally {if (conn != null) {conn.disconnect();}}}private boolean needRedirect(int code) {return code >= 300 && code < 400;} 预解析域名
顾名思义,在请求某个东西之前先请求,比如app刚打开的时候。
这样通过sdk进行查询就会直接从缓存中取出。
/*** 设置预解析域名列表代码示例*/private void setPreResoveHosts() {// 设置预解析域名列表// 可以替换成您在后台配置的域名httpdns.setPreResolveHosts(new ArrayList<>(Arrays.asList(APPLE_URL, ALIYUN_URL, TAOBAO_URL, DOUBAN_URL)));sendConsoleMessage("设置预解析域名成功");} 降级解析
/*** 自定义降级逻辑代码示例*/private void setDegrationFilter() {DegradationFilter filter = new DegradationFilter() {@Overridepublic boolean shouldDegradeHttpDNS(String hostName) {// 此处可以自定义降级逻辑,例如www.taobao.com不使用HttpDNS解析// 参照HttpDNS API文档,当存在中间HTTP代理时,应选择降级,使用Local DNSreturn hostName.equals(DOUBAN_URL);}};// 将filter传进httpdns,解析时会回调shouldDegradeHttpDNS方法,判断是否降级httpdns.setDegradationFilter(filter);sendConsoleMessage("自定义降级逻辑成功");} 降级解析就是不用他们的dns,使用运营商的。
处理webview
class WebviewTlsSniSocketFactory extends SSLSocketFactory {private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName();HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();private HttpsURLConnection conn;public WebviewTlsSniSocketFactory(HttpsURLConnection conn) {this.conn = conn;}@Overridepublic Socket createSocket() throws IOException {return null;}@Overridepublic Socket createSocket(String host, int port) throws IOException, UnknownHostException {return null;}@Overridepublic Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {return null;}@Overridepublic Socket createSocket(InetAddress host, int port) throws IOException {return null;}@Overridepublic Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {return null;}// TLS layer@Overridepublic String[] getDefaultCipherSuites() {return new String[0];}@Overridepublic String[] getSupportedCipherSuites() {return new String[0];}@Overridepublic Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {String peerHost = this.conn.getRequestProperty("Host");if (peerHost == null)peerHost = host;Log.i(TAG, "customized createSocket. host: " + peerHost);InetAddress address = plainSocket.getInetAddress();if (autoClose) {// we don't need the plainSocketplainSocket.close();}// create and connect SSL socket, but don't do hostname/certificate verification yetSSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);// enable TLSv1.1/1.2 if availablessl.setEnabledProtocols(ssl.getSupportedProtocols());// set up SNI before the handshakeif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {Log.i(TAG, "Setting SNI hostname");sslSocketFactory.setHostname(ssl, peerHost);} else {Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");try {java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);setHostnameMethod.invoke(ssl, peerHost);} catch (Exception e) {Log.w(TAG, "SNI not useable", e);}}// verify hostname and certificateSSLSession session = ssl.getSession();if (!hostnameVerifier.verify(peerHost, session))throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +" using " + session.getCipherSuite());return ssl;}}webView.setWebViewClient(new WebViewClient() {@SuppressLint("NewApi")@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {String scheme = request.getUrl().getScheme().trim();String method = request.getMethod();Map<String, String> headerFields = request.getRequestHeaders();String url = request.getUrl().toString();Log.e(TAG, "url:" + url);// 无法拦截body,拦截方案只能正常处理不带body的请求;if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))&& method.equalsIgnoreCase("get")) {try {URLConnection connection = recursiveRequest(url, headerFields, null);if (connection == null) {Log.e(TAG, "connection null");return super.shouldInterceptRequest(view, request);}// 注*:对于POST请求的Body数据,WebResourceRequest接口中并没有提供,这里无法处理String contentType = connection.getContentType();String mime = getMime(contentType);String charset = getCharset(contentType);HttpURLConnection httpURLConnection = (HttpURLConnection)connection;int statusCode = httpURLConnection.getResponseCode();String response = httpURLConnection.getResponseMessage();Map<String, List<String>> headers = httpURLConnection.getHeaderFields();Set<String> headerKeySet = headers.keySet();Log.e(TAG, "code:" + httpURLConnection.getResponseCode());Log.e(TAG, "mime:" + mime + "; charset:" + charset);// 无mime类型的请求不拦截if (TextUtils.isEmpty(mime)) {Log.e(TAG, "no MIME");return super.shouldInterceptRequest(view, request);} else {// 二进制资源无需编码信息if (!TextUtils.isEmpty(charset) || (isBinaryRes(mime))) {WebResourceResponse resourceResponse = new WebResourceResponse(mime, charset, httpURLConnection.getInputStream());resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response);Map<String, String> responseHeader = new HashMap<String, String>();for (String key: headerKeySet) {// HttpUrlConnection可能包含key为null的报头,指向该http请求状态码responseHeader.put(key, httpURLConnection.getHeaderField(key));}resourceResponse.setResponseHeaders(responseHeader);return resourceResponse;} else {Log.e(TAG, "non binary resource for " + mime);return super.shouldInterceptRequest(view, request);}}} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}return super.shouldInterceptRequest(view, request);}@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, String url) {// API < 21 只能拦截URL参数return super.shouldInterceptRequest(view, url);}});webView.loadUrl(targetUrl);}public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {HttpURLConnection conn;URL url = null;try {url = new URL(path);conn = (HttpURLConnection) url.openConnection();// 异步接口获取IPString ip = httpdns.getIpByHostAsync(url.getHost());if (ip != null) {// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");String newUrl = path.replaceFirst(url.getHost(), ip);conn = (HttpURLConnection) new URL(newUrl).openConnection();if (headers != null) {for (Map.Entry<String, String> field : headers.entrySet()) {conn.setRequestProperty(field.getKey(), field.getValue());}}// 设置HTTP请求头Host域conn.setRequestProperty("Host", url.getHost());} else {return null;}conn.setConnectTimeout(30000);conn.setReadTimeout(30000);conn.setInstanceFollowRedirects(false);if (conn instanceof HttpsURLConnection) {final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory((HttpsURLConnection) conn);// sni场景,创建SSLScockethttpsURLConnection.setSSLSocketFactory(sslSocketFactory);// https场景,证书校验httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {@Overridepublic boolean verify(String hostname, SSLSession session) {String host = httpsURLConnection.getRequestProperty("Host");if (null == host) {host = httpsURLConnection.getURL().getHost();}return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);}});}int code = conn.getResponseCode();// Network blockif (needRedirect(code)) {// 原有报头中含有cookie,放弃拦截if (containCookie(headers)) {return null;}String location = conn.getHeaderField("Location");if (location == null) {location = conn.getHeaderField("location");}if (location != null) {if (!(location.startsWith("http://") || location.startsWith("https://"))) {//某些时候会省略host,只返回后面的path,所以需要补全urlURL originalUrl = new URL(path);location = originalUrl.getProtocol() + "://"+ originalUrl.getHost() + location;}Log.e(TAG, "code:" + code + "; location:" + location + "; path" + path);return recursiveRequest(location, headers, path);} else {// 无法获取location信息,让浏览器获取return null;}} else {// redirect finish.Log.e(TAG, "redirect finish");return conn;}} catch (MalformedURLException e) {Log.w(TAG, "recursiveRequest MalformedURLException");} catch (IOException e) {Log.w(TAG, "recursiveRequest IOException");} catch (Exception e) {Log.w(TAG, "unknow exception");}return null;} 其他demo
public class NetworkRequestUsingHttpDNS {private static HttpDnsService httpdns;// 填入您的HTTPDNS accoutID信息,您可以从HTTPDNS控制台获取该信息private static String accountID = "100000";// 您的热点域名private static final String[] TEST_URL = {"http://www.aliyun.com", "http://www.taobao.com"};public static void main(final Context ctx) {try {// 设置APP Context和Account ID,并初始化HTTPDNShttpdns = HttpDns.getService(ctx, accountID);// DegradationFilter用于自定义降级逻辑// 通过实现shouldDegradeHttpDNS方法,可以根据需要,选择是否降级DegradationFilter filter = new DegradationFilter() {@Overridepublic boolean shouldDegradeHttpDNS(String hostName) {// 此处可以自定义降级逻辑,例如www.taobao.com不使用HttpDNS解析// 参照HttpDNS API文档,当存在中间HTTP代理时,应选择降级,使用Local DNSreturn hostName.equals("www.taobao.com") || detectIfProxyExist(ctx);}};// 将filter传进httpdns,解析时会回调shouldDegradeHttpDNS方法,判断是否降级httpdns.setDegradationFilter(filter);// 设置预解析域名列表,真正使用时,建议您将预解析操作放在APP启动函数中执行。预解析操作为异步行为,不会阻塞您的启动流程httpdns.setPreResolveHosts(new ArrayList<>(Arrays.asList("www.aliyun.com", "www.taobao.com")));// 允许返回过期的IP,通过设置允许返回过期的IP,配合异步查询接口,我们可以实现DNS懒更新策略httpdns.setExpiredIPEnabled(true);// 发送网络请求String originalUrl = "http://www.aliyun.com";URL url = new URL(originalUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();// 异步接口获取IP,当IP TTL过期时,由于采用DNS懒更新策略,我们可以直接从内存获得最近的DNS解析结果,同时HTTPDNS SDK在后台自动更新对应域名的解析结果ip = httpdns.getIpByHostAsync(url.getHost());if (ip != null) {// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置Log.d("HTTPDNS Demo", "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");String newUrl = originalUrl.replaceFirst(url.getHost(), ip);conn = (HttpURLConnection) new URL(newUrl).openConnection();}DataInputStream dis = new DataInputStream(conn.getInputStream());int len;byte[] buff = new byte[4096];StringBuilder response = new StringBuilder();while ((len = dis.read(buff)) != -1) {response.append(new String(buff, 0, len));}Log.e("HTTPDNS Demo", "Response: " + response.toString());} catch (Exception e) {e.printStackTrace();}}/*** 检测系统是否已经设置代理,请参考HttpDNS API文档。*/public static boolean detectIfProxyExist(Context ctx) {boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;String proxyHost;int proxyPort;if (IS_ICS_OR_LATER) {proxyHost = System.getProperty("http.proxyHost");String port = System.getProperty("http.proxyPort");proxyPort = Integer.parseInt(port != null ? port : "-1");} else {proxyHost = android.net.Proxy.getHost(ctx);proxyPort = android.net.Proxy.getPort(ctx);}return proxyHost != null && proxyPort != -1;}
} 参考https://help.aliyun.com/document_detail/30143.html
okhttp接入httpdns最佳实践https://help.aliyun.com/document_detail/52008.html

















