iOS即時通訊進階 - CocoaAsyncSocket源碼解析(Read篇)

前言:

本文為CocoaAsyncSocket源碼系列中第二篇:Read篇,將重點涉及該框架是如何利用緩沖區(qū)對數(shù)據(jù)進行讀取、以及各種情況下的數(shù)據(jù)包處理,其中還包括普通的、和基于TLS的不同讀取操作等等。
注:由于該框架源碼篇幅過大,且有大部分相對抽象的數(shù)據(jù)操作邏輯,盡管樓主竭力想要簡單的去陳述相關內(nèi)容,但是閱讀起來仍會有一定的難度。如果不是誠心想學習IM相關知識,在這里就可以離場了...

本文系列第一篇:Connect篇已經(jīng)完結(jié),感興趣可以看看:
iOS即時通訊進階 - CocoaAsyncSocket源碼解析(Connect篇)
iOS即時通訊進階 - CocoaAsyncSocket源碼解析(Connect篇終)

注:文中涉及代碼比較多,建議大家結(jié)合源碼一起閱讀比較容易能加深理解。這里有樓主標注好注釋的源碼,有需要的可以作為參照:CocoaAsyncSocket源碼注釋
如果對該框架用法不熟悉的話,可以參考樓主之前文章:
iOS即時通訊,從入門到“放棄”?,
即時通訊下數(shù)據(jù)粘包、斷包處理實例(基于CocoaAsyncSocket)
或者自行查閱。

目錄:
  • 1.淺析Read讀取,并闡述數(shù)據(jù)從socket到用戶手中的流程。?
  • 2.講講兩種TLS建立連接的過程。?
  • 3.深入講解Read的核心方法---doReadData的實現(xiàn)。?
正文:
一.淺析Read讀取,并闡述數(shù)據(jù)從socket到用戶手中的流程

大家用過這個框架就知道,我們每次讀取數(shù)據(jù)之前都需要主動調(diào)用這么一個Read方法:

[gcdSocket readDataWithTimeout:-1 tag:110];

設置一個超時和tag值,這樣我們就可以在這個超時的時間里,去讀取到達當前socket的數(shù)據(jù)了。

那么本篇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 讀取數(shù)據(jù)
- (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];
          }
     }});
}

這個方法很簡單。最終調(diào)用,去創(chuàng)建了一個GCDAsyncReadPacket類型的對象packet,簡單來說這個對象是用來標識讀取任務的。然后把這個packet對象添加到讀取隊列中。然后去調(diào)用:

[self maybeDequeueRead];

去從隊列中取出讀取任務包,做讀取操作。

還記得我們之前Connect篇講到的GCDAsyncSocket這個類的一些屬性,其中有這么一個:

//當前這次讀取數(shù)據(jù)任務包
GCDAsyncReadPacket *currentRead;

這個屬性標識了我們當前這次讀取的任務,當讀取到packet任務時,其實這個屬性就被賦值成packet,做數(shù)據(jù)讀取。

接著來看看GCDAsyncReadPacket這個類,同樣我們先看看屬性:

@interface GCDAsyncReadPacket : NSObject
{
  @public
    //當前包的數(shù)據(jù) ,(容器,有可能為空)
    NSMutableData *buffer;
    //開始偏移 (數(shù)據(jù)在容器中開始寫的偏移)
    NSUInteger startOffset;
    //已讀字節(jié)數(shù) (已經(jīng)寫了個字節(jié)數(shù))
    NSUInteger bytesDone;
    
    //想要讀取數(shù)據(jù)的最大長度 (有可能沒有)
    NSUInteger maxLength;
    //超時時長
    NSTimeInterval timeout;
    //當前需要讀取總長度  (這一次read讀取的長度,不一定有,如果沒有則可用maxLength)
    NSUInteger readLength;
    
