你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS即時通訊進階

iOS即時通訊進階

編輯:IOS開發基礎

2016.10_28-app-metrics.jpg

前言:

本文為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等等一些判斷。當然,如果判斷失敗,那麼就不在讀取,直接返回。

2.接著我們從全局的readQueue中,拿到第一條任務,去做讀取,我們來判斷這個任務的類型,如果是GCDAsyncSpecialPacket類型的,我們將開啟TLS認證。(後面再來詳細講)

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

3.如果隊列中沒有任務,我們先去判斷,是否是上一次是讀取了數據,但是沒有數據的標記,如果是的話我們則斷開socket連接(注:還記得麼,我們之前應用篇有說過,調取讀取任務時給一個超時,如果超過這個時間,還沒讀取到任務,則會斷開連接,就是在這觸發的)。

4.如果我們是安全的連接(基於TLS的Socket),我們就去調用flushSSLBuffers,把數據從SSL通道中,移到我們的全局緩沖區preBuffer中。

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

47.png

這張圖就是整個數據的流向了,這裡我們讀取數據分為兩種情況,一種是基於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中去,並且標記當前的狀態,然後去執行maybeDequeueRead或maybeDequeueWrite。

需要注意的是,這裡只有讀到這個GCDAsyncSpecialPacket時,才開始TLS認證和握手。

接著我們就來到了maybeDequeueRead這個方法,這個方法我們在前面第一條中講到過,忘了的可以往上拉一下頁面就可以看到。

它就是讓我們的ReadQueue中的讀任務離隊,並且開始執行這條讀任務。

當我們讀到的是GCDAsyncSpecialPacket類型的包,則開始進行TLS認證。

當我們讀到的是GCDAsyncReadPacket類型的包,則開始進行一次讀取數據的任務。

如果ReadQueue為空,則對幾種情況進行判斷,是否是讀取上一次數據失敗,則斷開連接。

如果是基於TLS的Socket,則把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,取得配置字典中key為GCDAsyncSocketUseCFStreamForTLS的值:

如果為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的函數,其余細節可以看看注釋:

創建SSL上下文對象:

sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);

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

給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];
}

他們分別調用了sslReadWithBuffer和sslWriteWithBuffer兩個函數進行SSL的讀寫處理,關於這兩個函數,我們後面再來說。

發起SSL連接:

status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);

到這一步,前置的重要操作就完成了,接下來我們是對SSL進行一些額外的參數配置:

我們根據tlsSettings中GCDAsyncSocketManuallyEvaluateTrust字段,去判斷是否需要手動信任服務端證書,調用如下函數

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種情況:

  • 如果返回為noErr,這個會話已經准備好了安全的通信,握手成功。

  • 如果返回的value為errSSLWouldBlock,握手方法必須再次調用。

  • 如果返回為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認證。

接著我們來看看基於CFStream的TLS:

因為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種觸發的回調:

讀寫source:這個和socket綁定在一起,一旦有數據到達,就會觸發事件句柄,但是我們可以看到在cf_startTLS方法中我們調用了:

//掛起讀寫source
[self suspendReadSource];
[self suspendWriteSource];

所以,對於CFStream形式的TLS的讀寫並不是由source觸發的,而其他的都是由source來觸發。

CFStream綁定的幾種事件的讀寫回調函數:

static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)

這個和CFStream形式的TLS相關,會觸發這種形式的握手,流末尾等出現的錯誤,還有該形式下數據到達。

因為我們在一開始的連接完成就初始化過stream,所以非CFStream形式下也回觸發這個回調,只是不會在數據到達觸發而已。

SSL安全通道形式,綁定的SSL讀寫函數:

static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)

這個函數並不是由系統觸發,而是需要我們主動去調用SSLRead和SSLWrite兩個函數,回調才能被觸發。

這裡我們需要講一下的是,無論我們是否去調用該框架的Read方法,數據始終是到達後,觸發回調,然後經過一系列的流動,最後總是流向全局緩沖區prebuffer。

而我們調用Read,只是從這個全局緩沖區去讀取數據而已。

暫時的結尾:

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

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

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved