| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
HeiUWSH
8年前发布

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

   <p><img src="https://simg.open-open.com/show/9f956dd41d266d315551a0febbba582a.png"></p>    <h3>前言:</h3>    <p>本篇 ,将重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于 TLS 的不同读取操作等等。</p>    <p>注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。如果不是诚心想学习 IM 相关知识,在这里就可以离场了...</p>    <p>注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。</p>    <p>或者自行查阅。</p>    <h3>目录:</h3>    <ul>     <li>1.浅析 Read 读取,并阐述数据从 socket 到用户手中的流程。</li>     <li>2.讲讲两种 TLS 建立连接的过程。</li>     <li>3.深入讲解 Read 的核心方法--- doReadData 的实现。</li>    </ul>    <h3>正文:</h3>    <h2>一.浅析 Read 读取,并阐述数据从 socket 到用户手中的流程</h2>    <p>大家用过这个框架就知道,我们每次读取数据之前都需要主动调用这么一个 Read 方法:</p>    <pre>  <code class="language-objectivec">[gcdSocket readDataWithTimeout:-1 tag:110];</code></pre>    <p>设置一个超时和 tag 值,这样我们就可以在这个超时的时间里,去读取到达当前 socket 的数据了。</p>    <p>那么本篇 Read 就从这个方法开始说起,我们点进框架里,来到这个方法:</p>    <pre>  <code class="language-objectivec">- (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];            }       }});  }</code></pre>    <p>这个方法很简单。最终调用,去创建了一个 GCDAsyncReadPacket 类型的对象 packet ,简单来说这个对象是用来标识读取任务的。然后把这个 packet 对象添加到读取队列中。然后去调用:</p>    <pre>  <code class="language-objectivec">[self maybeDequeueRead];</code></pre>    <p>去从队列中取出读取任务包,做读取操作。</p>    <p>还记得我们之前 Connect 篇讲到的 GCDAsyncSocket 这个类的一些属性,其中有这么一个:</p>    <pre>  <code class="language-objectivec">//当前这次读取数据任务包  GCDAsyncReadPacket *currentRead;</code></pre>    <p>这个属性标识了我们当前这次读取的任务,当读取到 packet 任务时,其实这个属性就被赋值成 packet ,做数据读取。</p>    <p>接着来看看 GCDAsyncReadPacket 这个类,同样我们先看看属性:</p>    <pre>  <code class="language-objectivec">@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;  }</code></pre>    <p>这个类的内容还是比较多的,但是其实理解起来也很简单, 它主要是来装当前任务的一些标识和数据,使我们能够正确的完成我们预期的读取任务。</p>    <p>这些属性,大家同样过一个眼熟即可,后面大家就能理解它们了。</p>    <p>这个类还有一堆方法,包括初始化的、和一些数据的操作方法,其具体作用如下注释:</p>    <pre>  <code class="language-objectivec">//初始化  - (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;</code></pre>    <p>这里暂时仍然不准备去讲这些方法,等我们用到了在去讲它。</p>    <p>我们通过上述的属性和这些方法,能够把数据正确的读取到 packet 的属性 buffer 中,再用代理回传给用户。</p>    <p>这个 GCDAsyncReadPacket 类暂时就先这样了,我们接着往下看,前面讲到调用 maybeDequeueRead 开始读取任务,我们接下来就看看这个方法:</p>    <pre>  <code class="language-objectivec">//让读任务离队,开始执行这条读任务  - (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];                  }              }          }      }  }</code></pre>    <p>详细的细节看注释即可,这里我们讲讲主要的作用:</p>    <ol>     <li>我们首先做了一些是否连接,读队列任务是否大于0等等一些判断。当然,如果判断失败,那么就不在读取,直接返回。</li>     <li> <p>接着我们从全局的 readQueue 中,拿到第一条任务,去做读取,我们来判断这个任务的类型,如果是 GCDAsyncSpecialPacket 类型的,我们将开启 TLS 认证。(后面再来详细讲)</p> <p>如果是是我们之前加入队列中的 GCDAsyncReadPacket 类型,我们则开始读取操作,调用 doReadData ,这个方法将是整个 Read 篇的核心方法。</p> </li>     <li>如果队列中没有任务,我们先去判断,是否是上一次是读取了数据,但是没有数据的标记,如果是的话我们则断开 socket 连接(注:还记得么, 我们之前应用篇有说过,调取读取任务时给一个超时,如果超过这个时间,还没读取到任务,则会断开连接,就是在这触发的 )。</li>     <li>如果我们是安全的连接(基于TLS的 Socket ),我们就去调用 flushSSLBuffers ,把数据从 SSL 通道中,移到我们的全局缓冲区 preBuffer 中。</li>    </ol>    <p>讲到这,大家可能觉得有些迷糊,为了能帮助大家理解,这里我准备了一张流程图,来讲讲整个框架读取数据的流程:</p>    <p><img src="https://simg.open-open.com/show/acf9d44b54cc4bdf150b26f3c8df65b3.png"></p>    <ol>     <li>这张图就是整个数据的流向了,这里我们读取数据分为两种情况,一种是基于 TLS ,一种是普通的数据读取。</li>     <li>而基于 TLS 的数据读取,又分为两种,一种是基于 CFStream ,另一种则是安全通道 SecureTransport 形式。</li>     <li>这两种类型的 TLS 都会在各自的通道内,完成数据的解密,然后解密后的数据又流向了全局缓冲区 prebuffer 。</li>     <li>这个全局缓冲区 prebuffer 就像一个蓄水池,如果我们一直不去做读取任务的话,它里面的数据会越来越多,当我们读取其中所有数据,它就会回归最初的状态。</li>     <li>我们用 currentRead 的方式,从 prebuffer 中读取数据,当读到我们想要的位置时,就会回调代理,用户得到数据。</li>    </ol>    <h2>二.讲讲两种TLS建立连接的过程</h2>    <p>讲到这里,就不得不提一下,这里个框架开启 TLS 的过程。它对外提供了这么一个方法来开启 TLS :</p>    <pre>  <code class="language-objectivec">- (void)startTLS:(NSDictionary *)tlsSettings</code></pre>    <p>可以根据一个字典,去开启并且配置 TLS ,那么这个字典里包含什么内容呢?</p>    <p>一共包含以下这些 key :</p>    <pre>  <code class="language-objectivec">//配置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</code></pre>    <p>其中有些 Key 的值,具体是什么意思,value如何设置,可以查查苹果文档,限于篇幅,我们就不赘述了,只需要了解重要的几个参数即可。</p>    <p>后面一部分是被废弃的参数,如果我们设置了,就会报错关闭 socket 连接。</p>    <p>除此之外,还有这么3个 key 被我们遗漏了,这3个key,是框架内部用来判断,并且做一些处理的标识:</p>    <pre>  <code class="language-objectivec">kCFStreamSSLIsServer  //判断当前是否是服务端  GCDAsyncSocketManuallyEvaluateTrust //判断是否需要手动信任SSL  GCDAsyncSocketUseCFStreamForTLS //判断是否使用CFStream形式的TLS</code></pre>    <p>这3个key的大意如注释,后面我们还会讲到,其中最重要的是 GCDAsyncSocketUseCFStreamForTLS 这个 key ,一旦我们设置为YES,将开启 CFStream 的TLS,关于这种基于流的 TLS 与普通的 TLS 的区别,我们来看看官方说明:</p>    <ul>     <li>      <ul>       <li>GCDAsyncSocketUseCFStreamForTLS (iOS only)</li>       <li>The value must be of type NSNumber, encapsulating a BOOL value.</li>       <li>By default GCDAsyncSocket will use the SecureTransport layer to perform encryption.</li>       <li>This gives us more control over the security protocol (many more configuration options),</li>       <li>plus it allows us to optimize things like sys calls and buffer allocation.</li>       <li>However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption</li>       <li>technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket</li>       <li>will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property</li>       <li>(via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method.</li>       <li>Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket,</li>       <li>and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty.</li>       <li>For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings.<br> *</li>       <li>If unspecified, the default value is NO.</li>      </ul> </li>    </ul>    <p>从上述说明中,我们可以得知, CFStream 形式的 TLS 仅仅可以被用于 iOS 平台,并且它是一种过时的加解密技术,如果我们没有必要,最好还是不要用这种方式的 TLS 。</p>    <p>至于它的实现,我们接着往下看。</p>    <pre>  <code class="language-objectivec">//开启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];            }       }});    }</code></pre>    <p>这个方法就是对外提供的开启 TLS 的方法,它把传进来的字典,包成一个TLS的特殊包,这个 GCDAsyncSpecialPacket 类包里面就一个字典属性:</p>    <pre>  <code class="language-objectivec">- (id)initWithTLSSettings:(NSDictionary *)settings;</code></pre>    <p>然后我们把这个包添加到读写 queue 中去,并且标记当前的状态,然后去执行 maybeDequeueRead 或 maybeDequeueWrite 。</p>    <p>需要注意的是,这里只有读到这个 GCDAsyncSpecialPacket 时,才开始TLS认证和握手。</p>    <p>接着我们就来到了 maybeDequeueRead 这个方法,这个方法我们在前面第一条中讲到过,忘了的可以往上拉一下页面就可以看到。</p>    <p>它就是让我们的 ReadQueue 中的读任务离队,并且开始执行这条读任务。</p>    <ul>     <li>当我们读到的是 GCDAsyncSpecialPacket 类型的包,则开始进行TLS认证。</li>     <li>当我们读到的是 GCDAsyncReadPacket 类型的包,则开始进行一次读取数据的任务。</li>     <li>如果 ReadQueue 为空,则对几种情况进行判断,是否是读取上一次数据失败,则断开连接。<br> 如果是基于 TLS 的 Socket ,则把 SSL 安全通道的数据,移到全局缓冲区 preBuffer 中。如果数据仍然为空,则恢复读 source ,等待下一次读 source 的触发。</li>    </ul>    <p>接着我们来看看这其中第一条,当读到的是一个 GCDAsyncSpecialPacket 类型的包,我们会调用 maybeStartTLS 这个方法:</p>    <pre>  <code class="language-objectivec">//可能开启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            }       }  }</code></pre>    <p>这里根据我们之前添加标记,判断是否读写TLS状态,是才继续进行接下来的 TLS 认证。</p>    <p>接着我们拿到当前 GCDAsyncSpecialPacket ,取得配置字典中 key 为 GCDAsyncSocketUseCFStreamForTLS 的值:</p>    <p>如果为 YES 则说明使用 CFStream 形式的 TLS ,否则使用 SecureTransport 安全通道形式的 TLS 。关于这个配置项,还有二者的区别,我们前面就讲过了。</p>    <p>接着我们分别来看看这两个方法,先来看看 ssl_startTLS 。</p>    <p>这个方法非常长,大概有400多行,所以为了篇幅和大家阅读体验,楼主简化了一部分内容用省略号+注释的形式表示。大家可以参照着源码来阅读。</p>    <pre>  <code class="language-objectivec">//开启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];  }</code></pre>    <p>这个方法的结构也很清晰,主要就是建立 TLS 连接,并且配置 SSL 上下文对象: sslContext ,为 TLS 握手做准备。</p>    <p>这里我们就讲讲几个重要的关于 SSL 的函数,其余细节可以看看注释:</p>    <ol>     <li>创建SSL上下文对象: <pre>  <code class="language-objectivec">sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);  sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);</code></pre> 这个函数用来创建一个SSL上下文,我们接下来会把配置字典 tlsSettings 中所有的参数,都设置到这个 sslContext 中去,然后用这个 sslContext 进行 TLS 后续操作,握手等。</li>     <li> <p>给SSL设置读写回调:</p> <pre>  <code class="language-objectivec">status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);</code></pre> <p>这两个回调函数如下:</p> <pre>  <code class="language-objectivec">//读函数  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];  }</code></pre> <p>他们分别调用了 sslReadWithBuffer 和 sslWriteWithBuffer 两个函数进行 SSL 的读写处理,关于这两个函数,我们后面再来说。</p> </li>     <li>发起 SSL 连接: <pre>  <code class="language-objectivec">status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);</code></pre> </li>    </ol>    <p>到这一步,前置的重要操作就完成了,接下来我们是对 SSL 进行一些额外的参数配置:</p>    <p>我们根据 tlsSettings 中 GCDAsyncSocketManuallyEvaluateTrust 字段,去判断是否需要手动信任服务端证书,调用如下函数</p>    <pre>  <code class="language-objectivec">status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);</code></pre>    <p>这个函数是用来设置一些可选项的,当然不止 kSSLSessionOptionBreakOnServerAuth 这一种,还有许多种类型的可选项,感兴趣的朋友可以自行点进去看看这个枚举。</p>    <p>接着我们按照字典中的设置项,一项一项去设置ssl上下文,类似:</p>    <pre>  <code class="language-objectivec">status = SSLSetPeerDomainName(sslContext, peer, peerLen);</code></pre>    <p>设置完这些有效的,我们还需要去检查无效的 key ,万一我们设置了这些废弃的api,我们需要报错处理。</p>    <p>做完这些操作后,我们初始化了一个 sslPreBuffer ,这个 ssl 安全通道下的全局缓冲区:</p>    <pre>  <code class="language-objectivec">sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];</code></pre>    <p>然后把 prebuffer 全局缓冲区中的数据全部挪到 sslPreBuffer 中去,这里为什么要这么做呢?按照我们上面的流程图来说,正确的数据流向应该是从 sslPreBuffer -> prebuffer 的,楼主在这里也思考了很久,最后我的想法是,就是初始化的时候,数据的流向的统一,在我们真正数据读取的时候,就不需要做额外的判断了。</p>    <p>到这里我们所有的握手前初始化工作都做完了。</p>    <p>接着我们调用了 ssl_continueSSLHandshake 方法开始 SSL 握手:</p>    <pre>  <code class="language-objectivec">//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]];       }  }</code></pre>    <p>这个方法就做了一件事,就是 SSL 握手,我们调用了这个函数完成握手:</p>    <pre>  <code class="language-objectivec">OSStatus status = SSLHandshake(sslContext);</code></pre>    <p>然后握手的结果分为4种情况:</p>    <ol>     <li>如果返回为 noErr ,这个会话已经准备好了安全的通信,握手成功。</li>     <li>如果返回的 value 为 errSSLWouldBlock ,握手方法必须再次调用。</li>     <li>如果返回为 errSSLServerAuthCompleted ,如果我们要调用代理,我们需要相信服务器,然后再次调用握手,去恢复握手或者关闭连接。</li>     <li>否则,返回的 value 表明了错误的 code 。</li>    </ol>    <p>其中需要说说的是 errSSLWouldBlock ,这个是 IO 阻塞下的错误,也就是服务器的结果还没来得及返回,当握手结果返回的时候,这个方法会被再次触发。</p>    <p>还有就是 errSSLServerAuthCompleted 下,我们回调了代理:</p>    <pre>  <code class="language-objectivec">[theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];</code></pre>    <p>我们可以去手动对证书进行认证并且信任,当完成回调后,会调用到这个方法里来,再次进行握手:</p>    <pre>  <code class="language-objectivec">//修改信息后再次进行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]];      }  }</code></pre>    <p>到这里,我们就整个完成安全通道下的 TLS 认证。</p>    <p>接着我们来看看基于 CFStream 的 TLS :</p>    <p>因为 CFStream 是上层API,所以它的 TLS 流程相当简单,我们来看看 cf_startTLS 这个方法:</p>    <pre>  <code class="language-objectivec">//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...");  }</code></pre>    <p>1.这个方法很简单,首先它挂起了读写 source ,然后重新初始化了读写流,并且绑定了回调,和添加了 runloop 。</p>    <p>这里我们为什么要用重新这么做?看过之前 connect 篇的同学就知道,我们在连接成功之后,去初始化过读写流,这些操作之前都做过。而在这里重新初始化,并不会重新创建,只是修改读写流的一些参数,其中主要是下面这个方法,传递了一个 YES 过去:</p>    <pre>  <code class="language-objectivec">if (![self registerForStreamCallbacksIncludingReadWrite:YES])</code></pre>    <p>这个参数会使方法里多添加一种触发回调的方式: kCFStreamEventHasBytesAvailable 。</p>    <p>当有数据可读时候,触发 Stream 回调。</p>    <p>2.接着我们用下面这个函数把TLS的配置参数,设置给读写stream:</p>    <pre>  <code class="language-objectivec">//直接设置给读写stream  BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);  BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);</code></pre>    <p>3.最后打开读写流,整个 CFStream 形式的 TLS 就完成了。</p>    <p>看到这,大家可能对数据触发的问题有些迷惑。总结一下,我们到现在一共有3种触发的回调:</p>    <ol>     <li>读写 source :这个和 socket 绑定在一起,一旦有数据到达,就会触发事件句柄。</li>     <li>CFStream 绑定的几种事件的读写回调函数: <pre>  <code class="language-objectivec">static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)  static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)</code></pre> 这个只和 CFStream 形式的 TLS 相关,会触发这种形式的握手,流末尾等出现的错误,还有该形式下数据到达。</li>     <li>SSL 安全通道形式,绑定的 SSL 读写函数: <pre>  <code class="language-objectivec">static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)  static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)</code></pre> 这个函数并不是由系统触发,而是需要我们主动去调用 SSLRead 和 SSLWrite 两个函数,回调才能被触发。</li>    </ol>    <p>这里我们需要讲一下的是, 无论我们是否去调用该框架的 Read 方法,数据始终是到达后,触发回调,然后经过一系列的流动,最后总是流向全局缓冲区 prebuffer 。</p>    <p>而我们调用 Read ,只是从这个全局缓冲区去读取数据而已。</p>    <p>暂时的结尾:</p>    <p>篇幅原因,本篇断在这里。如果大家对本文内容有些地方不明白的话,也没关系,等我们下篇把核心方法 doReadData 讲完,在整个梳理一遍,或许大家就会对整个框架的 Read 流程有一个清晰的认识。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/fdd3d429bdb3</p>    <p> </p>    
 本文由用户 HeiUWSH 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1487126726555.html
SSL协议 iOS开发 移动开发