    //包的邊界標識數(shù)據(jù) (可能沒有)
    NSData *term;
    //判斷buffer的擁有者是不是這個類,還是用戶。
    //跟初始化傳不傳一個buffer進來有關,如果傳了,則擁有者為用戶 NO, 否則為YES
    BOOL bufferOwner;
    //原始傳過來的data長度
    NSUInteger originalBufferLength;
    //數(shù)據(jù)包的tag
    long tag;
}

這個類的內(nèi)容還是比較多的,但是其實理解起來也很簡單,它主要是來裝當前任務的一些標識和數(shù)據(jù),使我們能夠正確的完成我們預期的讀取任務。
這些屬性,大家同樣過一個眼熟即可,后面大家就能理解它們了。

這個類還有一堆方法,包括初始化的、和一些數(shù)據(jù)的操作方法,其具體作用如下注釋:

//初始化
- (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;
//讀取指定長度的數(shù)據(jù)
- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;

//上兩個方法的綜合
- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;

//根據(jù)一個終結(jié)符去讀數(shù)據(jù),直到讀到終結(jié)的位置或者最大數(shù)據(jù)的位置,返回值為該包的確定長度
- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr;
////查找終結(jié)符,在prebuffer之后,返回值為該包的確定長度
- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;

這里暫時仍然不準備去講這些方法,等我們用到了在去講它。

我們通過上述的屬性和這些方法,能夠把數(shù)據(jù)正確的讀取到packet的屬性buffer中,再用代理回傳給用戶。

這個GCDAsyncReadPacket類暫時就先這樣了,我們接著往下看,前面講到調(diào)用maybeDequeueRead開始讀取任務,我們接下來就看看這個方法:

//讓讀任務離隊,開始執(zhí)行這條讀任務
- (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數(shù)據(jù)包)
        if ([readQueue count] > 0)
        {
            // Dequeue the next object in the write queue
            //使得下一個對象從寫的queue中離開
            
            //從readQueue中拿到第一個寫的數(shù)據(jù)
            currentRead = [readQueue objectAtIndex:0];
            //移除
            [readQueue removeObjectAtIndex:0];
            
            //我們的數(shù)據(jù)包,如果是GCDAsyncSpecialPacket這種類型,這個包里裝了TLS的一些設置
            //如果是這種類型的數(shù)據(jù),那么我們就進行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)
                //設置讀的任務超時,每次延時的時候還會調(diào)用 [self doReadData];
                [self setupReadTimerWithTimeout:currentRead->timeout];
                
                // Immediately read, if possible
                //讀取數(shù)據(jù)
                [self doReadData];
            }
        }
        
        //讀的隊列沒有數(shù)據(jù),標記flag為,讀了沒有數(shù)據(jù)則斷開連接狀態(tài)
        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];
                    
            //如果可讀字節(jié)數(shù)為0
            if ([preBuffer availableBytes] == 0)
            {
                //
                if ([self usingCFStreamForTLS]) {
                    // Callbacks never disabled
                }
                else {
                    //重新恢復讀的source。因為每次開始讀數(shù)據(jù)的時候,都會掛起讀的source
                    [self resumeReadSource];
                }
            }
        }
    }
}

詳細的細節(jié)看注釋即可,這里我們講講主要的作用:

  1. 我們首先做了一些是否連接,讀隊列任務是否大于0等等一些判斷。當然,如果判斷失敗,那么就不在讀取,直接返回。
  • 接著我們從全局的readQueue中,拿到第一條任務,去做讀取,我們來判斷這個任務的類型,如果是GCDAsyncSpecialPacket類型的,我們將開啟TLS認證。(后面再來詳細講)

如果是是我們之前加入隊列中的GCDAsyncReadPacket類型,我們則開始讀取操作,調(diào)用doReadData,這個方法將是整個Read篇的核心方法。

  • 如果隊列中沒有任務,我們先去判斷,是否是上一次是讀取了數(shù)據(jù),但是沒有數(shù)據(jù)的標記,如果是的話我們則斷開socket連接(注:還記得么,我們之前應用篇有說過,調(diào)取讀取任務時給一個超時,如果超過這個時間,還沒讀取到任務,則會斷開連接,就是在這觸發(fā)的)。
  • 如果我們是安全的連接(基于TLS的Socket),我們就去調(diào)用flushSSLBuffers,把數(shù)據(jù)從SSL通道中,移到我們的全局緩沖區(qū)preBuffer中。

講到這,大家可能覺得有些迷糊,為了能幫助大家理解,這里我準備了一張流程圖,來講講整個框架讀取數(shù)據(jù)的流程:

  1. 這張圖就是整個數(shù)據(jù)的流向了,這里我們讀取數(shù)據(jù)分為兩種情況,一種是基于TLS,一種是普通的數(shù)據(jù)讀取。
  • 而基于TLS的數(shù)據(jù)讀取,又分為兩種,一種是基于CFStream,另一種則是安全通道SecureTransport形式。
  • 這兩種類型的TLS都會在各自的通道內(nèi),完成數(shù)據(jù)的解密,然后解密后的數(shù)據(jù)又流向了全局緩沖區(qū)prebuffer。
  • 這個全局緩沖區(qū)prebuffer就像一個蓄水池,如果我們一直不去做讀取任務的話,它里面的數(shù)據(jù)會越來越多,當我們讀取其中所有數(shù)據(jù),它就會回歸最初的狀態(tài)。
  • 我們用currentRead的方式,從prebuffer中讀取數(shù)據(jù),當讀到我們想要的位置時,就會回調(diào)代理,用戶得到數(shù)據(jù)。
二.講講兩種TLS建立連接的過程

講到這里,就不得不提一下,這里個框架開啟TLS的過程。它對外提供了這么一個方法來開啟TLS

- (void)startTLS:(NSDictionary *)tlsSettings

可以根據(jù)一個字典,去開啟并且配置TLS,那么這個字典里包含什么內(nèi)容呢?
一共包含以下這些key

//配置SSL上下文的設置
// Configure SSLContext from given settings
// 
// Checklist:
//  1. kCFStreamSSLPeerName  //證書名
//  2. kCFStreamSSLCertificates //證書數(shù)組
//  3. GCDAsyncSocketSSLPeerID  //證書ID
//  4. GCDAsyncSocketSSLProtocolVersionMin  //SSL最低版本
//  5. GCDAsyncSocketSSLProtocolVersionMax  //SSL最高版本
//  6. GCDAsyncSocketSSLSessionOptionFalseStart  
//  7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
//  8. GCDAsyncSocketSSLCipherSuites
//  9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
//
// Deprecated (throw error): //被廢棄的參數(shù),如果設置了就會報錯關閉socket
// 10. kCFStreamSSLAllowsAnyRoot
// 11. kCFStreamSSLAllowsExpiredRoots
// 12. kCFStreamSSLAllowsExpiredCertificates
// 13. kCFStreamSSLValidatesCertificateChain
// 14. kCFStreamSSLLevel

其中有些Key的值,具體是什么意思,value如何設置,可以查查蘋果文檔,限于篇幅,我們就不贅述了,只需要了解重要的幾個參數(shù)即可。
后面一部分是被廢棄的參數(shù),如果我們設置了,就會報錯關閉socket連接。
除此之外,還有這么3個key被我們遺漏了,這3個key,是框架內(nèi)部用來判斷,并且做一些處理的標識:

kCFStreamSSLIsServer  //判斷當前是否是服務端
GCDAsyncSocketManuallyEvaluateTrust //判斷是否需要手動信任SSL
GCDAsyncSocketUseCFStreamForTLS //判斷是否使用CFStream形式的TLS

這3個key的大意如注釋,后面我們還會講到,其中最重要的是GCDAsyncSocketUseCFStreamForTLS這個key,一旦我們設置為YES,將開啟CFStream的TLS,關于這種基于流的TLS與普通的TLS的區(qū)別,我們來看看官方說明:

    • 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平臺,并且它是一種過時的加解密技術(shù),如果我們沒有必要,最好還是不要用這種方式的TLS。

