摘要: 「IP直連方案」主要在於解決DNS污染、省去DNS解析時間,一般狀況下咱們能夠在項目中使用 NSURLProtocol 攔截 NSURLSession 請求,下面將支持 Post 請求中面臨的一個挑戰,以及應對策略介紹一下。html
「IP直連方案」主要在於解決DNS污染、省去DNS解析時間,一般狀況下咱們能夠在項目中使用 NSURLProtocol 攔截 NSURLSession 請求,下面將支持 Post 請求中面臨的一個挑戰,以及應對策略介紹一下:ios
在支持POST請求過程當中會遇到丟失 body的 問題,有如下幾種解決方法:git
方案以下:github
1.換用 NSURLConnection
2.將 body 放進 Header 中
3.使用 HTTPBodyStream 獲取 body,並賦值到 body 中
4.換用 Get 請求,不使用 Post 請求。網絡
對方案作如下分析session
// // 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
上面是我給出的實現,這裏注意,剛開始有人作過這樣的實現:app
- (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]; } } }
這個實現的問題在於:不能用 [stream hasBytesAvailable]) 判斷,處理圖片文件的時候這裏的[stream hasBytesAvailable]會始終返回YES,致使在while裏面死循環。curl
Apple的文檔也說得很清楚:ide
// returns in O(1) a pointer to the buffer in 'buffer' and by reference in 'len' how many bytes are available. This buffer is only valid until the next stream operation. Subclassers may return NO for this if it is not appropriate for the stream type. This may return NO if the buffer is not available. @property (readonly) BOOL hasBytesAvailable;
給出了實現,下面介紹下使用方法:工具
在用於攔截請求的 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 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。
參考連接:
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。
閱讀更多幹貨好文,請關注掃描如下二維碼: