Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS 防 DNS 污染方案调研(二)--- SNI 业务场景 #12

Open
ChenYilong opened this issue Jul 20, 2017 · 7 comments
Open

iOS 防 DNS 污染方案调研(二)--- SNI 业务场景 #12

ChenYilong opened this issue Jul 20, 2017 · 7 comments
Labels

Comments

@ChenYilong
Copy link
Owner

ChenYilong commented Jul 20, 2017

iOS 防 DNS 污染方案调研--- SNI 业务场景

对应的GitHub仓库镜像地址在这里 ,欢迎提PR进行修改。

概述

SNI(单IP多HTTPS证书)场景下,iOS上层网络库 NSURLConnection/NSURLSession 没有提供接口进行 SNI 字段 配置,因此需要 Socket 层级的底层网络库例如 CFNetwork,来实现 IP 直连网络请求适配方案。而基于 CFNetwork 的解决方案需要开发者考虑数据的收发、重定向、解码、缓存等问题(CFNetwork是非常底层的网络实现)。

针对 SNI 场景的方案, Socket 层级的底层网络库,大致有两种:

  • 基于 CFNetWork ,hook 证书校验步骤。
  • 基于原生支持设置 SNI 字段的更底层的库,比如 libcurl。

下面将目前面临的一些挑战,以及应对策略介绍一下:

支持 Post 请求

使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body,故有以下几种解决方法:

方案如下:

  1. 换用 NSURLConnection
  2. 将 body 放进 Header 中
  3. 使用 HTTPBodyStream 获取 body,并赋值到 body 中
  4. 换用 Get 请求,不使用 Post 请求。

对方案做以下分析

  • 换用 NSURLConnection ,不多说了,与 NSURLSession 相比终究会被淘汰,不作考虑。
  • body放header的方法,2M以下没问题,超过2M会导致请求延迟,超过 10M 就直接 Request timeout。而且无法解决 Body 为二进制数据的问题,因为Header里都是文本数据。
  • 换用 Get 请求,不使用 Post 请求。这个也是可行的,但是毕竟对请求方式有限制,终究还是要解决 Post 请求所存在的问题。如果是基于旧项目做修改,则侵入性太大。这种方案适合新的项目。
  • 另一种方法是我们下面主要要讲的,使用 HTTPBodyStream 获取 body,并赋值到 body 中,具体的代码如下,可以解决上面提到的问题:
//
//  NSURLRequest+CYLNSURLProtocolExtension.h
//
//
//  Created by ElonChan on 28/07/2017.
//  Copyright © 2017 ChenYilong. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSURLRequest (CYLNSURLProtocolExtension)

- (NSURLRequest *)cyl_getPostRequestIncludeBody;

@end



//
//  NSURLRequest+CYLNSURLProtocolExtension.h
//
//
//  Created by ElonChan on 28/07/2017.
//  Copyright © 2017 ChenYilong. All rights reserved.
//

#import "NSURLRequest+CYLNSURLProtocolExtension.h"

@implementation NSURLRequest (CYLNSURLProtocolExtension)

- (NSURLRequest *)cyl_getPostRequestIncludeBody {
   return [[self cyl_getMutablePostRequestIncludeBody] copy];
}

- (NSMutableURLRequest *)cyl_getMutablePostRequestIncludeBody {
   NSMutableURLRequest * req = [self mutableCopy];
   if ([self.HTTPMethod isEqualToString:@"POST"]) {
       if (!self.HTTPBody) {
           NSInteger maxLength = 1024;
           uint8_t d[maxLength];
           NSInputStream *stream = self.HTTPBodyStream;
           NSMutableData *data = [[NSMutableData alloc] init];
           [stream open];
           BOOL endOfStreamReached = NO;
           //不能用 [stream hasBytesAvailable]) 判断,处理图片文件的时候这里的[stream hasBytesAvailable]会始终返回YES,导致在while里面死循环。
           while (!endOfStreamReached) {
               NSInteger bytesRead = [stream read:d maxLength:maxLength];
               if (bytesRead == 0) { //文件读取到最后
                   endOfStreamReached = YES;
               } else if (bytesRead == -1) { //文件读取错误
                   endOfStreamReached = YES;
               } else if (stream.streamError == nil) {
                   [data appendBytes:(void *)d length:bytesRead];
               }
           }
           req.HTTPBody = [data copy];
           [stream close];
       }
       
   }
   return req;
}
@end

