背景
CFNetwork是比BSD套接字层级高,比Foundation的NSURLSession层级低的网络API。CFNetwork更侧重于网络协议,而Foundation级别API侧重于数据访问,例如通过HTTP或FTP传输数据。虽然NSURLSession使用起来更方便,但是对网络协议的可控性较低,这在iOS下使用HttpDNS进行IP直连避免DNS劫持中针对服务器使用多个域名和证书问题却没有解决办法,需要依靠低一层的CFNetwork去解决这个问题。
关键流程
创建请求
在握手之前设置SNI(iOS下使用HttpDNS进行IP直连避免DNS劫持第四个注意事项)。客户端在发起 SSL 握手请求时(具体说来,是客户端发出 SSL 请求中的 ClientHello 阶段),就提交请求的 Host 信息,使得服务器能够切换到正确的域并返回相应的证书。
// HTTPS请求处理SNI场景
if ([self isHTTPSScheme]) {
// 设置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字段的内容进行验证:
目前有疑问:
1> 使用Safari进行IP直连,SNI中是IP地址;使用Chrome进行IP直连,没有设置SNI。
读取数据流
使用CFNetwork与NSURLSession的的最大区别就是需要自己来维护数据的读取:
{
// 创建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];
}
在从流中读取数据的时候,可能会等待很长时间,如果使用同步读取,那么app会强制等待数据传输,因此需要使用非阻塞读取数据的方法,iOS推荐使用runLoop来实现非阻塞读取。“-scheduleInRunLoop:forMode:”就实现了通过runLoop来避免阻塞读取。
大致看一下"-scheduleInRunLoop:forMode:"实现了一个什么效果,runLoop是当前线程的runLoop,当前线程为:
(lldb) po [NSThread currentThread]
<NSThread: 0x600001ad9100>{number = 3, name = com.apple.CFNetwork.CustomProtocols}
通过观察"-scheduleInRunLoop:forMode:"执行前后runLoop中多出来的东西,就可以判断出该方法向runLoop中注册了什么内容,经过验证,是向runLoop中注册了一个source0:
<CFRunLoopSource 0x600003a53a80 [0x111416b68]>{signalled = Yes, valid = Yes, order = 0, context = (
"<__NSCFInputStream: 0x600003d5b3c0>",
"<__NSCFInputStream: 0x600003d53330>",
"<__NSCFOutputStream: 0x600003d522e0>"
)
当有数据可读的时候,当前线程上的source0就会被激活,然后当前线程的runLoop被唤醒,执行source0的回调,这个回调中就会执行self.inputStream的
delegate的方法"-stream:handleEvent:"。在有数据可读的时候,读取数据,保存进本地缓存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;
}
}
处理数据
在self.inputStream的代理delegate的方法"-stream:handleEvent:"中eventCode为NSStreamEventEndEncountered时,标识数据读取完成,这时需要处理数据,处理数据分为两部分,第一部分是响应头,第二部分是实体主体。
处理响应头
首先从self.inputStream中读取响应头
CFReadStreamRef readStream = (__bridge CFReadStreamRef) self.inputStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (!message) {
return NO;
}
result = CFHTTPMessageIsHeaderComplete(message);
然后判断是否需要进行重定向,如果返回状态码为301,302,303则进行重定向,
- (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数据(很少使用)
//case 307:
// break;
default:
break;
}
return needRedirect;
}
如果是HTTPS协议,则需要校验证书,校验证书的时候需要获取request的header中的host字段的值(iOS下IP直连避免DNS劫持第一个注意事项)来与服务器证书中的域名进行比较(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()];
}
// 绑定校验策略到服务端的证书上
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点,就是当响应头中的"Content-Encoding"为"gzip"时,需要进行解压。