iOS即時通訊進階 - CocoaAsyncSocket源碼解析(Read篇)
前言:
本篇 ,將重點涉及該框架是如何利用緩沖區對數據進行讀取、以及各種情況下的數據包處理,其中還包括普通的、和基于 TLS 的不同讀取操作等等。
注:由于該框架源碼篇幅過大,且有大部分相對抽象的數據操作邏輯,盡管樓主竭力想要簡單的去陳述相關內容,但是閱讀起來仍會有一定的難度。如果不是誠心想學習 IM 相關知識,在這里就可以離場了...
注:文中涉及代碼比較多,建議大家結合源碼一起閱讀比較容易能加深理解。
或者自行查閱。
目錄:
- 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];
}
}
}
}
}
詳細的細節看注釋即可,這里我們講講主要的作用:
- 我們首先做了一些是否連接,讀隊列任務是否大于0等等一些判斷。當然,如果判斷失敗,那么就不在讀取,直接返回。
-
接著我們從全局的 readQueue 中,拿到第一條任務,去做讀取,我們來判斷這個任務的類型,如果是 GCDAsyncSpecialPacket 類型的,我們將開啟 TLS 認證。(后面再來詳細講)
如果是是我們之前加入隊列中的 GCDAsyncReadPacket 類型,我們則開始讀取操作,調用 doReadData ,這個方法將是整個 Read 篇的核心方法。
- 如果隊列中沒有任務,我們先去判斷,是否是上一次是讀取了數據,但是沒有數據的標記,如果是的話我們則斷開 socket 連接(注:還記得么, 我們之前應用篇有說過,調取讀取任務時給一個超時,如果超過這個時間,還沒讀取到任務,則會斷開連接,就是在這觸發的 )。
- 如果我們是安全的連接(基于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設置讀寫回調:
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 綁定在一起,一旦有數據到達,就會觸發事件句柄。
- CFStream 綁定的幾種事件的讀寫回調函數:
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
- SSL 安全通道形式,綁定的 SSL 讀寫函數:
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
這里我們需要講一下的是, 無論我們是否去調用該框架的 Read 方法,數據始終是到達后,觸發回調,然后經過一系列的流動,最后總是流向全局緩沖區 prebuffer 。
而我們調用 Read ,只是從這個全局緩沖區去讀取數據而已。
暫時的結尾:
篇幅原因,本篇斷在這里。如果大家對本文內容有些地方不明白的話,也沒關系,等我們下篇把核心方法 doReadData 講完,在整個梳理一遍,或許大家就會對整個框架的 Read 流程有一個清晰的認識。
來自:http://www.jianshu.com/p/fdd3d429bdb3