
前言:
本文為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é)看注釋即可,這里我們講講主要的作用:
- 我們首先做了一些是否連接,讀隊列任務是否大于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ù)的流程:

- 這張圖就是整個數(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 encryptiontechnique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocketwill 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í)行maybeDequeueRead或maybeDequeueWrite。
需要注意的是,這里只有讀到這個GCDAsyncSpecialPacket時,才開始TLS認證和握手。
接著我們就來到了maybeDequeueRead這個方法,這個方法我們在前面第一條中講到過,忘了的可以往上拉一下頁面就可以看到。
它就是讓我們的ReadQueue中的讀任務離隊,并且開始執(zhí)行這條讀任務。
- 當我們讀到的是
GCDAsyncSpecialPacket類型的包,則開始進行TLS認證。 - 當我們讀到的是
GCDAsyncReadPacket類型的包,則開始進行一次讀取數(shù)據(jù)的任務。 - 如果
ReadQueue為空,則對幾種情況進行判斷,是否是讀取上一次數(shù)據(jù)失敗,則斷開連接。
如果是基于TLS的Socket,則把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,取得配置字典中key為GCDAsyncSocketUseCFStreamForTLS的值:
如果為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é)可以看看注釋:
- 創(chuàng)建SSL上下文對象:
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
這個函數(shù)用來創(chuàng)建一個SSL上下文,我們接下來會把配置字典tlsSettings中所有的參數(shù),都設置到這個sslContext中去,然后用這個sslContext進行TLS后續(xù)操作,握手等。
- 給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)用了sslReadWithBuffer和sslWriteWithBuffer兩個函數(shù)進行SSL的讀寫處理,關于這兩個函數(shù),我們后面再來說。
- 發(fā)起
SSL連接:
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
到這一步,前置的重要操作就完成了,接下來我們是對SSL進行一些額外的參數(shù)配置:
我們根據(jù)tlsSettings中GCDAsyncSocketManuallyEvaluateTrust字段,去判斷是否需要手動信任服務端證書,調(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種情況:
- 如果返回為
noErr,這個會話已經(jīng)準備好了安全的通信,握手成功。
- 如果返回的
value為errSSLWouldBlock,握手方法必須再次調(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認證。
接著我們來看看基于CFStream的TLS:
因為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):
- 讀寫
source:這個和socket綁定在一起,一旦有數(shù)據(jù)到達,就會觸發(fā)事件句柄,但是我們可以看到在cf_startTLS方法中我們調(diào)用了:
//掛起讀寫source
[self suspendReadSource];
[self suspendWriteSource];
所以,對于CFStream形式的TLS的讀寫并不是由source觸發(fā)的,而其他的都是由source來觸發(fā)。
-
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ā)而已。
-
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)用SSLRead和SSLWrite兩個函數(shù),回調(diào)才能被觸發(fā)。
暫時的結(jié)尾:
篇幅原因,本篇斷在這里。如果大家對本文內(nèi)容有些地方不明白的話,也沒關系,等我們下篇把核心方法doReadData講完,在整個梳理一遍,或許大家就會對整個框架的Read流程有一個清晰的認識。
過完年,因為各種節(jié)后綜合征。。導致這個系列的內(nèi)容拖了比較長的時間,最近會加快腳步,早日填完這個系列的坑。
書山有路勤為徑,學海無涯苦作舟。自勉之~