至于它的實現(xiàn),我們接著往下看。

//開啟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中去,并且標記當前的狀態(tài),然后去執(zhí)行maybeDequeueReadmaybeDequeueWrite。
需要注意的是,這里只有讀到這個GCDAsyncSpecialPacket時,才開始TLS認證和握手。

接著我們就來到了maybeDequeueRead這個方法,這個方法我們在前面第一條中講到過,忘了的可以往上拉一下頁面就可以看到。
它就是讓我們的ReadQueue中的讀任務離隊,并且開始執(zhí)行這條讀任務。

  • 當我們讀到的是GCDAsyncSpecialPacket類型的包,則開始進行TLS認證。
  • 當我們讀到的是GCDAsyncReadPacket類型的包,則開始進行一次讀取數(shù)據(jù)的任務。
  • 如果ReadQueue為空,則對幾種情況進行判斷,是否是讀取上一次數(shù)據(jù)失敗,則斷開連接。
    如果是基于TLSSocket,則把SSL安全通道的數(shù)據(jù),移到全局緩沖區(qū)preBuffer中。如果數(shù)據(jù)仍然為空,則恢復讀source,等待下一次讀source的觸發(fā)。

接著我們來看看這其中第一條,當讀到的是一個GCDAsyncSpecialPacket類型的包,我們會調(diào)用maybeStartTLS這個方法:

//可能開啟TLS
- (void)maybeStartTLS
{
     
    //只有讀和寫TLS都開啟
     if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
     {
        //需要安全傳輸
          BOOL useSecureTransport = YES;
          
          #if TARGET_OS_IPHONE
          {
            //拿到當前讀的數(shù)據(jù)
               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
          }
     }
}

這里根據(jù)我們之前添加標記,判斷是否讀寫TLS狀態(tài),是才繼續(xù)進行接下來的TLS認證。
接著我們拿到當前GCDAsyncSpecialPacket,取得配置字典中keyGCDAsyncSocketUseCFStreamForTLS的值:
如果為YES則說明使用CFStream形式的TLS,否則使用SecureTransport安全通道形式的TLS。關于這個配置項,還有二者的區(qū)別,我們前面就講過了。

接著我們分別來看看這兩個方法,先來看看ssl_startTLS

這個方法非常長,大概有400多行,所以為了篇幅和大家閱讀體驗,樓主簡化了一部分內(nèi)容用省略號+注釋的形式表示。大家可以參照著源碼來閱讀。

//開啟TLS
- (void)ssl_startTLS
{
     LogTrace();
     
     LogVerbose(@"Starting TLS (via SecureTransport)...");
     
    //狀態(tài)標記
     OSStatus status;
     
    //拿到當前讀的數(shù)據(jù)包
     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
     
    //根據(jù)key來判斷,當前包是否是服務端的
     BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue];
     
    //創(chuàng)建SSL上下文
     #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
     {
        //如果是服務端的創(chuàng)建服務端上下文,否則是客戶端的上下文,用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回調(diào) 分別為SSL 讀寫函數(shù)
     status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
    //設置出錯
     if (status != noErr)
     {
          [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
          return;
     }
    
     //在握手之調(diào)用,建立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這種情況下,產(chǎn)生了不受信任的環(huán)境
          status = SSLSetEnableCertVerify(sslContext, NO);
          if (status != noErr)
          {
               [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
               return;
          }
          
          #endif
     }

    //配置SSL上下文的設置
     
     id value;
    //這個參數(shù)是用來獲取證書名驗證,如果設置為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];
     
    //如果有可讀內(nèi)容
     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];
}

這個方法的結(jié)構(gòu)也很清晰,主要就是建立TLS連接,并且配置SSL上下文對象:sslContext,為TLS握手做準備。
這里我們就講講幾個重要的關于SSL的函數(shù),其余細節(jié)可以看看注釋:

  1. 創(chuàng)建SSL上下文對象:
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);

