HTTPDNS 在 iOS 中的实践

“未找到主机名”,这是很常见的错误。出现这个错误,按理来说,应该也是正常的。但郁闷的是,常常别的应用能正常使用,偏就是自己开发的应用不行,这实在令人头痛。

原因

目前很多 APP 会用 HTTP/HTTPS 来进行网络交互。APP 会用域名访问接口,正常情况下,如果设备网络畅通的情况下,都是能正常访问到服务器的。但是 DNS 劫持、UDP 不稳定等,者导致经常出现域名无法解析的情况,自然也就无法正常请求了。

解决方案

找到了问题所在,自然也就是解决方法了。
1、替换系统的域名解析方案;目前 iOS 和 安卓 都是无法替换的,自然行不通了。
2、用 IP 访问;即不采用域名的形式,直接用 IP 访问,自然也是不存在域名解析的问题了。

直接用完全不用域名,只用 IP 访问也是不可能的。现在大家一般都将服务器托管在各大服务商处,说不定哪天将换到另一台服务器,新用了另一个 IP 呢;再说现在很多资源为了能尽快访问到,都会采用 CDN,固定 IP 就更不太可能了。所以好的办法就是我们手动将 host 替换成 IP。如:http://api.example.com/path 替换成:http://124.12.42.xx/path,如果是在 IPv6 的环境下则是:http://[2002:0:0:0:0:0:7c0c:xxx]/path

域名解析

国内提供域名解析 API 接口的,有 DNSPod,示例如下:

http://119.29.29.29/d?dn=www.163.com&ttl=1

// 输出如下:
183.47.248.109;125.90.206.144;14.215.100.95;183.6.245.191,17

可以看到,解析域名很简单,其中 ttl=1 是表示要求在响应结果中携带解析结果的缓存时间,上例中是 17。很多服务往往会部署在多台服务器上,所以它会根据源 IP 返回的是一个合适的 IP 数组。但目前(2016/12/17)它好像还不支持 HTTPS。

拿到 IP 后,就能以合适的形式替换掉 host 了:

// originURL 是原始要被替换 host 的 URL
var urlComponets = URLComponents(url: originURL, resolvingAgainstBaseURL: false)

// func asyncParseIPAsURLFormat 会为 host 解析出合适的 ip
urlComponets?.host = parse.asyncIPParseInURLFormat(ofhost: urlComponets?.host)
let url = urlComponets?.url

这个示例的核心点在于 asyncParseIPAsURLFormat。它需要考虑网络环境、缓存等问题,最主要还是热开关功能(万一突然出现 IP 不能访问的情况,不至于应用都用不了了),所以涉及的东西还是比较多的。

现在国内有很多厂商为 DNSPod 开发了 SDK,比如 阿里、七牛(开源)等。不想自己写的,不妨使用这些 SDK。

注意事项

HTTP 请求头中的 host 字段

HTTP 标准中规定,服务器会将请求头中的 host 字段的值作为请求的域名。咱们使用 IP 替换 URL 中的 host 进行访问,此时网络库会将 IP 当作 host,服务器就会解析异常了。为了解决这个问题,咱们可以手动设置请求中的 host 字段:

request.setValue(originHost, forHTTPHeaderField: "host")

可能的特殊字段要求

其它一些协议也会识别 host 字段,如 websocket 协议。还有的协议会对其它字段作同样的要求,我们自己在开发中遇到过要设置的字段有:

  • refer
  • originHost

当然这根服务器的配置相关,如果你有遇到访问服务器被拒的情况,请与服务器的小伙伴沟通。

无法以 URLRequest 访问的情况

有很多第三方服务,要求只能以 URL 的形式访问,比如很多直播 SDK。只能以 URL 的形式访问,自然也就无法重写 host 字段了。所以我们在最开始的时候(2016年6月)使用了其中一家能以 IP 访问的服务商,但现在(2016年12月)他们也要求不能再以 IP 访问了。

在这种情况下,可以在 URL 的后面加上一个参数用来指定 host。当然,这需要服务器的配合。

有部分网络库支持自动管理 COOKIE,而且用的还是 URL 中的 host,所以 COOKIE 管理也就悲剧了。在使用 HTTPDNS 时,还是关闭 COOKIE 自动管理吧。

代理

在代理情况下,代理服务器会将 URL 中的 host 发送给服务器,服务器也就无法处理了。所以在代理情况下还是关了好。

HTTPS

HTTPS 要分两种情况:
1、单 IP 单域名;很多小应用只在服务器上部署了一个 HTTPS 服务,就是这种情况。
2、单 IP 多域名;这种在 CDN 下常见,一个服务器部署了多个 HTTPS 服务。

1、单 IP 单域名

HTTPS 请求首先会跟 SSL/TLS 握手。步骤如下:
1、客户端将其支持的算法列表和用于产生密钥的随机数等参数发给服务器;
2、服务器选择合适的算法,下发公钥证书和用于产生密钥的随机数给客户端;
3、客户端检验服务器证书,然后用服务器的公钥生成一个随机密码串,发给服务器;
4、客户端与服务器根据以上信息独立计算加密和 MAC 密钥,并相互交换;
5、基于密钥通信。

其中与 HTTPDNS 有关的在第 3 步的 检验服务器证书。客户端会检查证书的 domain 域和扩展域,只有包含了请求的 host,才会验证通过。HTTPDNS 用 IP 替换了 host,会导致出现 domain 不匹配的情况,是无法通过的。

解决起来也简单,只需在验证时,传入真实的 host 即可:

func urlSession(session: URLSession, task: URLSessionTask, challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void){

  if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
    let host = challenge.protectionSpace.host
    /// 如果 host 是 ip,则获取该 ip 所对应的真实的 host
    let realHost = host.isIp ? host(forIp: host) : host
    if self.evaluate(serverTrust: challenge.protectionSpace.serverTrust domain: realHost) {
      ...
    }
    ...
  }
}

如果觉得麻烦,也可以 swizzle URLProtectionSpace 类的 host 属性的 get 方法,让它返回真实 host。这样就可以无需对 AFNetworking 或 SDWebImage 等网络框架过多的配置了。

2、单 IP 多域名

多域名往往意味着多证书(证书有几种类型,有的可以带有通配符,所以不一定)。上面提到,在 SSL/TLS 握手的第二步中,服务器会下发公钥证书。如果用 IP 直接访问服务器,那么服务器怎么知道下发哪个证书合适呢?所以结果往往是不确定的。

我们用的服务器是 nginx 1.4.7,在多域名情况下,它返回的是第一个域名所对应的证书。有一次我们后台开发同学自已在线上环境下配置了一次,其结果就是我们客户端的 https 验证全部失败,造成线上应用无法使用的事故(影响大概10来分钟)。

let result: SecTrustResultType
SecTrustEvaluate(trust, &result)
/// 多域名情况下,服务器下发的证书可能与 host 不一致。导致 result 的结果是:recoverableTrustFailure

针对这种情况,阿里的解决方案是 hook SSL 的连接过程,用 Socket 进行真正的请求。我们的做法是:多域名的情况下不用 HTTPDNS -_-!。CDN 下使用 HTTPS 的话,往往就是这种情况。

阿里的方案:地址

总结

我们自己的应用 HTTPDNS 上线一年,极大的改善了 “找不到主机” 的问题。虽然中间有些小坑,不过对于收效来说还是值得的。

你可能感兴趣的:(HTTP)