iOSBlog
iOSBlog copied to clipboard
iOS 防 DNS 污染方案调研(二)--- SNI 业务场景
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,故有以下几种解决方法:
方案如下:
- 换用 NSURLConnection
- 将 body 放进 Header 中
- 使用 HTTPBodyStream 获取 body,并赋值到 body 中
- 换用 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
参考链接:
- Apple - Communicating with HTTP Servers
- Apple - HTTPS Server Trust Evaluation - Server Name Failures
- Apple - HTTPS Server Trust Evaluation - Trusting One Specific Certificate
- 《HTTPDNS > 最佳实践 > HTTPS(含SNI)业务场景“IP直连”方案说明 HTTPS(含SNI)业务场景“IP直连”方案说明》
- 《在 curl 中使用指定 ip 来进行请求 https》
- 支持SNI与WebView的 alicloud-ios-demo
- 《SNI: 实现多域名虚拟主机的SSL/TLS认证》
补充说明
文中提到的几个概念:
概念 | 解释 | 举例 |
---|---|---|
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。
//TODO: 测试原生NSURLSession请求,与 NSURLProtocol 拦截NSURLSession请求,两种方式之间的性能对比,可以直接基于NSURLSession的回调,回调前与回调后之差进行统计。相比于 WebView 更佳统计,WebView 用 NSURLProtocol 拦截后是黑盒,而NSURLSession能看到回调。基于测试,是制作成图。并对比body大小不同的性能差距。
对于拦截网络请求带来的编解码问题,需要手动编解码,比如 gzip 编码,需要在接收到response 后进行 gzip 解码,再传回。
上面使用 +[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:]
中处理。
下面测试下添加 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 均未过期。
基于 CFNetWork 有性能瓶颈
方案:
- 调研性能瓶颈的原因
- 换用其他提供了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
。
发送相同的网络请求,然后通过对比两个的时间来观察性能。
瓶颈原因
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -canInitWithRequest: only defined for abstract class. Define -[CUSTOMEURLProtocol canInitWithRequest:]!'
闪退报这个错
你好,关于使用NSURLProtocol接管NSURLSession请求的bug ,可否这样处理。 把body通过objc_setAssociatedObject关联到request对象上。 这样在protocol中可以取到body。
为啥我们获取到的httpBodyStream也是空的?