這個函數(shù)用來創(chuàng)建一個SSL上下文,我們接下來會把配置字典tlsSettings中所有的參數(shù),都設置到這個sslContext中去,然后用這個sslContext進行TLS后續(xù)操作,握手等。

  1. 給SSL設置讀寫回調(diào):
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);

這兩個回調(diào)函數(shù)如下:

//讀函數(shù)
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?");
    
    //讀取數(shù)據(jù),并且返回狀態(tài)碼
    return [asyncSocket sslReadWithBuffer:data length:dataLength];
}
//寫函數(shù)
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];
}

他們分別調(diào)用了sslReadWithBuffersslWriteWithBuffer兩個函數(shù)進行SSL的讀寫處理,關于這兩個函數(shù),我們后面再來說。

  1. 發(fā)起SSL連接:
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);

到這一步,前置的重要操作就完成了,接下來我們是對SSL進行一些額外的參數(shù)配置:
我們根據(jù)tlsSettingsGCDAsyncSocketManuallyEvaluateTrust字段,去判斷是否需要手動信任服務端證書,調(diào)用如下函數(shù)

status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);

這個函數(shù)是用來設置一些可選項的,當然不止kSSLSessionOptionBreakOnServerAuth這一種,還有許多種類型的可選項,感興趣的朋友可以自行點進去看看這個枚舉。

接著我們按照字典中的設置項,一項一項去設置ssl上下文,類似:

status = SSLSetPeerDomainName(sslContext, peer, peerLen);

設置完這些有效的,我們還需要去檢查無效的key,萬一我們設置了這些廢棄的api,我們需要報錯處理。

做完這些操作后,我們初始化了一個sslPreBuffer,這個ssl安全通道下的全局緩沖區(qū):

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

然后把prebuffer全局緩沖區(qū)中的數(shù)據(jù)全部挪到sslPreBuffer中去,這里為什么要這么做呢?按照我們上面的流程圖來說,正確的數(shù)據(jù)流向應該是從sslPreBuffer->prebuffer的,樓主在這里也思考了很久,最后我的想法是,就是初始化的時候,數(shù)據(jù)的流向的統(tǒng)一,在我們真正數(shù)據(jù)讀取的時候,就不需要做額外的判斷了。

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

接著我們調(diào)用了ssl_continueSSLHandshake方法開始SSL握手:
//SSL的握手
- (void)ssl_continueSSLHandshake
{
     LogTrace();
    
     //用我們的SSL上下文對象去握手
     OSStatus status = SSLHandshake(sslContext);
    //拿到握手的結(jié)果,賦值給上次握手的結(jié)果
     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 {
                    //調(diào)用socket已經(jīng)開啟安全通道的代理方法
                    [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拿到證書相關的細節(jié)
          status = SSLCopyPeerTrust(sslContext, &trust);
        //SSl證書賦值出錯
          if (status != noErr)
          {
               [self closeWithError:[self sslError:status]];
               return;
          }
          
        //拿到狀態(tài)值
          int aStateIndex = stateIndex;
        //socketQueue
          dispatch_queue_t theSocketQueue = socketQueue;
          
          __weak GCDAsyncSocket *weakSelf = self;
          
        //創(chuàng)建一個完成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 - 調(diào)用代理我們自己去https認證
                    [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];
               }});
          }
        //沒實現(xiàn)代理直接報錯關閉連接。
          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握手,我們調(diào)用了這個函數(shù)完成握手:

OSStatus status = SSLHandshake(sslContext);

然后握手的結(jié)果分為4種情況:

  1. 如果返回為noErr,這個會話已經(jīng)準備好了安全的通信,握手成功。
  • 如果返回的valueerrSSLWouldBlock,握手方法必須再次調(diào)用。
  • 如果返回為errSSLServerAuthCompleted,如果我們要調(diào)用代理,我們需要相信服務器,然后再次調(diào)用握手,去恢復握手或者關閉連接。
  • 否則,返回的value表明了錯誤的code。

