使用CFNetwork進(jìn)行HTTP請求

背景

CFNetwork是比BSD套接字層級高,比Foundation的NSURLSession層級低的網(wǎng)絡(luò)API。CFNetwork更側(cè)重于網(wǎng)絡(luò)協(xié)議,而Foundation級別API側(cè)重于數(shù)據(jù)訪問,例如通過HTTP或FTP傳輸數(shù)據(jù)。雖然NSURLSession使用起來更方便,但是對網(wǎng)絡(luò)協(xié)議的可控性較低,這在iOS下使用HttpDNS進(jìn)行IP直連避免DNS劫持中針對服務(wù)器使用多個域名和證書問題卻沒有解決辦法,需要依靠低一層的CFNetwork去解決這個問題。

關(guān)鍵流程

創(chuàng)建請求

在握手之前設(shè)置SNI(iOS下使用HttpDNS進(jìn)行IP直連避免DNS劫持第四個注意事項)??蛻舳嗽诎l(fā)起 SSL 握手請求時(具體說來,是客戶端發(fā)出 SSL 請求中的 ClientHello 階段),就提交請求的 Host 信息,使得服務(wù)器能夠切換到正確的域并返回相應(yīng)的證書。

// HTTPS請求處理SNI場景
if ([self isHTTPSScheme]) {
    // 設(shè)置SNI host信息
    NSString *host = [self.swizzleRequest.allHTTPHeaderFields objectForKey:@"host"];
    if (!host) {
        host = self.originalRequest.URL.host;
    }
    [self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
    NSDictionary *sslProperties = @{ (__bridge id) kCFStreamSSLPeerName : host };
    [self.inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
}

然后通過wireshare抓取SSL握手中clientHello報文,查看其中的Server Name Indication extension字段的內(nèi)容進(jìn)行驗證:


屏幕快照 2019-05-27 上午12.37.17.png

目前有疑問:
1> 使用Safari進(jìn)行IP直連,SNI中是IP地址;使用Chrome進(jìn)行IP直連,沒有設(shè)置SNI。

讀取數(shù)據(jù)流

使用CFNetwork與NSURLSession的的最大區(qū)別就是需要自己來維護數(shù)據(jù)的讀?。?/p>

{
    // 創(chuàng)建CFHTTPMessage對象的輸入流
    CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfRequest);
    self.inputStream = (__bridge_transfer NSInputStream *) readStream;
    
   // 打開流
    __weak typeof(self) weakSelf = self;
    self.runloop = [NSRunLoop currentRunLoop];
    [self startTimer];
    [self.inputStream setDelegate:weakSelf];
    [self.inputStream scheduleInRunLoop:self.runloop forMode:[self runloopMode]];
    [self.inputStream open];
}

在從流中讀取數(shù)據(jù)的時候,可能會等待很長時間,如果使用同步讀取,那么app會強制等待數(shù)據(jù)傳輸,因此需要使用非阻塞讀取數(shù)據(jù)的方法,iOS推薦使用runLoop來實現(xiàn)非阻塞讀取?!?scheduleInRunLoop:forMode:”就實現(xiàn)了通過runLoop來避免阻塞讀取。
大致看一下"-scheduleInRunLoop:forMode:"實現(xiàn)了一個什么效果,runLoop是當(dāng)前線程的runLoop,當(dāng)前線程為:

(lldb) po [NSThread currentThread]
<NSThread: 0x600001ad9100>{number = 3, name = com.apple.CFNetwork.CustomProtocols}

通過觀察"-scheduleInRunLoop:forMode:"執(zhí)行前后runLoop中多出來的東西,就可以判斷出該方法向runLoop中注冊了什么內(nèi)容,經(jīng)過驗證,是向runLoop中注冊了一個source0:

<CFRunLoopSource 0x600003a53a80 [0x111416b68]>{signalled = Yes, valid = Yes, order = 0, context = (
    "<__NSCFInputStream: 0x600003d5b3c0>",
    "<__NSCFInputStream: 0x600003d53330>",
    "<__NSCFOutputStream: 0x600003d522e0>"
)

當(dāng)有數(shù)據(jù)可讀的時候,當(dāng)前線程上的source0就會被激活,然后當(dāng)前線程的runLoop被喚醒,執(zhí)行source0的回調(diào),這個回調(diào)中就會執(zhí)行self.inputStream的
delegate的方法"-stream:handleEvent:"。在有數(shù)據(jù)可讀的時候,讀取數(shù)據(jù),保存進(jìn)本地緩存self.resultData中。

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    switch (eventCode) {
        case NSStreamEventOpenCompleted:
            //NSLog(@"InputStream opened success.");
            break;
        case NSStreamEventHasBytesAvailable:
        {
            if (![self analyseResponse]) {
                return;
            }
            UInt8 buffer[BUFFER_SIZE];
            NSInteger numBytesRead = 0;
            NSInputStream *inputstream = (NSInputStream *) aStream;
            // Read data
            do {
                numBytesRead = [inputstream read:buffer maxLength:sizeof(buffer)];
                if (numBytesRead > 0) {
                    [self.resultData appendBytes:buffer length:numBytesRead];
                }
            } while (numBytesRead > 0);
        }
            break;
        case NSStreamEventErrorOccurred:
            self.completed = YES;
            [self.delegate task:self didCompleteWithError:[aStream streamError]];
            break;
        case NSStreamEventEndEncountered:
            self.completed = YES;
            if (!self.responseAlreadyAnalysed) {
                if (![self analyseResponse]) {
                    return;
                }
            }
            [self handleResult];
            break;
        default:
            break;
    }
}
處理數(shù)據(jù)

在self.inputStream的代理delegate的方法"-stream:handleEvent:"中eventCode為NSStreamEventEndEncountered時,標(biāo)識數(shù)據(jù)讀取完成,這時需要處理數(shù)據(jù),處理數(shù)據(jù)分為兩部分,第一部分是響應(yīng)頭,第二部分是實體主體。

處理響應(yīng)頭

首先從self.inputStream中讀取響應(yīng)頭

CFReadStreamRef readStream = (__bridge CFReadStreamRef) self.inputStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (!message) {
    return NO;
}
result = CFHTTPMessageIsHeaderComplete(message);

然后判斷是否需要進(jìn)行重定向,如果返回狀態(tài)碼為301,302,303則進(jìn)行重定向,

- (BOOL)needRedirection {
    BOOL needRedirect = NO;
    switch (self.response.statusCode) {
            // 永久重定向
        case 301:
            // 暫時重定向
        case 302:
            // POST重定向GET
        case 303:
        {
            NSString *location = self.response.headerFields[@"Location"];
            if (location) {
                NSURL *url = [[NSURL alloc] initWithString:location];
                NSMutableURLRequest *mRequest = [self.swizzleRequest mutableCopy];
                mRequest.URL = url;
                if ([[self.swizzleRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
                    // POST重定向為GET
                    mRequest.HTTPMethod = @"GET";
                    mRequest.HTTPBody = nil;
                }
                [mRequest setValue:nil forHTTPHeaderField:@"host"];
                self.redirectRequest = mRequest;
                needRedirect = YES;
                break;
            }
        }
            // POST不重定向為GET,詢問用戶是否攜帶POST數(shù)據(jù)(很少使用)
            //case 307:
            //    break;
        default:
            break;
    }
    return needRedirect;
}

如果是HTTPS協(xié)議,則需要校驗證書,校驗證書的時候需要獲取request的header中的host字段的值(iOS下IP直連避免DNS劫持第一個注意事項)來與服務(wù)器證書中的域名進(jìn)行比較(iOS下IP直連避免DNS劫持第三個注意事項)。

// HTTPS校驗證書
if ([self isHTTPSScheme]) {
    SecTrustRef trust = (__bridge SecTrustRef) [self.inputStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust];
    SecTrustResultType res = kSecTrustResultInvalid;
    NSMutableArray *policies = [NSMutableArray array];
    NSString *domain = [[self.swizzleRequest allHTTPHeaderFields] valueForKey:@"host"];
    if (domain) {
        [policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
    } else {
        [policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
    }
    // 綁定校驗策略到服務(wù)端的證書上
    SecTrustSetPolicies(trust, (__bridge CFArrayRef) policies);
    if (SecTrustEvaluate(trust, &res) != errSecSuccess) {
        [self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]];
        result = NO;
    } else if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
        // 證書驗證不通過
        [self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
        result = NO;
    }
}
處理實體主體

處理實體主體需要注意的只有1點,就是當(dāng)響應(yīng)頭中的"Content-Encoding"為"gzip"時,需要進(jìn)行解壓。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容