使用方法:

在用于拦截请求的 NSURLProtocol 的子类中实现方法 +canonicalRequestForRequest: 并处理 request 对象:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
   return [request cyl_getPostRequestIncludeBody];
}

下面介绍下相关方法的作用:

//NSURLProtocol.h

/*! 
   @method canInitWithRequest:
   @abstract This method determines whether this protocol can handle
   the given request.
   @discussion A concrete subclass should inspect the given request and
   determine whether or not the implementation can perform a load with
   that request. This is an abstract method. Sublasses must provide an
   implementation.
   @param request A request to inspect.
   @result YES if the protocol can handle the given request, NO if not.
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

/*! 
   @method canonicalRequestForRequest:
   @abstract This method returns a canonical version of the given
   request.
   @discussion It is up to each concrete protocol implementation to
   define what "canonical" means. However, a protocol should
   guarantee that the same input request always yields the same
   canonical form. Special consideration should be given when
   implementing this method since the canonical form of a request is
   used to look up objects in the URL cache, a process which performs
   equality checks between NSURLRequest objects.
   <p>
   This is an abstract method; sublasses must provide an
   implementation.
   @param request A request to make canonical.
   @result The canonical form of the given request. 
*/
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

翻译下:

//NSURLProtocol.h
/*!
*  @method:创建NSURLProtocol实例,NSURLProtocol注册之后,所有的NSURLConnection都会通过这个方法检查是否持有该Http请求。
@parma :
@return: YES:持有该Http请求NO:不持有该Http请求
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request

/*!
*  @method: NSURLProtocol抽象类必须要实现。通常情况下这里有一个最低的标准:即输入输出请求满足最基本的协议规范一致。因此这里简单的做法可以直接返回。一般情况下我们是不会去更改这个请求的。如果你想更改,比如给这个request添加一个title,组合成一个新的http请求。
@parma: 本地HttpRequest请求:request
@return:直接转发
*/

+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)request

简单说:

  • +[NSURLProtocol canInitWithRequest:] 负责筛选哪些网络请求需要被拦截
  • +[NSURLProtocol canonicalRequestForRequest:] 负责对需要拦截的网络请求NSURLRequest 进行重新构造。

这里有一个注意点:+[NSURLProtocol canonicalRequestForRequest:] 的执行条件是 +[NSURLProtocol canInitWithRequest:] 返回值为 YES

注意在拦截 NSURLSession 请求时,需要将用于拦截请求的 NSURLProtocol 的子类添加到 NSURLSessionConfiguration 中,用法如下:

    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSArray *protocolArray = @[ [CYLURLProtocol class] ];
    configuration.protocolClasses = protocolArray;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];

换用其他提供了SNI字段配置接口的更底层网络库

如果使用第三方网络库:curl, 中有一个 -resolve 方法可以实现使用指定 ip 访问 https 网站,iOS 中集成 curl 库,参考 curl文档

另外有一点也可以注意下,它也是支持 IPv6 环境的,只需要你在 build 时添加上 --enable-ipv6 即可。

curl 支持指定 SNI 字段,设置 SNI 时我们需要构造的参数形如: {HTTPS域名}:443:{IP地址}

假设你要访问. www.example.org ,若IP为 127.0.0.1 ,那么通过这个方式来调用来设置 SNI 即可:

curl *** --resolve 'www.example.org:443:127.0.0.1'

iOS CURL 库

使用libcurl 来解决,libcurl / cURL 至少 7.18.1 (2008年3月30日) 在 SNI 支持下编译一个 SSL/TLS 工具包,curl 中有一个 --resolve 方法可以实现使用指定ip访问https网站。

在iOS实现中,代码如下

   //{HTTPS域名}:443:{IP地址}
   NSString *curlHost = ...;
   _hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
   curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);

其中 curlHost 形如:

{HTTPS域名}:443:{IP地址}

_hosts_list 是结构体类型hosts_list,可以设置多个IP与Host之间的映射关系。curl_easy_setopt方法中传入CURLOPT_RESOLVE 将该映射设置到 HTTPS 请求中。

这样就可以达到设置SNI的目的。

我在这里写了一个 Demo:CYLCURLNetworking,里面包含了编译好的支持 IPv6 的 libcurl 包,演示了下如何通过curl来进行类似NSURLSession。

走过的弯路

误以为 iOS11 新 API 可以直接拦截 DNS 解析过程

参考:NEDNSProxyProvider:DNS based on HTTP supported in iOS11

参考链接:

补充说明

文中提到的几个概念:

概念 解释 举例
host 可以是 IP 也可以是 FQDN。 www.xxx.com 或 1.1.1.1
FQDN fully qualified domain name,由主机名和域名两部分组成 www.xxx.com
域名 域名分为全称和简称,全称就是FQDN、简称就是 FQDN 不包括主机名的部分 比如:xxx.com ,也就是www.xxx.com 这个 FQDN 中,www 是主机名,xxx.com 是域名。

文中部分提到的域名,如果没有特殊说明均指的是 FQDN。

@ChenYilong ChenYilong changed the title iOS SNI 场景下的防 DNS 污染方案调研 防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景 Aug 8, 2017
@ChenYilong ChenYilong changed the title 防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景 iOS SNI 场景下的防 DNS 污染方案调研 Aug 8, 2017
@ChenYilong ChenYilong changed the title iOS SNI 场景下的防 DNS 污染方案调研 防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景(二)-- iOS SNI 场景下的防 DNS 污染方案调研 Aug 8, 2017
@ChenYilong ChenYilong changed the title 防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景(二)-- iOS SNI 场景下的防 DNS 污染方案调研 防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景(二)-- SNI 场景 Aug 8, 2017
@ChenYilong ChenYilong changed the title 防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景(二)-- SNI 场景 iOS 防 DNS 污染方案调研(二)--- SNI 业务场景 Aug 8, 2017
@ChenYilong
Copy link
Owner Author

ChenYilong commented Aug 9, 2017

//TODO: 测试原生NSURLSession请求,与 NSURLProtocol 拦截NSURLSession请求,两种方式之间的性能对比,可以直接基于NSURLSession的回调,回调前与回调后之差进行统计。相比于 WebView 更佳统计,WebView 用 NSURLProtocol 拦截后是黑盒,而NSURLSession能看到回调。基于测试,是制作成图。并对比body大小不同的性能差距。

对于拦截网络请求带来的编解码问题,需要手动编解码,比如 gzip 编码,需要在接收到response 后进行 gzip 解码,再传回。

@ChenYilong
Copy link
Owner Author

ChenYilong commented Aug 24, 2017

上面使用 +[NSURLProtocol canonicalRequestForRequest:] 并处理 request 对象的效果等同于在-[NSURLProtocol startLoading]中处理 request 对象。

/**
* 开始加载,在该方法中,加载一个请求
*/
- (void)startLoading {
   NSMutableURLRequest *request = [self.request mutableCopy];
   [request cyl_handlePostRequestBody];
   // 表示该请求已经被处理,防止无限循环
   [NSURLProtocol setProperty:@(YES) forKey:CYL_NSURLPROTOCOL_REQUESTED_FLAG_KEY inRequest:request];
   [self startRequest];
}

其中 -cyl_handlePostRequestBody 方法的实现如下:

//
//  NSURLRequest+CYLNSURLProtocolExtension.h
//
//
//  Created by ElonChan on 28/07/2017.
//  Copyright © 2017 ChenYilong. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSMutableURLRequest (CYLNSURLProtocolExtension)

- (void)cyl_handlePostRequestBody;

@end
@implementation NSMutableURLRequest (CYLNSURLProtocolExtension)

- (void)cyl_handlePostRequestBody {
   if ([self.HTTPMethod isEqualToString:@"POST"]) {
       if (!self.HTTPBody) {
           uint8_t d[1024] = {0};
           NSInputStream *stream = self.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];
               }
           }
           self.HTTPBody = [data copy];
           [stream close];
       }
   }
}

@end

两种方法的实现效果一致,推荐在 +[NSURLProtocol canonicalRequestForRequest:] 中处理。

@ChenYilong
Copy link
Owner Author

ChenYilong commented Aug 28, 2017

下面测试下添加 SNI 策略后,与原生网络请求相比,对性能的影响。

测试数据如下:

正常wifi网络环境下,iPhone 6s plus - iOS9.3 系统:

SNI 场景:

IP 直连:

IP 直连:

