iOS-网络优化(一)-ip直连

一、基础背景

1. DNS解析

现在假如我们访问一个网站www.baidu.com从按下回车到百度页面显示到我们的电脑上会经历如下几个步骤

  • 1:计算机会向我们的运营商(移动、电信、联通等)发出打开www.baidu.com的请求。
  • 2:运营商收到请求后会到自己的DNS服务器中找www.baidu.com这个域名所对应的服务器的IP地址(也就是百度的服务器的IP地址),这里比如是180.149.132.47。
  • 3:运营商用第二步得到的IP地址去找到百度的服务器请求得到数据后返回给我们。

其中第二步就是我们所说的DNS解析过程,域名和IP地址的关系其实就是我们的身份证号和姓名的关系,都是来标记一个人或者是一个网站的,只是IP地址\身份证号只是一串没有意义的数字,辨识度低,又不好记,所以就会在IP上加上一个域名以便区分,或是做的更加个性化,但是如果真的要来准确的区分还是要靠身份证号码或者是IP的,所以DNS解析就应运而生了。

2. 什么是DNS劫持

DNS劫持,是指在DNS解析过程中拦截域名解析的请求,然后做一些自己的处理,比如返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。根本原因就是以下两点:

  • 1:恶意攻击,拦截运营商的解析过程,把自己的非法东西嵌入其中。
  • 2:运营商为了利益或者一些其他的因素,允许一些第三方在自己的链接里打打广告之类的。
4. 防止DNS劫持

了解了DNS劫持的相关资料后我们就知道了,防止NDS劫持就要从第二步入手,因为DNS解析过程是运营商来操作的,我们不能去干涉他们,不然我们也就成了劫持者了,所以我们要做的就是在我们请求之前对我们的请求链接做一些修改,将我们原本的请求链接www.baidu.com 修改为180.149.132.47,然后请求出去,这样的话就运营商在拿到我们的请求后发现我们直接用的就是IP地址就会直接给我们放行,而不会去走他自己DNS解析了,也就是说我们把运营商要做的事情自己先做好了。不走他的DNS解析也就不会存在DNS被劫持的问题,从根本是解决了。

5. IP直连

它具有多方面的优势:

  • 防劫持,可以绕过运营商 LocalDNS 解析过程,避免域名劫持,提高网络访问成功率。
  • 降低延迟,DNS 解析是一个相对耗时的工作,跳过这个过程可以降低一定的延迟。
  • 精准调度,运营商解析返回的节点不一定是最优的,自己获取 IP 可以基于自己的策略来获取最精准的、最优的节点。
5. 获取IP

对于获取 IP,有两种方案:

  1. HTTPDNS

HTTPDNS是客户端基于http协议向服务器A发送域名B解析请求(例如:www.baidu.com),服务器A直接返回域名B对应的ip地址(例如:119.75.217.109),客户端获取到的IP后就向直接往此IP发送业务协议请求。
这种方式替代了基于DNS协议向运营商LocalDNS发起解析请求,可以从根本上避免LocalDNS造成的域名劫持问题。
常规的DNS解析是通过UDP方式。

国内提供域名解析 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
现在国内有很多厂商为 DNSPod 开发了 SDK,比如 阿里、七牛(开源)等。不想自己写的,不妨使用这些 SDK。

  1. 内置IP列表
    可以在启动等阶段由服务端下发域名和 IP 的对应列表,客户端来进行缓存,发起网络请求的时候直接根据缓存 IP 来进行业务访问。

二、实际应用场景中的问题

实现 HTTP 协议下 IP 连接其实是很简单的,我们只需要通过 NSURLProtocol 来拦截网络请求,然后将符号条件的网络请求 URL 中的域名修改为 IP 就可以啦。

但是会有各种各样的问题:

1.http请求服务器无法判断请求访问的内容

原因:在我们修改http请求时,这时http的head中host字段会变成ip,因为一台服务器我们会有很多接口服务同时存在,服务器接收到请求后无法根据域名去判断我们访问的是哪个服务。

解决:由于服务器是根据host字段来判断请求的服务,所以在发起网络请求时,用带ip的URL生成request后,手动将request中的host字段改回域名。这样服务器可以正确识别,运营商也会根据域名中的ip为我们路由。

//原始URL
NSURL *originalUrl =[NSURL URLWithString:@"https://api.helijia.com/app-merchant"];
//根据原始URL获取 第三方解析出的ip
NSString *ip = [self getHostByUrlSyn:url];
//替换ip后的URL
NSURL *url = [ip replaceHostWithIp:ip];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//将request的host字段改为原始URL的域名
[request setValue:originalUrl.host forHTTPHeaderField:@"host"];
2. POST请求这块也算是一个大坑

我们知道http的post请求会包含一个body体,里面包含我们需要上传的参数等一些资料,对于POST请求我们的NSURLProtocol是可以正常拦截的,但是我们拦截之后发现无论怎么样我们获得的body体都为nil!后来查了一些资料发下又是苹果爸爸在做手脚。NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Request timeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。你以为这么就结束了吗?并没有,后来查了大量的资料发现,既然post请求的httpbody没有苹果复制下来,那我们就不用httpbody,我们再往底层去看就会发现HTTPBodyStream这个东西我们可以通过他来获取请求的body体具体代吗如下

#pragma mark -
#pragma mark 处理POST请求相关POST  用HTTPBodyStream来处理BODY体
- (NSMutableURLRequest *)handlePostRequestBodyWithRequest:(NSMutableURLRequest *)request {
    NSMutableURLRequest * req = [request mutableCopy];
    if ([request.HTTPMethod isEqualToString:@"POST"]) {
        if (!request.HTTPBody) {
            uint8_t d[1024] = {0};
            NSInputStream *stream = request.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            while ([stream hasBytesAvailable]) {
                NSInteger len = [stream read:d maxLength:1024];
                if (len > 0 && stream.streamError == nil) {
                    [data appendBytes:(void *)d length:len];
                }
            }
            req.HTTPBody = [data copy];
            [stream close];
        }
    }
    return req;
}

这样之后的req就是携带了body体的request啦,可以愉快地做post请求啦。

3.Https请求证书校验错误

分析:
发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下:

  • 客户端发起握手请求,携带随机数、支持算法列表等参数。
  • 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
  • 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥加密。
  • 服务端通过私钥获取随机数信息。
  • 双方根据以上交互的信息生成session ticket,用作该连接后续数据传输的加密密钥。

上述过程中,和HTTPDNS有关的是第3步,客户端需要验证服务端下发的证书,验证过程有以下两个要点:

  • 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
  • 客户端需要检查证书的domain域和扩展域,看是否包含本次请求的host。
    如果上述两点都校验通过,就证明当前的服务端是可信任的,否则就是不可信任,应当中断当前连接。

当客户端使用HTTPDNS解析域名时,请求URL中的host会被替换成HTTPDNS解析出来的IP,所以在证书验证的第2步,会出现domain不匹配的情况,导致SSL/TLS握手不成功。
解决方案:只需在验证时,传入真实的 host 即可:

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     * 创建证书校验策略
     */
    NSMutableArray *policies = [NSMutableArray array];
    if (domain) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    /*
     * 绑定校验策略到服务端的证书上
     */
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
    /*
     * 评估当前serverTrust是否可信任,
     * 官方建议在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed
     * 的情况下serverTrust可以被验证通过,https://developer.apple.com/library/ios/technotes/tn2232/_index.html
     * 关于SecTrustResultType的详细信息请参考SecTrust.h
     */
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
/*
 * NSURLSession
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if (!challenge) {
        return;
    }
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    /*
     * 获取原始域名信息。
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // 对于其他的challenges直接使用默认的验证方案
    completionHandler(disposition,credential);
}

4. webview中H5页面部分

HTTPDNS实施的主要难点与坑点都在H5页面上面,下面逐条记录下在实施webview的HTTPDNS时遇到的问题:由于web页面的请求并不是由客户端发起,我们无法在生成request的时候修改host。
解决:在这里我们使用NSURLProtocol来解决。

用一句话解释NSURLProtocol :NSURLProtocol就是一个苹果允许的中间人攻击。
NSURLProtocol可以劫持系统所有基于C socket的网络请求。
注意:WKWebView基于Webkit,并不走底层的C socket,所以NSURLProtocol拦截不了WKWebView中的请求。

具体步骤为:
注册NSURLProtocol子类 -> 使用NSURLProtocol子类拦截Webview请求 -> 使用NSURLSession重新发起请求 -> 将NSURLSession请求的响应内容返回给Webview

  1. NSURLProtocol子类的实现:
    拦截哪些请求
  • request的URL是ip的(ipv4、ipv6)
  • 非白名单的请求
/**

 *  是否拦截处理指定的请求

 *

 *  @param request 指定的请求

 *

 *  @return 返回YES表示要拦截处理,返回NO表示不拦截处理

 */

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {

    //DNS开关控制功能开启关闭

    if (![[HLJHttpDNS shareInstance] isDNSConfigWorking]) {

        return NO;

    }

    /* 防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环 */

    if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {

        return NO;

    }

    // 防止无限循环, 第三方解析会发出ip域名的请求,这里筛选

    // 判断请求URL的Host是否Ipv4

    if ([WebViewURLProtocol checkHostIp:request.URL.host]) {

        return NO;

    }    

    NSString *url = [request.URL.host mutableCopy];

    //去掉Ipv6的大括号

    url = [url stringByReplacingOccurrencesOfString:@"[" withString:@""];

    url = [url stringByReplacingOccurrencesOfString:@"]" withString:@""];

    // 判断请求URL的Host是否Ipv6

    if ([WebViewURLProtocol checkHostIpv6:url]) {

        return NO;

    }

    NSMutableURLRequest *mutableReq = [request mutableCopy];

    //假设原始的请求头部没有host信息,只有使用IP替换后的请求才有

    NSString *host = [mutableReq valueForHTTPHeaderField:@"host"];

    if (!mutableReq && host) {

        return NO;

    }

    return YES;

}