其中需要說說的是errSSLWouldBlock,這個是IO阻塞下的錯誤,也就是服務器的結(jié)果還沒來得及返回,當握手結(jié)果返回的時候,這個方法會被再次觸發(fā)。

還有就是errSSLServerAuthCompleted下,我們回調(diào)了代理:

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

我們可以去手動對證書進行認證并且信任,當完成回調(diào)后,會調(diào)用到這個方法里來,再次進行握手:

//修改信息后再次進行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的中可讀數(shù)據(jù)大于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];
     
    //把未讀的數(shù)據(jù)大小置為0
     socketFDBytesAvailable = 0;
    //去掉下面兩種flag
     flags &= ~kSocketCanAcceptBytes;
     flags &= ~kSecureSocketHasBytesAvailable;
     
    //標記為CFStream
     flags |=  kUsingCFStreamForTLS;
     
    //如果創(chuàng)建讀寫stream失敗
     if (![self createReadAndWriteStream])
     {
          [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
          return;
     }
     //注冊回調(diào),這回監(jiān)聽可讀數(shù)據(jù)了??!
     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,然后重新初始化了讀寫流,并且綁定了回調(diào),和添加了runloop。
這里我們?yōu)槭裁匆弥匦逻@么做?看過之前connect篇的同學就知道,我們在連接成功之后,去初始化過讀寫流,這些操作之前都做過。而在這里重新初始化,并不會重新創(chuàng)建,只是修改讀寫流的一些參數(shù),其中主要是下面這個方法,傳遞了一個YES過去:

if (![self registerForStreamCallbacksIncludingReadWrite:YES])

這個參數(shù)會使方法里多添加一種觸發(fā)回調(diào)的方式:kCFStreamEventHasBytesAvailable
當有數(shù)據(jù)可讀時候,觸發(fā)Stream回調(diào)。

2.接著我們用下面這個函數(shù)把TLS的配置參數(shù),設置給讀寫stream:

//直接設置給讀寫stream
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);

3.最后打開讀寫流,整個CFStream形式的TLS就完成了。

看到這,大家可能對數(shù)據(jù)觸發(fā)的問題有些迷惑??偨Y(jié)一下,我們到現(xiàn)在一共有3種觸發(fā)的回調(diào):
  1. 讀寫source:這個和socket綁定在一起,一旦有數(shù)據(jù)到達,就會觸發(fā)事件句柄,但是我們可以看到在cf_startTLS方法中我們調(diào)用了:
 //掛起讀寫source
[self suspendReadSource];
[self suspendWriteSource];

所以,對于CFStream形式的TLS的讀寫并不是由source觸發(fā)的,而其他的都是由source來觸發(fā)。

  1. CFStream綁定的幾種事件的讀寫回調(diào)函數(shù):
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)

這個和CFStream形式的TLS相關,會觸發(fā)這種形式的握手,流末尾等出現(xiàn)的錯誤,還有該形式下數(shù)據(jù)到達。
因為我們在一開始的連接完成就初始化過stream,所以非CFStream形式下也回觸發(fā)這個回調(diào),只是不會在數(shù)據(jù)到達觸發(fā)而已。

  1. SSL安全通道形式,綁定的SSL讀寫函數(shù):
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)

這個函數(shù)并不是由系統(tǒng)觸發(fā),而是需要我們主動去調(diào)用SSLReadSSLWrite兩個函數(shù),回調(diào)才能被觸發(fā)。

暫時的結(jié)尾:

篇幅原因,本篇斷在這里。如果大家對本文內(nèi)容有些地方不明白的話,也沒關系,等我們下篇把核心方法doReadData講完,在整個梳理一遍,或許大家就會對整個框架的Read流程有一個清晰的認識。

過完年,因為各種節(jié)后綜合征。。導致這個系列的內(nèi)容拖了比較長的時間,最近會加快腳步,早日填完這個系列的坑。

書山有路勤為徑,學海無涯苦作舟。自勉之~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容