iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Read篇)

前言:

本文为CocoaAsyncSocket源码系列中第二篇:Read篇,将重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于TLS的不同读取操作等等。
注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。如果不是诚心想学习IM相关知识,在这里就可以离场了...

本文系列第一篇:Connect篇已经完结,感兴趣可以看看:
iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)
iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇终)

注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释
如果对该框架用法不熟悉的话,可以参考楼主之前文章:
iOS即时通讯,从入门到“放弃”?
即时通讯下数据粘包、断包处理实例(基于CocoaAsyncSocket)
或者自行查阅。

目录:
  • 1.浅析Read读取,并阐述数据从socket到用户手中的流程。✅
  • 2.讲讲两种TLS建立连接的过程。✅
  • 3.深入讲解Read的核心方法---doReadData的实现。❌
正文:
一.浅析Read读取,并阐述数据从socket到用户手中的流程

大家用过这个框架就知道,我们每次读取数据之前都需要主动调用这么一个Read方法:

[gcdSocket readDataWithTimeout:-1 tag:110];

设置一个超时和tag值,这样我们就可以在这个超时的时间里,去读取到达当前socket的数据了。

那么本篇Read就从这个方法开始说起,我们点进框架里,来到这个方法:

- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag
{
     [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
}

- (void)readDataWithTimeout:(NSTimeInterval)timeout
                     buffer:(NSMutableData *)buffer
               bufferOffset:(NSUInteger)offset
                        tag:(long)tag
{
     [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
}

//用偏移量 maxLength 读取数据
- (void)readDataWithTimeout:(NSTimeInterval)timeout
                     buffer:(NSMutableData *)buffer
               bufferOffset:(NSUInteger)offset
                  maxLength:(NSUInteger)length
                        tag:(long)tag
{
     if (offset > [buffer length]) {
          LogWarn(@"Cannot read: offset > [buffer length]");
          return;
     }
     
     GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
                                                               startOffset:offset
                                                                 maxLength:length
                                                                   timeout:timeout
                                                                readLength:0
                                                                terminator:nil
                                                                       tag:tag];
     
     dispatch_async(socketQueue, ^{ @autoreleasepool {
          
          LogTrace();
          
          if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
          {
            //往读的队列添加任务,任务是包的形式
               [readQueue addObject:packet];
               [self maybeDequeueRead];
          }
     }});
}

这个方法很简单。最终调用,去创建了一个GCDAsyncReadPacket类型的对象packet,简单来说这个对象是用来标识读取任务的。然后把这个packet对象添加到读取队列中。然后去调用:

[self maybeDequeueRead];

去从队列中取出读取任务包,做读取操作。

还记得我们之前Connect篇讲到的GCDAsyncSocket这个类的一些属性,其中有这么一个:

//当前这次读取数据任务包
GCDAsyncReadPacket *currentRead;

这个属性标识了我们当前这次读取的任务,当读取到packet任务时,其实这个属性就被赋值成packet,做数据读取。

接着来看看GCDAsyncReadPacket这个类,同样我们先看看属性:

@interface GCDAsyncReadPacket : NSObject
{
  @public
    //当前包的数据 ,(容器,有可能为空)
    NSMutableData *buffer;
    //开始偏移 (数据在容器中开始写的偏移)
    NSUInteger startOffset;
    //已读字节数 (已经写了个字节数)
    NSUInteger bytesDone;
    
    //想要读取数据的最大长度 (有可能没有)
    NSUInteger maxLength;
    //超时时长
    NSTimeInterval timeout;
    //当前需要读取总长度  (这一次read读取的长度,不一定有,如果没有则可用maxLength)
    NSUInteger readLength;
    
    //包的边界标识数据 (可能没有)
    NSData *term;
    //判断buffer的拥有者是不是这个类,还是用户。
    //跟初始化传不传一个buffer进来有关,如果传了,则拥有者为用户 NO, 否则为YES
    BOOL bufferOwner;
    //原始传过来的data长度
    NSUInteger originalBufferLength;
    //数据包的tag
    long tag;
}

这个类的内容还是比较多的,但是其实理解起来也很简单,它主要是来装当前任务的一些标识和数据,使我们能够正确的完成我们预期的读取任务。
这些属性,大家同样过一个眼熟即可,后面大家就能理解它们了。

这个类还有一堆方法,包括初始化的、和一些数据的操作方法,其具体作用如下注释:

//初始化
- (id)initWithData:(NSMutableData *)d
       startOffset:(NSUInteger)s
         maxLength:(NSUInteger)m
           timeout:(NSTimeInterval)t
        readLength:(NSUInteger)l
        terminator:(NSData *)e
               tag:(long)i;

//确保容器大小给多余的长度
- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead;
////预期中读的大小,决定是否走preBuffer
- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
//读取指定长度的数据
- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;

//上两个方法的综合
- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;

//根据一个终结符去读数据,直到读到终结的位置或者最大数据的位置,返回值为该包的确定长度
- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr;
////查找终结符,在prebuffer之后,返回值为该包的确定长度
- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;

这里暂时仍然不准备去讲这些方法,等我们用到了在去讲它。

我们通过上述的属性和这些方法,能够把数据正确的读取到packet的属性buffer中,再用代理回传给用户。

这个GCDAsyncReadPacket类暂时就先这样了,我们接着往下看,前面讲到调用maybeDequeueRead开始读取任务,我们接下来就看看这个方法:

//让读任务离队,开始执行这条读任务
- (void)maybeDequeueRead
{
    LogTrace();
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    
    // If we're not currently processing a read AND we have an available read stream
    
    //如果当前读的包为空,而且flag为已连接
    if ((currentRead == nil) && (flags & kConnected))
    {
        //如果读的queue大于0 (里面装的是我们封装的GCDAsyncReadPacket数据包)
        if ([readQueue count] > 0)
        {
            // Dequeue the next object in the write queue
            //使得下一个对象从写的queue中离开
            
            //从readQueue中拿到第一个写的数据
            currentRead = [readQueue objectAtIndex:0];
            //移除
            [readQueue removeObjectAtIndex:0];
            
            //我们的数据包,如果是GCDAsyncSpecialPacket这种类型,这个包里装了TLS的一些设置
            //如果是这种类型的数据,那么我们就进行TLS
            if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
            {
                LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
                
                // Attempt to start TLS
                //标记flag为正在读取TLS
                flags |= kStartingReadTLS;
                
                // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
                //只有读写都开启了TLS,才会做TLS认证
                [self maybeStartTLS];
            }
            else
            {
                LogVerbose(@"Dequeued GCDAsyncReadPacket");
                
                // Setup read timer (if needed)
                //设置读的任务超时,每次延时的时候还会调用 [self doReadData];
                [self setupReadTimerWithTimeout:currentRead->timeout];
                
                // Immediately read, if possible
                //读取数据
                [self doReadData];
            }
        }
        
        //读的队列没有数据,标记flag为,读了没有数据则断开连接状态
        else if (flags & kDisconnectAfterReads)
        {
            //如果标记有写然后断开连接
            if (flags & kDisconnectAfterWrites)
            {
                //如果写的队列为0,而且写为空
                if (([writeQueue count] == 0) && (currentWrite == nil))
                {
                    //断开连接
                    [self closeWithError:nil];
                }
            }
            else
            {
                //断开连接
                [self closeWithError:nil];
            }
        }
        //如果有安全socket。
        else if (flags & kSocketSecure)
        {
            [self flushSSLBuffers];
                    
            //如果可读字节数为0
            if ([preBuffer availableBytes] == 0)
            {
                //
                if ([self usingCFStreamForTLS]) {
                    // Callbacks never disabled
                }
                else {
                    //重新恢复读的source。因为每次开始读数据的时候,都会挂起读的source
                    [self resumeReadSource];
                }
            }
        }
    }
}

详细的细节看注释即可,这里我们讲讲主要的作用:

  1. 我们首先做了一些是否连接,读队列任务是否大于0等等一些判断。当然,如果判断失败,那么就不在读取,直接返回。
  • 接着我们从全局的readQueue中,拿到第一条任务,去做读取,我们来判断这个任务的类型,如果是GCDAsyncSpecialPacket类型的,我们将开启TLS认证。(后面再来详细讲)

如果是是我们之前加入队列中的GCDAsyncReadPacket类型,我们则开始读取操作,调用doReadData,这个方法将是整个Read篇的核心方法。

  • 如果队列中没有任务,我们先去判断,是否是上一次是读取了数据,但是没有数据的标记,如果是的话我们则断开socket连接(注:还记得么,我们之前应用篇有说过,调取读取任务时给一个超时,如果超过这个时间,还没读取到任务,则会断开连接,就是在这触发的)。
  • 如果我们是安全的连接(基于TLS的Socket),我们就去调用flushSSLBuffers,把数据从SSL通道中,移到我们的全局缓冲区preBuffer中。

讲到这,大家可能觉得有些迷糊,为了能帮助大家理解,这里我准备了一张流程图,来讲讲整个框架读取数据的流程:

  1. 这张图就是整个数据的流向了,这里我们读取数据分为两种情况,一种是基于TLS,一种是普通的数据读取。
  • 而基于TLS的数据读取,又分为两种,一种是基于CFStream,另一种则是安全通道SecureTransport形式。
  • 这两种类型的TLS都会在各自的通道内,完成数据的解密,然后解密后的数据又流向了全局缓冲区prebuffer
  • 这个全局缓冲区prebuffer就像一个蓄水池,如果我们一直不去做读取任务的话,它里面的数据会越来越多,当我们读取其中所有数据,它就会回归最初的状态。
  • 我们用currentRead的方式,从prebuffer中读取数据,当读到我们想要的位置时,就会回调代理,用户得到数据。
二.讲讲两种TLS建立连接的过程

讲到这里,就不得不提一下,这里个框架开启TLS的过程。它对外提供了这么一个方法来开启TLS

- (void)startTLS:(NSDictionary *)tlsSettings

可以根据一个字典,去开启并且配置TLS,那么这个字典里包含什么内容呢?
一共包含以下这些key

//配置SSL上下文的设置
// Configure SSLContext from given settings
// 
// Checklist:
//  1. kCFStreamSSLPeerName  //证书名
//  2. kCFStreamSSLCertificates //证书数组
//  3. GCDAsyncSocketSSLPeerID  //证书ID
//  4. GCDAsyncSocketSSLProtocolVersionMin  //SSL最低版本
//  5. GCDAsyncSocketSSLProtocolVersionMax  //SSL最高版本
//  6. GCDAsyncSocketSSLSessionOptionFalseStart  
//  7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
//  8. GCDAsyncSocketSSLCipherSuites
//  9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
//
// Deprecated (throw error): //被废弃的参数,如果设置了就会报错关闭socket
// 10. kCFStreamSSLAllowsAnyRoot
// 11. kCFStreamSSLAllowsExpiredRoots
// 12. kCFStreamSSLAllowsExpiredCertificates
// 13. kCFStreamSSLValidatesCertificateChain
// 14. kCFStreamSSLLevel

其中有些Key的值,具体是什么意思,value如何设置,可以查查苹果文档,限于篇幅,我们就不赘述了,只需要了解重要的几个参数即可。
后面一部分是被废弃的参数,如果我们设置了,就会报错关闭socket连接。
除此之外,还有这么3个key被我们遗漏了,这3个key,是框架内部用来判断,并且做一些处理的标识:

kCFStreamSSLIsServer  //判断当前是否是服务端
GCDAsyncSocketManuallyEvaluateTrust //判断是否需要手动信任SSL
GCDAsyncSocketUseCFStreamForTLS //判断是否使用CFStream形式的TLS

这3个key的大意如注释,后面我们还会讲到,其中最重要的是GCDAsyncSocketUseCFStreamForTLS这个key,一旦我们设置为YES,将开启CFStream的TLS,关于这种基于流的TLS与普通的TLS的区别,我们来看看官方说明:

    • GCDAsyncSocketUseCFStreamForTLS (iOS only)
  • The value must be of type NSNumber, encapsulating a BOOL value.
    
  • By default GCDAsyncSocket will use the SecureTransport layer to perform encryption.
    
  • This gives us more control over the security protocol (many more configuration options),
    
  • plus it allows us to optimize things like sys calls and buffer allocation.
    
  • However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption
    
  • technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket
    
  • will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property
    
  • (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method.
    
  • Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket,
    
  • and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty.
    
  • For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings.
    
  • If unspecified, the default value is NO.
    

从上述说明中,我们可以得知,CFStream形式的TLS仅仅可以被用于iOS平台,并且它是一种过时的加解密技术,如果我们没有必要,最好还是不要用这种方式的TLS

至于它的实现,我们接着往下看。

//开启TLS
- (void)startTLS:(NSDictionary *)tlsSettings
{
     LogTrace();
     
     if (tlsSettings == nil)
    {
        
        tlsSettings = [NSDictionary dictionary];
    }
     //新生成一个TLS特殊的包
     GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
     
    
     dispatch_async(socketQueue, ^{ @autoreleasepool {
          
          if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
          {
            //添加到读写Queue中去
               [readQueue addObject:packet];
               [writeQueue addObject:packet];
               //把TLS标记加上
               flags |= kQueuedTLS;
               //开始读取TLS的任务,读到这个包会做TLS认证。在这之前的包还是不用认证就可以传送完
               [self maybeDequeueRead];
               [self maybeDequeueWrite];
          }
     }});
     
}

这个方法就是对外提供的开启TLS的方法,它把传进来的字典,包成一个TLS的特殊包,这个GCDAsyncSpecialPacket类包里面就一个字典属性:

- (id)initWithTLSSettings:(NSDictionary *)settings;

然后我们把这个包添加到读写queue中去,并且标记当前的状态,然后去执行maybeDequeueReadmaybeDequeueWrite
需要注意的是,这里只有读到这个GCDAsyncSpecialPacket时,才开始TLS认证和握手。

接着我们就来到了maybeDequeueRead这个方法,这个方法我们在前面第一条中讲到过,忘了的可以往上拉一下页面就可以看到。
它就是让我们的ReadQueue中的读任务离队,并且开始执行这条读任务。

  • 当我们读到的是GCDAsyncSpecialPacket类型的包,则开始进行TLS认证。
  • 当我们读到的是GCDAsyncReadPacket类型的包,则开始进行一次读取数据的任务。
  • 如果ReadQueue为空,则对几种情况进行判断,是否是读取上一次数据失败,则断开连接。
    如果是基于TLSSocket,则把SSL安全通道的数据,移到全局缓冲区preBuffer中。如果数据仍然为空,则恢复读source,等待下一次读source的触发。

接着我们来看看这其中第一条,当读到的是一个GCDAsyncSpecialPacket类型的包,我们会调用maybeStartTLS这个方法:

//可能开启TLS
- (void)maybeStartTLS
{
     
    //只有读和写TLS都开启
     if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
     {
        //需要安全传输
          BOOL useSecureTransport = YES;
          
          #if TARGET_OS_IPHONE
          {
            //拿到当前读的数据
               GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
            //得到设置字典
               NSDictionary *tlsSettings = tlsPacket->tlsSettings;
               
            //拿到Key为CFStreamTLS的 value
               NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];
            
               if (value && [value boolValue])
                //如果是用CFStream的,则安全传输为NO
                    useSecureTransport = NO;
          }
          #endif
          //如果使用安全通道
          if (useSecureTransport)
          {
            //开启TLS
               [self ssl_startTLS];
          }
        //CFStream形式的Tls
          else
          {
          #if TARGET_OS_IPHONE
               [self cf_startTLS];
          #endif
          }
     }
}

这里根据我们之前添加标记,判断是否读写TLS状态,是才继续进行接下来的TLS认证。
接着我们拿到当前GCDAsyncSpecialPacket,取得配置字典中keyGCDAsyncSocketUseCFStreamForTLS的值:
如果为YES则说明使用CFStream形式的TLS,否则使用SecureTransport安全通道形式的TLS。关于这个配置项,还有二者的区别,我们前面就讲过了。

接着我们分别来看看这两个方法,先来看看ssl_startTLS

这个方法非常长,大概有400多行,所以为了篇幅和大家阅读体验,楼主简化了一部分内容用省略号+注释的形式表示。大家可以参照着源码来阅读。

//开启TLS
- (void)ssl_startTLS
{
     LogTrace();
     
     LogVerbose(@"Starting TLS (via SecureTransport)...");
     
    //状态标记
     OSStatus status;
     
    //拿到当前读的数据包
     GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
     if (tlsPacket == nil) // Code to quiet the analyzer
     {
          NSAssert(NO, @"Logic error");
          
          [self closeWithError:[self otherError:@"Logic error"]];
          return;
     }
    //拿到设置
     NSDictionary *tlsSettings = tlsPacket->tlsSettings;
     
     // Create SSLContext, and setup IO callbacks and connection ref
     
    //根据key来判断,当前包是否是服务端的
     BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue];
     
    //创建SSL上下文
     #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
     {
        //如果是服务端的创建服务端上下文,否则是客户端的上下文,用stream形式
          if (isServer)
               sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
          else
               sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
          //为空则报错返回
          if (sslContext == NULL)
          {
               [self closeWithError:[self otherError:@"Error in SSLCreateContext"]];
               return;
          }
     }
    
     #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
     {
          status = SSLNewContext(isServer, &sslContext);
          if (status != noErr)
          {
               [self closeWithError:[self otherError:@"Error in SSLNewContext"]];
               return;
          }
     }
     #endif
     
    //给SSL上下文设置 IO回调 分别为SSL 读写函数
     status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
    //设置出错
     if (status != noErr)
     {
          [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
          return;
     }
    
     //在握手之调用,建立SSL连接 ,第一次连接 1
     status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
    //连接出错
     if (status != noErr)
     {
          [self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
          return;
     }

    //是否应该手动的去信任SSL
     BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue];
    //如果需要手动去信任
     if (shouldManuallyEvaluateTrust)
     {
        //是服务端的话,不需要,报错返回
          if (isServer)
          {
               [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]];
               return;
          }
          //第二次连接 再去连接用kSSLSessionOptionBreakOnServerAuth的方式,去连接一次,这种方式可以直接信任服务端证书
          status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
        //错误直接返回
          if (status != noErr)
          {
               [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]];
               return;
          }
          
          #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
          
          // Note from Apple's documentation:
          //
          // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8.
          // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the
          // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus
          // SSLSetEnableCertVerify is not available on that platform at all.
        
          //为了防止kSSLSessionOptionBreakOnServerAuth这种情况下,产生了不受信任的环境
          status = SSLSetEnableCertVerify(sslContext, NO);
          if (status != noErr)
          {
               [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
               return;
          }
          
          #endif
     }

    //配置SSL上下文的设置
     
     id value;
    //这个参数是用来获取证书名验证,如果设置为NULL,则不验证
     // 1. kCFStreamSSLPeerName
     
     value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName];
     if ([value isKindOfClass:[NSString class]])
     {
          NSString *peerName = (NSString *)value;
          
          const char *peer = [peerName UTF8String];
          size_t peerLen = strlen(peer);
          
        //把证书名设置给SSL
          status = SSLSetPeerDomainName(sslContext, peer, peerLen);
          if (status != noErr)
          {
               [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]];
               return;
          }
     }
    //不是string就错误返回
     else if (value)
     {
        //这个断言啥用也没有啊。。
          NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString.");
          
          [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]];
          return;
     }
     
     //  2. kCFStreamSSLCertificates
      ...
     //  3. GCDAsyncSocketSSLPeerID
      ...
     //  4. GCDAsyncSocketSSLProtocolVersionMin
      ...
     //  5. GCDAsyncSocketSSLProtocolVersionMax
      ...
     //  6. GCDAsyncSocketSSLSessionOptionFalseStart
      ...
     //  7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
      ...
     //  8. GCDAsyncSocketSSLCipherSuites
      ...
     //  9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
      ...
     
     //弃用key的检查,如果有下列key对应的value,则都报弃用的错误
     
     // 10. kCFStreamSSLAllowsAnyRoot  
      ...
     // 11. kCFStreamSSLAllowsExpiredRoots
      ...
     // 12. kCFStreamSSLAllowsExpiredCertificates
      ...
     // 13. kCFStreamSSLValidatesCertificateChain
      ...
     // 14. kCFStreamSSLLevel
      ...

     // Setup the sslPreBuffer
     // 
     // Any data in the preBuffer needs to be moved into the sslPreBuffer,
     // as this data is now part of the secure read stream.
     
    //初始化SSL提前缓冲 也是4Kb
     sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
     //获取到preBuffer可读大小
     size_t preBufferLength  = [preBuffer availableBytes];
     
    //如果有可读内容
     if (preBufferLength > 0)
     {
        //确保SSL提前缓冲的大小
          [sslPreBuffer ensureCapacityForWrite:preBufferLength];
          //从readBuffer开始读,读这个长度到 SSL提前缓冲的writeBuffer中去
          memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength);
        //移动提前的读buffer
          [preBuffer didRead:preBufferLength];
        //移动sslPreBuffer的写buffer
          [sslPreBuffer didWrite:preBufferLength];
     }
     //拿到上次错误的code,并且让上次错误code = 没错
     sslErrCode = lastSSLHandshakeError = noErr;
     
     // Start the SSL Handshake process
     //开始SSL握手过程
     [self ssl_continueSSLHandshake];
}

