背景
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)行驗證:

目前有疑問:
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)行解壓。