美文网首页移动开发网络
iOS 移动开发网络 part5.4:CocoaAsyncSoc

iOS 移动开发网络 part5.4:CocoaAsyncSoc

作者: 破弓 | 来源:发表于2018-01-03 11:26 被阅读28次
    kSocketSecure                  = 1 << 13,  // If set, socket is using secure communication 
    kUsingCFStreamForTLS           = 1 << 18,  // If set, we're forced to use CFStream instead of SecureTransport
    

    在标识CocoaAsyncSocket的状态的GCDAsyncSocketFlags内有这样两个数值.不是说好的CFStream for TLSSecureTransport for TLS.为什么没有kUsingSecureTransportForTLS?

    kSocketSecure包含了kUsingCFStreamForTLSkUsingSecureTransportForTLS;判断用什么方式实现TLS是用以下两个方法:

    - (BOOL)usingCFStreamForTLS
    {
        #if TARGET_OS_IPHONE
        
        if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS))
        {
            // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag.
            
            return YES;
        }
        
        #endif
        
        return NO;
    }
    
    - (BOOL)usingSecureTransportForTLS
    {
        // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable)
        
        #if TARGET_OS_IPHONE
        
        if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS))
        {
            // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag.
            
            return NO;
        }
        
        #endif
        
        return YES;
    }
    

    SecureTransport for TLS用到了系统的Secure.framework的内容.

    开启SecureTransport for TLS代码如下:

    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    if(![_socket connectToHost:@"127.0.0.1" onPort:443 withTimeout:30 error:nil]){
        NSLog(@"连接失败");
    }else{
        NSMutableDictionary *settings = [[NSMutableDictionary alloc] init];
        
        [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust];
        
        [_socket startTLS:settings];
        
        [_socket readDataWithTimeout:-1 tag:110];
    }
    

    开启CFStream for TLS代码如下:

    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    if(![_socket connectToHost:@"127.0.0.1" onPort:443 withTimeout:30 error:nil]){
        NSLog(@"连接失败");
    }else{
        NSMutableDictionary *settings = [[NSMutableDictionary alloc] init];
        
        [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust];
        [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketUseCFStreamForTLS];
        [_socket startTLS:settings];
        
        [_socket readDataWithTimeout:-1 tag:110];
    }
    

    调用方法startTLS:,传入参数字典就开启了TLS.

    - (void)startTLS:(NSDictionary *)tlsSettings
    {
        LogTrace();
        
        if (tlsSettings == nil)
        {
            // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary,
            // but causes problems if we later try to fetch the remote host's certificate.
            // 
            // To be exact, it causes the following to return NULL instead of the normal result:
            // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates)
            // 
            // So we use an empty dictionary instead, which works perfectly.
            
            tlsSettings = [NSDictionary dictionary];
        }
        
        GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
        
        dispatch_async(socketQueue, ^{ @autoreleasepool {
            
            if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
            {
                [readQueue addObject:packet];
                [writeQueue addObject:packet];
                
                flags |= kQueuedTLS;
                
                [self maybeDequeueRead];
                [self maybeDequeueWrite];
            }
        }});
        
    }
    

    利用传入的字典创建一个GCDAsyncSpecialPacket.(与GCDAsyncSpecialPacket对应的是GCDAsyncWritePacketGCDAsyncReadPacket.GCDAsyncSpecialPacket是专门用于开启TLS的;GCDAsyncReadPacketGCDAsyncWritePacket是做读写用的)

    方法- (void)maybeStartTLS这里分水岭,外围传入的字典如果设置了GCDAsyncSocketUseCFStreamForTLSYES,则走[self cf_startTLS],否则就走[self ssl_startTLS].

    1.cf_startTLS

    CocoaAsyncSocket创建与连接的过程中两个CFStream早就已经建好.
    但是还记得我们刚刚说的吗?不开启CF for TLS,两个CFStream是没有监听作用的.而调用cf_startTLS后,会又调用registerForStreamCallbacksIncludingReadWrite:传入YES.

    [self registerForStreamCallbacksIncludingReadWrite:YES]
    

    打开CFStream对可读数据和可用空间的监听.

    我们通过- (void)startTLS:(NSDictionary *)tlsSettings;传入的字典,字典会赋给两个CFStream.

    GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
    CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;
    
    BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
    BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);
    

    2.ssl_startTLS

    ssl_startTLS会对应有:

    SSLContextRef sslContext;
    

    我们通过- (void)startTLS:(NSDictionary *)tlsSettings;传入的字典,字典的每个键值对会挨个赋给SSLContextRef sslContext.
    SSLContextRef sslContext的属性很多,有些我也不清楚具体的用途,想弄明白的请自行看文档:

    // Configure SSLContext from given settings
    //
    // Checklist:
    //  1. kCFStreamSSLPeerName
    //  2. kCFStreamSSLCertificates
    //  3. GCDAsyncSocketSSLPeerID
    //  4. GCDAsyncSocketSSLProtocolVersionMin
    //  5. GCDAsyncSocketSSLProtocolVersionMax
    //  6. GCDAsyncSocketSSLSessionOptionFalseStart
    //  7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
    //  8. GCDAsyncSocketSSLCipherSuites
    //  9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
    //
    // Deprecated (throw error):
    // 10. kCFStreamSSLAllowsAnyRoot
    // 11. kCFStreamSSLAllowsExpiredRoots
    // 12. kCFStreamSSLAllowsExpiredCertificates
    // 13. kCFStreamSSLValidatesCertificateChain
    // 14. kCFStreamSSLLevel
    

    配置完SSLContextRef sslContext后,调用- (void)ssl_continueSSLHandshake;

    - (void)ssl_continueSSLHandshake;方法内部会调用OSStatus status = SSLHandshake(sslContext);得到的status决定了下一步怎么走.

    status:
    noErr==>握手成功
    errSSLPeerAuthCompleted==>将SecTrustRef传给代理,征求代理的意见
    errSSLWouldBlock==>出现拥塞,这时socket读写已经打开,所以在读与写的时候都有可能再次调用ssl_continueSSLHandshake
    其他==>报错
    
    GCDAsyncSocket for TLS.png

    <<开启TLS总结>>
    开启TLS的所有步骤可以总结为上面的流程图.需要注意的是,不开启TLS的数据读写与SecureTransport for TLS的数据读写都走dispatch_sourcebsd socket的监听,而CFStream for TLS的数据读写则用的是CFStream自己对内部bsd socket的监听.

    无论用什么方式监听bsd socket,一旦监听到有数据可读,或者有空间可以写入,都会调用对应方法- (void)doReadData或者- (void)doWriteData.

    相关文章

      网友评论

      本文标题:iOS 移动开发网络 part5.4:CocoaAsyncSoc

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