这个方法的结构也很清晰,主要就是建立TLS连接,并且配置SSL上下文对象:sslContext,为TLS握手做准备。
这里我们就讲讲几个重要的关于SSL的函数,其余细节可以看看注释:

  1. 创建SSL上下文对象:
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);

这个函数用来创建一个SSL上下文,我们接下来会把配置字典tlsSettings中所有的参数,都设置到这个sslContext中去,然后用这个sslContext进行TLS后续操作,握手等。

  1. 给SSL设置读写回调:
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);

这两个回调函数如下:

//读函数
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
{
    //拿到socket
    GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;
    
    //断言当前为socketQueue
    NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");
    
    //读取数据,并且返回状态码
    return [asyncSocket sslReadWithBuffer:data length:dataLength];
}
//写函数
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
{
    GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;
    
    NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");
    
    return [asyncSocket sslWriteWithBuffer:data length:dataLength];
}

他们分别调用了sslReadWithBuffersslWriteWithBuffer两个函数进行SSL的读写处理,关于这两个函数,我们后面再来说。

  1. 发起SSL连接:
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);

到这一步,前置的重要操作就完成了,接下来我们是对SSL进行一些额外的参数配置:
我们根据tlsSettingsGCDAsyncSocketManuallyEvaluateTrust字段,去判断是否需要手动信任服务端证书,调用如下函数

