网络ios进阶

使用CFNetwork进行HTTP请求

2019-05-24  本文已影响0人  tom555cat

背景

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字段的内容进行验证:


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

目前有疑问:
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"时,需要进行解压。

上一篇下一篇

猜你喜欢

热点阅读