0.161788+0.156012+0.220671+0.149536+0.160505+0.158274+0.200679+0.164229+0.173180+0.171909+0.177469+0.195068+0.166481+0.165608+0.183510+0.183593+0.171261+0.176552+0.194354+0.172321+0.179747+0.166126+0.213673+0.173284+0.183772+0.154173+0.156247+0.159818+0.186316+0.203103=5.279259/30=.1759753

详细数据:

2017-08-25 14:53:30.155  executionTime(执行时间) = 0.161788
2017-08-25 14:53:31.800  executionTime(执行时间) = 0.156012
2017-08-25 14:53:33.127  executionTime(执行时间) = 0.220671
2017-08-25 14:53:34.342  executionTime(执行时间) = 0.149536
2017-08-25 14:53:36.323  executionTime(执行时间) = 0.160505
2017-08-25 14:53:38.031  executionTime(执行时间) = 0.158274
2017-08-25 14:53:39.254  executionTime(执行时间) = 0.200679
2017-08-25 14:53:40.709  executionTime(执行时间) = 0.164229
2017-08-25 14:53:42.867  executionTime(执行时间) = 0.173180
2017-08-25 14:53:45.073  executionTime(执行时间) = 0.171909
2017-08-25 14:53:46.673  executionTime(执行时间) = 0.177469
2017-08-25 14:53:48.289  executionTime(执行时间) = 0.195068
2017-08-25 14:53:49.404  executionTime(执行时间) = 0.166481
2017-08-25 14:53:50.663  executionTime(执行时间) = 0.165608
2017-08-25 14:53:51.849  executionTime(执行时间) = 0.183510
2017-08-25 14:53:53.428  executionTime(执行时间) = 0.183593
2017-08-25 14:53:55.366  executionTime(执行时间) = 0.171261
2017-08-25 14:53:57.672  executionTime(执行时间) = 0.176552
2017-08-25 14:53:58.749  executionTime(执行时间) = 0.194354
2017-08-25 14:54:00.337  executionTime(执行时间) = 0.172321
2017-08-25 14:54:02.393  executionTime(执行时间) = 0.179747
2017-08-25 14:54:04.311  executionTime(执行时间) = 0.166126
2017-08-25 14:54:05.910  executionTime(执行时间) = 0.213673
2017-08-25 14:54:07.224  executionTime(执行时间) = 0.173284
2017-08-25 14:54:10.001  executionTime(执行时间) = 0.183772
2017-08-25 14:54:11.850  executionTime(执行时间) = 0.154173
2017-08-25 14:54:14.316  executionTime(执行时间) = 0.156247
2017-08-25 14:54:16.927  executionTime(执行时间) = 0.159818
2017-08-25 14:54:18.834  executionTime(执行时间) = 0.186316
2017-08-25 14:54:21.505  executionTime(执行时间) = 0.203103



4.076027

非 IP 直连的方案:

非 IP 直连的方案:

0.152328+1.722805+0.160652+0.131074+0.155207+0.141891+0.134469+0.145446+0.198126+0.158850+0.138900+0.144652+0.137210+0.141466+0.135740+0.132003+0.142922+0.166419+0.153931+0.128710+0.150666+0.139668+0.136447+0.139566+0.141225+0.141087+0.153604+0.137701+0.140645+0.137058=5.940468/30=.1980156