status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);

这个函数是用来设置一些可选项的,当然不止kSSLSessionOptionBreakOnServerAuth这一种,还有许多种类型的可选项,感兴趣的朋友可以自行点进去看看这个枚举。

接着我们按照字典中的设置项,一项一项去设置ssl上下文,类似:

status = SSLSetPeerDomainName(sslContext, peer, peerLen);

设置完这些有效的,我们还需要去检查无效的key,万一我们设置了这些废弃的api,我们需要报错处理。

做完这些操作后,我们初始化了一个sslPreBuffer,这个ssl安全通道下的全局缓冲区:

sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];

然后把prebuffer全局缓冲区中的数据全部挪到sslPreBuffer中去,这里为什么要这么做呢?按照我们上面的流程图来说,正确的数据流向应该是从sslPreBuffer->prebuffer的,楼主在这里也思考了很久,最后我的想法是,就是初始化的时候,数据的流向的统一,在我们真正数据读取的时候,就不需要做额外的判断了。

到这里我们所有的握手前初始化工作都做完了。

接着我们调用了ssl_continueSSLHandshake方法开始SSL握手:
//SSL的握手
- (void)ssl_continueSSLHandshake
{
     LogTrace();
    
     //用我们的SSL上下文对象去握手
     OSStatus status = SSLHandshake(sslContext);
    //拿到握手的结果,赋值给上次握手的结果
     lastSSLHandshakeError = status;
     
    //如果没错
     if (status == noErr)
     {
          LogVerbose(@"SSLHandshake complete");
          
        //把开始读写TLS,从标记中移除
          flags &= ~kStartingReadTLS;
          flags &= ~kStartingWriteTLS;
        
          //把Socket安全通道标记加上
          flags |=  kSocketSecure;
          
        //拿到代理
          __strong id theDelegate = delegate;

          if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)])
          {
               dispatch_async(delegateQueue, ^{ @autoreleasepool {
                    //调用socket已经开启安全通道的代理方法
                    [theDelegate socketDidSecure:self];
               }});
          }
          //停止读取
          [self endCurrentRead];
        //停止写
          [self endCurrentWrite];
          //开始下一次读写任务
          [self maybeDequeueRead];
          [self maybeDequeueWrite];
     }
    //如果是认证错误
     else if (status == errSSLPeerAuthCompleted)
     {
          LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval");
          
          __block SecTrustRef trust = NULL;
        //从sslContext拿到证书相关的细节
          status = SSLCopyPeerTrust(sslContext, &trust);
        //SSl证书赋值出错
          if (status != noErr)
          {
               [self closeWithError:[self sslError:status]];
               return;
          }
          
        //拿到状态值
          int aStateIndex = stateIndex;
        //socketQueue
          dispatch_queue_t theSocketQueue = socketQueue;
          
          __weak GCDAsyncSocket *weakSelf = self;
          
        //创建一个完成Block
          void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool {
          #pragma clang diagnostic push
          #pragma clang diagnostic warning "-Wimplicit-retain-self"
               
               dispatch_async(theSocketQueue, ^{ @autoreleasepool {
                    
                    if (trust) {
                         CFRelease(trust);
                         trust = NULL;
                    }
                    
                    __strong GCDAsyncSocket *strongSelf = weakSelf;
                    if (strongSelf)
                    {
                         [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex];
                    }
               }});
               
          #pragma clang diagnostic pop
          }};
          
          __strong id theDelegate = delegate;
          
          if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)])
          {
               dispatch_async(delegateQueue, ^{ @autoreleasepool {
                
#pragma mark - 调用代理我们自己去https认证
                    [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];
               }});
          }
        //没实现代理直接报错关闭连接。
          else
          {
               if (trust) {
                    CFRelease(trust);
                    trust = NULL;
               }
               
               NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings,"
                               @" but delegate doesn't implement socket:shouldTrustPeer:";
               
               [self closeWithError:[self otherError:msg]];
               return;
          }
     }
    
    //握手错误为 IO阻塞的
     else if (status == errSSLWouldBlock)
     {
          LogVerbose(@"SSLHandshake continues...");
          
          // Handshake continues...
          // 
          // This method will be called again from doReadData or doWriteData.
     }
     else
     {
        //其他错误直接关闭连接
          [self closeWithError:[self sslError:status]];
     }
}

这个方法就做了一件事,就是SSL握手,我们调用了这个函数完成握手:

OSStatus status = SSLHandshake(sslContext);

然后握手的结果分为4种情况:

  1. 如果返回为noErr,这个会话已经准备好了安全的通信,握手成功。
  • 如果返回的valueerrSSLWouldBlock,握手方法必须再次调用。
  • 如果返回为errSSLServerAuthCompleted,如果我们要调用代理,我们需要相信服务器,然后再次调用握手,去恢复握手或者关闭连接。
  • 否则,返回的value表明了错误的code

其中需要说说的是errSSLWouldBlock,这个是IO阻塞下的错误,也就是服务器的结果还没来得及返回,当握手结果返回的时候,这个方法会被再次触发。

还有就是errSSLServerAuthCompleted下,我们回调了代理:

[theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];

我们可以去手动对证书进行认证并且信任,当完成回调后,会调用到这个方法里来,再次进行握手:

//修改信息后再次进行SSL握手
- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex
{
    LogTrace();
    
    if (aStateIndex != stateIndex)
    {
        return;
    }
    
    // Increment stateIndex to ensure completionHandler can only be called once.
    stateIndex++;
    
    if (shouldTrust)
    {
        NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError);
        [self ssl_continueSSLHandshake];
    }
    else
    {
        
        [self closeWithError:[self sslError:errSSLPeerBadCert]];
    }
}

