前言:
本文為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中。
講到這,大家可能覺得有些迷糊,為了能幫助大家理解,這裡我准備了一張流程圖,來講講整個框架讀取數據的流程:
這張圖就是整個數據的流向了,這裡我們讀取數據分為兩種情況,一種是基於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流程有一個清晰的認識。
過完年,因為各種節後綜合征。。導致這個系列的內容拖了比較長的時間,最近會加快腳步,早日填完這個系列的坑。