在拦截的部分,我们需要注意一点,因为我们向第三方解析域名的请求也是ip的。这里我们需要在拦截时对域名的host位进行判断,如果是ipv4、ipv6的域名,就不对其进行拦截。不然程序就会循环拦截重新发起后的请求,导致程序卡死。
我们项目中图片服务是走CDN的服务器,还有其他统计等第三方的服务等等。我们将这类第三方的域名加入了白名单,在请求时会跳过对白名单内域名的拦截。

  1. 拦截住的请求怎么修改
  • 替换域名为解析后的ip
  • 修改request的host
  • 修改证书校验中的host

拦截请求后,我们在重新发起的请求中对request进行修改:替换域名为解析后的ip、修改request的host

- (void)startLoading {

    NSMutableURLRequest *request = [self.request mutableCopy];

    // 表示该请求已经被处理,防止无限循环

    [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

    NSMutableURLRequest *mutableReq = [request mutableCopy];

    NSString *originalUrl = mutableReq.URL.absoluteString;

    NSURL *url = [NSURL URLWithString:originalUrl];

    // 同步接口获取IP地址

    NSString *ip = [[HLJHttpDNS shareInstance] getHostByNameSyn:url.absoluteString];    

    if (ip) {

        // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置

        NSRange hostFirstRange = [originalUrl rangeOfString:url.host];

        if (NSNotFound != hostFirstRange.location) {

            mutableReq.URL = [NSURL URLWithString:ip];

            // 添加原始URL的host

            [mutableReq setValue:url.host forHTTPHeaderField:@"host"];

            // 添加originalUrl保存原始URL

            [mutableReq addValue:originalUrl forHTTPHeaderField:@"originalUrl"];

        }

    }

    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

    self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];

    NSURLSessionTask *task = [_session dataTaskWithRequest:mutableReq];

    [task resume];

}

在NSURLProtocol中拦截了请求后,在重新发起NSURLSession代理方法中,我们将证书校验的Host重新改回域名,这样就会通过证书校验过程。

#pragma NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {

    if (!challenge) {

        return;

    }

    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;

    NSURLCredential *credential = nil;

    /*

     * 获取原始域名信息。

     */

    NSString *host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];

    if (!host) {

        host = self.request.URL.host;

    }

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {

        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {

            disposition = NSURLSessionAuthChallengeUseCredential;

            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        } else {

            disposition = NSURLSessionAuthChallengePerformDefaultHandling;

        }

    } else {

        disposition = NSURLSessionAuthChallengePerformDefaultHandling;

    }

    // 对于其他的challenges直接使用默认的验证方案

    completionHandler(disposition, credential);

}

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • 移动互联网的网络状况是十分复杂的,三大运营商、3G、4G、Wi-Fi、地点等任何一个状态的改变都会导致网络状况的变...
    Joy___阅读 10,220评论 4 89
  • ******科普片** 1、DNS劫持的危害 不知道大家有没有发现这样一个现象,在打开一些网页的时候会弹出一些与所...
    茉莉儿阅读 30,809评论 84 214
  • 看了Joy一篇关于网络部分优化的文章,总结一下,方便以后查阅使用 目前客户端存在的网络问题主要有下面几方面: 1....
    SpursGo阅读 3,594评论 1 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能...
    一直在努力hard阅读 4,590评论 3 19