到这里,我们就整个完成安全通道下的TLS认证。

接着我们来看看基于CFStreamTLS

因为CFStream是上层API,所以它的TLS流程相当简单,我们来看看cf_startTLS这个方法:

//CF流形式的TLS
- (void)cf_startTLS
{
     LogTrace();
     
     LogVerbose(@"Starting TLS (via CFStream)...");
     
    //如果preBuffer的中可读数据大于0,错误关闭
     if ([preBuffer availableBytes] > 0)
     {
          NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket.";
          
          [self closeWithError:[self otherError:msg]];
          return;
     }
     
    //挂起读写source
     [self suspendReadSource];
     [self suspendWriteSource];
     
    //把未读的数据大小置为0
     socketFDBytesAvailable = 0;
    //去掉下面两种flag
     flags &= ~kSocketCanAcceptBytes;
     flags &= ~kSecureSocketHasBytesAvailable;
     
    //标记为CFStream
     flags |=  kUsingCFStreamForTLS;
     
    //如果创建读写stream失败
     if (![self createReadAndWriteStream])
     {
          [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
          return;
     }
     //注册回调,这回监听可读数据了!!
     if (![self registerForStreamCallbacksIncludingReadWrite:YES])
     {
          [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
          return;
     }
     //添加runloop
     if (![self addStreamsToRunLoop])
     {
          [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
          return;
     }
     
     NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS");
     NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS");
     
    //拿到当前包
     GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
    //拿到ssl配置
     CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;
     
     // Getting an error concerning kCFStreamPropertySSLSettings ?
     // You need to add the CFNetwork framework to your iOS application.
     
    //直接设置给读写stream
     BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
     BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);
     
        //设置失败
     if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug.
     {
          [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]];
          return;
     }
     
    //打开流
     if (![self openStreams])
     {
          [self closeWithError:[self otherError:@"Error in CFStreamOpen"]];
          return;
     }
     
     LogVerbose(@"Waiting for SSL Handshake to complete...");
}

1.这个方法很简单,首先它挂起了读写source,然后重新初始化了读写流,并且绑定了回调,和添加了runloop
这里我们为什么要用重新这么做?看过之前connect篇的同学就知道,我们在连接成功之后,去初始化过读写流,这些操作之前都做过。而在这里重新初始化,并不会重新创建,只是修改读写流的一些参数,其中主要是下面这个方法,传递了一个YES过去:

if (![self registerForStreamCallbacksIncludingReadWrite:YES])

这个参数会使方法里多添加一种触发回调的方式:kCFStreamEventHasBytesAvailable
当有数据可读时候,触发Stream回调。

2.接着我们用下面这个函数把TLS的配置参数,设置给读写stream:

//直接设置给读写stream
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);

