NSURLProtocol 拦截 NSURLSession 请求
摘要: “IP直连方案”主要在于解决DNS污染、省去DNS解析时间,通常情况下我们可以在项目中使用 NSURLProtocol 拦截 NSURLSession 请求,下面将支持 Post 请求中面临的一个挑战,以及应对策略介绍一下。
“IP直连方案”主要在于解决DNS污染、省去DNS解析时间,通常情况下我们可以在项目中使用 NSURLProtocol 拦截 NSURLSession 请求,下面将支持 Post 请求中面临的一个挑战,以及应对策略介绍一下:
在支持POST请求过程中会遇到丢失 body的 问题,有以下几种解决方法:
方案如下:
1.换用 NSURLConnection
2.将 body 放进 Header 中
3.使用 HTTPBodyStream 获取 body,并赋值到 body 中
4.换用 Get 请求,不使用 Post 请求。
对方案做以下分析
换用 NSURLConnection。NSURLConnection 与 NSURLSession
相比会遇到较多的性能问题,同时Apple的一些新特性也无法使用,终究会被淘汰,不作考虑。
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@interfaceNSURLRequest(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"@implementationNSURLRequest(CYLNSURLProtocolExtension)- (NSURLRequest*)cyl_getPostRequestIncludeBody {return[[selfcyl_getMutablePostRequestIncludeBody] copy];}- (NSMutableURLRequest *)cyl_getMutablePostRequestIncludeBody { NSMutableURLRequest * req = [selfmutableCopy];if([self.HTTPMethodisEqualToString:@"POST"]) {if(!self.HTTPBody) {NSIntegermaxLength =1024; uint8_t d[maxLength]; NSInputStream *stream =self.HTTPBodyStream; NSMutableData *data = [[NSMutableData alloc] init]; [stream open];BOOLendOfStreamReached =NO;//不能用 [stream hasBytesAvailable]) 判断,处理图片文件的时候这里的[stream hasBytesAvailable]会始终返回YES,导致在while里面死循环。while(!endOfStreamReached) {NSIntegerbytesRead = [stream read:d maxLength:maxLength];if(bytesRead ==0) {//文件读取到最后endOfStreamReached =YES; }elseif(bytesRead == -1) {//文件读取错误endOfStreamReached =YES; }elseif(stream.streamError==nil) { [data appendBytes:(void*)d length:bytesRead]; } } req.HTTPBody= [data copy]; [stream close]; } }returnreq;}@end
上面是我给出的实现,这里注意,刚开始有人做过这样的实现:
- (void)cyl_handlePostRequestBody {if([self.HTTPMethodisEqualToString:@"POST"]) {if(!self.HTTPBody) { uint8_t d[1024] = {0}; NSInputStream *stream =self.HTTPBodyStream; NSMutableData *data = [[NSMutableData alloc] init]; [stream open];while([stream hasBytesAvailable]) {NSIntegerlen = [stream read:d maxLength:1024];if(len >0&& stream.streamError==nil) { [data appendBytes:(void*)d length:len]; } }self.HTTPBody= [data copy]; [stream close]; } }}
这个实现的问题在于:不能用 [stream hasBytesAvailable]) 判断,处理图片文件的时候这里的[stream hasBytesAvailable]会始终返回YES,导致在while里面死循环。
Apple的文档也说得很清楚:
// returnsinO(1) a pointertothebufferin'buffer'andby referencein'len' how many bytes are available. Thisbufferisonly validuntilthenextstream operation. Subclassers mayreturnNOforthisifitisnotappropriateforthe streamtype. This mayreturnNOifthebufferisnotavailable. @property(readonly) BOOL hasBytesAvailable;
给出了实现,下面介绍下使用方法:
在用于拦截请求的 NSURLProtocol 的子类中实现方法 +canonicalRequestForRequest: 并处理 request 对象:
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {return[request cyl_getPostRequestIncludeBody];}
下面介绍下相关方法的作用:
//NSURLProtocol.h/*!@methodcanInitWithRequest:@abstractThis method determines whetherthisprotocol can handle the given request.@discussionA concrete subclass should inspect the given requestanddetermine whetherornotthe implementation can perform a loadwiththat request. Thisisan abstract method. Sublasses must provide an implementation.@paramrequest A request to inspect.@resultYESifthe protocol can handle the given request, NOifnot.*/+ (BOOL)canInitWithRequest:(NSURLRequest *)request;/*!@methodcanonicalRequestForRequest:@abstractThis method returns a canonical versionofthe given request.@discussionItisup 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 givenwhenimplementingthismethod since the canonical formofa requestisused to look up objectsinthe URL cache, a process which performs equality checks between NSURLRequest objects.
Thisisan abstract method; sublasses must provide an implementation.@paramrequest A request to make canonical.@resultThe canonical formofthe 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 = [NSURLSessionConfigurationdefaultSessionConfiguration];NSArray*protocolArray = @[ [CYLURLProtocolclass] ];configuration.protocolClasses = protocolArray;NSURLSession*session = [NSURLSessionsessionWithConfiguration:configurationdelegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
换用其他提供了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。
参考链接:
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直连”方案说明》
支持SNI与WebView的 alicloud-ios-demo
补充说明
注意以上讨论不涉及 WKWebView 中拦截 NSURLSession 请求的 body 丢失问题。
文中提到的几个概念:
文中部分提到的域名,如果没有特殊说明均指的是 FQDN。
阅读更多干货好文,请关注扫描以下二维码: