美文网首页网络ios进阶
使用CFNetwork进行HTTP请求

使用CFNetwork进行HTTP请求

作者: tom555cat | 来源:发表于2019-05-24 15:02 被阅读0次

    背景

    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"时,需要进行解压。

    相关文章

      网友评论

        本文标题:使用CFNetwork进行HTTP请求

        本文链接:https://www.haomeiwen.com/subject/uhggzqtx.html