3.最后打开读写流,整个CFStream形式的TLS就完成了。

看到这,大家可能对数据触发的问题有些迷惑。总结一下,我们到现在一共有3种触发的回调:
  1. 读写source:这个和socket绑定在一起,一旦有数据到达,就会触发事件句柄,但是我们可以看到在cf_startTLS方法中我们调用了:
 //挂起读写source
[self suspendReadSource];
[self suspendWriteSource];

所以,对于CFStream形式的TLS的读写并不是由source触发的,而其他的都是由source来触发。

  1. CFStream绑定的几种事件的读写回调函数:
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)

这个和CFStream形式的TLS相关,会触发这种形式的握手,流末尾等出现的错误,还有该形式下数据到达。
因为我们在一开始的连接完成就初始化过stream,所以非CFStream形式下也回触发这个回调,只是不会在数据到达触发而已。

  1. SSL安全通道形式,绑定的SSL读写函数:
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)

这个函数并不是由系统触发,而是需要我们主动去调用SSLReadSSLWrite两个函数,回调才能被触发。

暂时的结尾:

篇幅原因,本篇断在这里。如果大家对本文内容有些地方不明白的话,也没关系,等我们下篇把核心方法doReadData讲完,在整个梳理一遍,或许大家就会对整个框架的Read流程有一个清晰的认识。

过完年,因为各种节后综合征。。导致这个系列的内容拖了比较长的时间,最近会加快脚步,早日填完这个系列的坑。

书山有路勤为径,学海无涯苦作舟。自勉之~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容