iOSBlog icon indicating copy to clipboard operation
iOSBlog copied to clipboard

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

Open ChenYilong opened this issue 7 years ago • 7 comments

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 avatar Jul 20 '17 08:07 ChenYilong

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

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

ChenYilong avatar Aug 09 '17 08:08 ChenYilong

上面使用 +[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 avatar Aug 24 '17 09:08 ChenYilong

下面测试下添加 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 avatar Aug 28 '17 01:08 ChenYilong

基于 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

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

瓶颈原因

ChenYilong avatar Aug 28 '17 03:08 ChenYilong

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

闪退报这个错

heiheiLqq avatar Mar 19 '18 09:03 heiheiLqq

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

feikang avatar Mar 22 '19 07:03 feikang

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

huangturen avatar Jan 06 '21 01:01 huangturen