移动开发网络

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

2018-01-03  本文已影响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.

上一篇下一篇

猜你喜欢

热点阅读