互联网架构

NSURLProtocol 拦截 NSURLSession 请求

2018-03-26  本文已影响17人  猫耳呀

摘要: “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直连”方案说明》 

《在 curl 中使用指定 ip 来进行请求 https》 

支持SNI与WebView的 alicloud-ios-demo 

《SNI: 实现多域名虚拟主机的SSL/TLS认证》

补充说明

注意以上讨论不涉及 WKWebView 中拦截 NSURLSession 请求的 body 丢失问题。

文中提到的几个概念:

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

原文链接

阅读更多干货好文,请关注扫描以下二维码: 

上一篇下一篇

猜你喜欢

热点阅读