2017-08-25 11:36:52.532 executionTime(执行时间) = 0.152328
2017-08-25 11:37:09.077 executionTime(执行时间) = 1.722805
2017-08-25 11:37:12.116 executionTime(执行时间) = 0.160652
2017-08-25 11:37:14.482 executionTime(执行时间) = 0.131074
2017-08-25 11:37:17.019 executionTime(执行时间) = 0.155207
2017-08-25 11:37:19.220 executionTime(执行时间) = 0.141891
2017-08-25 11:37:21.694 executionTime(执行时间) = 0.134469
2017-08-25 11:37:23.996 executionTime(执行时间) = 0.145446
2017-08-25 11:37:25.950 executionTime(执行时间) = 0.198126
2017-08-25 11:37:27.584 executionTime(执行时间) = 0.158850
2017-08-25 11:37:29.699 executionTime(执行时间) = 0.138900
2017-08-25 11:37:32.145 executionTime(执行时间) = 0.144652
2017-08-25 11:37:34.440 executionTime(执行时间) = 0.137210
2017-08-25 11:37:36.392 executionTime(执行时间) = 0.141466
2017-08-25 11:37:38.088 executionTime(执行时间) = 0.135740
2017-08-25 11:37:39.959 executionTime(执行时间) = 0.132003
2017-08-25 11:37:41.451 executionTime(执行时间) = 0.142922
2017-08-25 11:37:43.430 executionTime(执行时间) = 0.166419
2017-08-25 11:37:45.767 executionTime(执行时间) = 0.153931
2017-08-25 11:37:47.454 executionTime(执行时间) = 0.128710
2017-08-25 11:37:49.401 executionTime(执行时间) = 0.150666
2017-08-25 11:37:51.162 executionTime(执行时间) = 0.139668
2017-08-25 11:37:54.506 executionTime(执行时间) = 0.136447
2017-08-25 11:37:56.696 executionTime(执行时间) = 0.139566
2017-08-25 11:37:59.650 executionTime(执行时间) = 0.141225
2017-08-25 11:38:01.720 executionTime(执行时间) = 0.141087
2017-08-25 11:38:03.672 executionTime(执行时间) = 0.153604
2017-08-25 11:38:05.040 executionTime(执行时间) = 0.137701
2017-08-25 11:38:06.457 executionTime(执行时间) = 0.140645
2017-08-25 11:38:07.853 executionTime(执行时间) = 0.137058
2017-08-25 11:38:09.871 executionTime(执行时间) = 0.146094
2017-08-25 11:38:19.822 executionTime(执行时间) = 0.160912
2017-08-25 11:38:22.341 executionTime(执行时间) = 0.138762

测试结果显示两个性能差别并不大,SNI 解决方案平均0.1759753秒 原生网络请求:0.1980156秒。

注意:测试采用的是密集的网络请求方式,每次网络请求间隔较短,如果间隔时间很大,比如10分钟,由于 TCP 通道不会被复用,波动可能较大,达到5秒、6秒级别。测试的代码部分,不涉及 TCP 通道复用部分,应该避免该部分的干扰,故采用密集的网络请求方式。另外SNI 解决方案中,解析出的 IP 有 TTL 过期时间属性,如果 TTL 过期会走原生的网络请求部分,以上测试期间 IP 均未过期。

@ChenYilong
Copy link
Owner Author

ChenYilong commented Aug 28, 2017

基于 CFNetWork 有性能瓶颈

方案:

  1. 调研性能瓶颈的原因
  2. 换用其他提供了SNI字段配置接口的更底层网络库。

调研性能瓶颈的原因

在使用 CFNetWork 实现了基本的 SNI 解决方案后,虽然问题解决了,但是遇到了性能瓶颈,对比 NSURLConnection/NSURLSession ,打开流到结束流时间明显更长。介绍下对比性能时的调研方法:

/one more thing/

调研性能瓶颈的方法

可以使用下面的方法,做一个简单的打点,将流开始和流结束记录下。

记录的数据如下:

key from to value
请求的序列号 开始时间戳 结束时间戳 耗时
#import <Foundation/Foundation.h>

@interface CYLRequestTimeMonitor : NSObject

+ (NSString *)requestBeginTimeKeyWithID:(NSUInteger)ID;
+ (NSString *)requestEndTimeKeyWithID:(NSUInteger)ID;
+ (NSString *)requestSpentTimeKeyWithID:(NSUInteger)ID;
+ (NSString *)getKey:(NSString *)key ID:(NSUInteger)ID;
+ (NSUInteger)timeFromKey:(NSString *)key;
+ (NSUInteger)frontRequetNumber;
+ (NSUInteger)changeToNextRequetNumber;
+ (void)setCurrentTimeForKey:(NSString *)key taskID:(NSUInteger)taskID time:(NSTimeInterval *)time;
+ (void)setTime:(NSUInteger)time key:(NSString *)key taskID:(NSUInteger)taskID;

+ (void)setBeginTimeForTaskID:(NSUInteger)taskID;
+ (void)setEndTimeForTaskID:(NSUInteger)taskID;
+ (void)setSpentTimeForKey:(NSString *)key endTime:(NSUInteger)endTime taskID:(NSUInteger)taskID;
   
@end
#import "CYLRequestTimeMonitor.h"

@implementation CYLRequestTimeMonitor

static NSString *const CYLRequestFrontNumber = @"CYLRequestFrontNumber";
static NSString *const CYLRequestBeginTime = @"CYLRequestBeginTime";
static NSString *const CYLRequestEndTime = @"CYLRequestEndTime";
static NSString *const CYLRequestSpentTime = @"CYLRequestSpentTime";

+ (NSString *)requestBeginTimeKeyWithID:(NSUInteger)ID {
   return [self getKey:CYLRequestBeginTime ID:ID];
}

+ (NSString *)requestEndTimeKeyWithID:(NSUInteger)ID {
   return [self getKey:CYLRequestEndTime ID:ID];
}

+ (NSString *)requestSpentTimeKeyWithID:(NSUInteger)ID {
   return [self getKey:CYLRequestSpentTime ID:ID];
}

+ (NSString *)getKey:(NSString *)key ID:(NSUInteger)ID {
   NSString *timeKeyWithID = [NSString stringWithFormat:@"%@-%@", @(ID), key];
   return timeKeyWithID;
}

+ (NSUInteger)timeFromKey:(NSString *)key {
   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
   NSUInteger time = [defaults integerForKey:key];
   return time ?: 0;
}

+ (NSUInteger)frontRequetNumber {
   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
   NSUInteger frontNumber = [defaults integerForKey:CYLRequestFrontNumber];
   return frontNumber ?: 0;
}

+ (NSUInteger)changeToNextRequetNumber {
   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
   NSUInteger nextNumber = ([self frontRequetNumber]+ 1);
   [defaults setInteger:nextNumber forKey:CYLRequestFrontNumber];
   [defaults synchronize];
   return nextNumber;
}

+ (void)setCurrentTimeForKey:(NSString *)key taskID:(NSUInteger)taskID time:(NSTimeInterval *)time {
   NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]*1000;
   *time = currentTime;
   [self setTime:currentTime key:key taskID:taskID];
}

+ (void)setTime:(NSUInteger)time key:(NSString *)key taskID:(NSUInteger)taskID {
   NSString *keyWithID = [self getKey:key ID:taskID];
   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
   [defaults setInteger:time forKey:keyWithID];
   [defaults synchronize];
}

+ (void)setBeginTimeForTaskID:(NSUInteger)taskID {
   NSTimeInterval begin;
   [self setCurrentTimeForKey:CYLRequestBeginTime taskID:taskID time:&begin];
}

+ (void)setEndTimeForTaskID:(NSUInteger)taskID {
   NSTimeInterval endTime = 0;
   [self setCurrentTimeForKey:CYLRequestEndTime taskID:taskID time:&endTime];
   [self setSpentTimeForKey:CYLRequestSpentTime endTime:endTime taskID:taskID];
}

+ (void)setSpentTimeForKey:(NSString *)key endTime:(NSUInteger)endTime taskID:(NSUInteger)taskID {
   NSString *beginTimeString = [self requestBeginTimeKeyWithID:taskID];
   NSUInteger beginTime = [self timeFromKey:beginTimeString];
   NSUInteger spentTime = endTime - beginTime;
   [self setTime:spentTime key:CYLRequestSpentTime taskID:taskID];
}

@end

NSURLConnection 的打点位置如下:

这里普通的做法就是继承 NSURLProtocol 这个类写一个子类,然后在子类中实现NSURLConnectionDelegate 的那五个代理方法。 

- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
//  这个方法里可以做计时的开始

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
//  这里可以得到返回包的总大小

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
//  这里将每次的data累加起来,可以做加载进度圆环之类的

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
//  这里作为结束的时间

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
//  错误的收集 

NSURLSession 类似。

然后在自定义CFNetwork的下面两个方法中打点:流开始和流结束,命名大致如:-startLoading-didReceiveRedirection

发送相同的网络请求,然后通过对比两个的时间来观察性能。

瓶颈原因

@heiheiLqq
Copy link

heiheiLqq commented Mar 19, 2018

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -canInitWithRequest: only defined for abstract class. Define -[CUSTOMEURLProtocol canInitWithRequest:]!'

闪退报这个错

@feikang
Copy link

feikang commented Mar 22, 2019

你好,关于使用NSURLProtocol接管NSURLSession请求的bug ,可否这样处理。 把body通过objc_setAssociatedObject关联到request对象上。 这样在protocol中可以取到body。

@huangturen
Copy link

为啥我们获取到的httpBodyStream也是空的?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants