
前言:
本文為CocoaAsyncSocket源碼系列中第二篇:Read篇,將重點(diǎn)涉及該框架是如何利用緩沖區(qū)對(duì)數(shù)據(jù)進(jìn)行讀取、以及各種情況下的數(shù)據(jù)包處理,其中還包括普通的、和基于TLS的不同讀取操作等等。
注:由于該框架源碼篇幅過(guò)大,且有大部分相對(duì)抽象的數(shù)據(jù)操作邏輯,盡管樓主竭力想要簡(jiǎn)單的去陳述相關(guān)內(nèi)容,但是閱讀起來(lái)仍會(huì)有一定的難度。如果不是誠(chéng)心想學(xué)習(xí)IM相關(guān)知識(shí),在這里就可以離場(chǎng)了...
本文系列第一篇:Connect篇已經(jīng)完結(jié),感興趣可以看看:
iOS即時(shí)通訊進(jìn)階 - CocoaAsyncSocket源碼解析(Connect篇)
iOS即時(shí)通訊進(jìn)階 - CocoaAsyncSocket源碼解析(Connect篇終)
注:文中涉及代碼比較多,建議大家結(jié)合源碼一起閱讀比較容易能加深理解。這里有樓主標(biāo)注好注釋的源碼,有需要的可以作為參照:CocoaAsyncSocket源碼注釋
如果對(duì)該框架用法不熟悉的話,可以參考樓主之前文章:
iOS即時(shí)通訊,從入門到“放棄”?,
即時(shí)通訊下數(shù)據(jù)粘包、斷包處理實(shí)例(基于CocoaAsyncSocket)
或者自行查閱。
目錄:
- 1.淺析
Read讀取,并闡述數(shù)據(jù)從socket到用戶手中的流程。? - 2.講講兩種
TLS建立連接的過(guò)程。? - 3.深入講解
Read的核心方法---doReadData的實(shí)現(xiàn)。?
正文:
一.淺析Read讀取,并闡述數(shù)據(jù)從socket到用戶手中的流程
大家用過(guò)這個(gè)框架就知道,我們每次讀取數(shù)據(jù)之前都需要主動(dòng)調(diào)用這么一個(gè)Read方法:
[gcdSocket readDataWithTimeout:-1 tag:110];
設(shè)置一個(gè)超時(shí)和tag值,這樣我們就可以在這個(gè)超時(shí)的時(shí)間里,去讀取到達(dá)當(dāng)前socket的數(shù)據(jù)了。
那么本篇Read就從這個(gè)方法開(kāi)始說(shuō)起,我們點(diǎn)進(jìn)框架里,來(lái)到這個(gè)方法:
- (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))
{
//往讀的隊(duì)列添加任務(wù),任務(wù)是包的形式
[readQueue addObject:packet];
[self maybeDequeueRead];
}
}});
}
這個(gè)方法很簡(jiǎn)單。最終調(diào)用,去創(chuàng)建了一個(gè)GCDAsyncReadPacket類型的對(duì)象packet,簡(jiǎn)單來(lái)說(shuō)這個(gè)對(duì)象是用來(lái)標(biāo)識(shí)讀取任務(wù)的。然后把這個(gè)packet對(duì)象添加到讀取隊(duì)列中。然后去調(diào)用:
[self maybeDequeueRead];
去從隊(duì)列中取出讀取任務(wù)包,做讀取操作。
還記得我們之前Connect篇講到的GCDAsyncSocket這個(gè)類的一些屬性,其中有這么一個(gè):
//當(dāng)前這次讀取數(shù)據(jù)任務(wù)包
GCDAsyncReadPacket *currentRead;
這個(gè)屬性標(biāo)識(shí)了我們當(dāng)前這次讀取的任務(wù),當(dāng)讀取到packet任務(wù)時(shí),其實(shí)這個(gè)屬性就被賦值成packet,做數(shù)據(jù)讀取。
接著來(lái)看看GCDAsyncReadPacket這個(gè)類,同樣我們先看看屬性:
@interface GCDAsyncReadPacket : NSObject
{
@public
//當(dāng)前包的數(shù)據(jù) ,(容器,有可能為空)
NSMutableData *buffer;
//開(kāi)始偏移 (數(shù)據(jù)在容器中開(kāi)始寫的偏移)
NSUInteger startOffset;
//已讀字節(jié)數(shù) (已經(jīng)寫了個(gè)字節(jié)數(shù))
NSUInteger bytesDone;
//想要讀取數(shù)據(jù)的最大長(zhǎng)度 (有可能沒(méi)有)
NSUInteger maxLength;
//超時(shí)時(shí)長(zhǎng)
NSTimeInterval timeout;
//當(dāng)前需要讀取總長(zhǎng)度 (這一次read讀取的長(zhǎng)度,不一定有,如果沒(méi)有則可用maxLength)
NSUInteger readLength;
//包的邊界標(biāo)識(shí)數(shù)據(jù) (可能沒(méi)有)
NSData *term;
//判斷buffer的擁有者是不是這個(gè)類,還是用戶。
//跟初始化傳不傳一個(gè)buffer進(jìn)來(lái)有關(guān),如果傳了,則擁有者為用戶 NO, 否則為YES
BOOL bufferOwner;
//原始傳過(guò)來(lái)的data長(zhǎng)度
NSUInteger originalBufferLength;
//數(shù)據(jù)包的tag
long tag;
}
這個(gè)類的內(nèi)容還是比較多的,但是其實(shí)理解起來(lái)也很簡(jiǎn)單,它主要是來(lái)裝當(dāng)前任務(wù)的一些標(biāo)識(shí)和數(shù)據(jù),使我們能夠正確的完成我們預(yù)期的讀取任務(wù)。
這些屬性,大家同樣過(guò)一個(gè)眼熟即可,后面大家就能理解它們了。
這個(gè)類還有一堆方法,包括初始化的、和一些數(shù)據(jù)的操作方法,其具體作用如下注釋:
//初始化
- (id)initWithData:(NSMutableData *)d
startOffset:(NSUInteger)s
maxLength:(NSUInteger)m
timeout:(NSTimeInterval)t
readLength:(NSUInteger)l
terminator:(NSData *)e
tag:(long)i;
//確保容器大小給多余的長(zhǎng)度
- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead;
////預(yù)期中讀的大小,決定是否走preBuffer
- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
//讀取指定長(zhǎng)度的數(shù)據(jù)
- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;
//上兩個(gè)方法的綜合
- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
//根據(jù)一個(gè)終結(jié)符去讀數(shù)據(jù),直到讀到終結(jié)的位置或者最大數(shù)據(jù)的位置,返回值為該包的確定長(zhǎng)度
- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr;
////查找終結(jié)符,在prebuffer之后,返回值為該包的確定長(zhǎng)度
- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;
這里暫時(shí)仍然不準(zhǔn)備去講這些方法,等我們用到了在去講它。
我們通過(guò)上述的屬性和這些方法,能夠把數(shù)據(jù)正確的讀取到packet的屬性buffer中,再用代理回傳給用戶。
這個(gè)GCDAsyncReadPacket類暫時(shí)就先這樣了,我們接著往下看,前面講到調(diào)用maybeDequeueRead開(kāi)始讀取任務(wù),我們接下來(lái)就看看這個(gè)方法:
//讓讀任務(wù)離隊(duì),開(kāi)始執(zhí)行這條讀任務(wù)
- (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
//如果當(dāng)前讀的包為空,而且flag為已連接
if ((currentRead == nil) && (flags & kConnected))
{
//如果讀的queue大于0 (里面裝的是我們封裝的GCDAsyncReadPacket數(shù)據(jù)包)
if ([readQueue count] > 0)
{
// Dequeue the next object in the write queue
//使得下一個(gè)對(duì)象從寫的queue中離開(kāi)
//從readQueue中拿到第一個(gè)寫的數(shù)據(jù)
currentRead = [readQueue objectAtIndex:0];
//移除
[readQueue removeObjectAtIndex:0];
//我們的數(shù)據(jù)包,如果是GCDAsyncSpecialPacket這種類型,這個(gè)包里裝了TLS的一些設(shè)置
//如果是這種類型的數(shù)據(jù),那么我們就進(jìn)行TLS
if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
{
LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
// Attempt to start TLS
//標(biāo)記flag為正在讀取TLS
flags |= kStartingReadTLS;
// This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
//只有讀寫都開(kāi)啟了TLS,才會(huì)做TLS認(rèn)證
[self maybeStartTLS];
}
else
{
LogVerbose(@"Dequeued GCDAsyncReadPacket");
// Setup read timer (if needed)
//設(shè)置讀的任務(wù)超時(shí),每次延時(shí)的時(shí)候還會(huì)調(diào)用 [self doReadData];
[self setupReadTimerWithTimeout:currentRead->timeout];
// Immediately read, if possible
//讀取數(shù)據(jù)
[self doReadData];
}
}
//讀的隊(duì)列沒(méi)有數(shù)據(jù),標(biāo)記flag為,讀了沒(méi)有數(shù)據(jù)則斷開(kāi)連接狀態(tài)
else if (flags & kDisconnectAfterReads)
{
//如果標(biāo)記有寫然后斷開(kāi)連接
if (flags & kDisconnectAfterWrites)
{
//如果寫的隊(duì)列為0,而且寫為空
if (([writeQueue count] == 0) && (currentWrite == nil))
{
//斷開(kāi)連接
[self closeWithError:nil];
}
}
else
{
//斷開(kāi)連接
[self closeWithError:nil];
}
}
//如果有安全socket。
else if (flags & kSocketSecure)
{
[self flushSSLBuffers];
//如果可讀字節(jié)數(shù)為0
if ([preBuffer availableBytes] == 0)
{
//
if ([self usingCFStreamForTLS]) {
// Callbacks never disabled
}
else {
//重新恢復(fù)讀的source。因?yàn)槊看伍_(kāi)始讀數(shù)據(jù)的時(shí)候,都會(huì)掛起讀的source
[self resumeReadSource];
}
}
}
}
}
詳細(xì)的細(xì)節(jié)看注釋即可,這里我們講講主要的作用:
- 我們首先做了一些是否連接,讀隊(duì)列任務(wù)是否大于0等等一些判斷。當(dāng)然,如果判斷失敗,那么就不在讀取,直接返回。
- 接著我們從全局的
readQueue中,拿到第一條任務(wù),去做讀取,我們來(lái)判斷這個(gè)任務(wù)的類型,如果是GCDAsyncSpecialPacket類型的,我們將開(kāi)啟TLS認(rèn)證。(后面再來(lái)詳細(xì)講)
如果是是我們之前加入隊(duì)列中的GCDAsyncReadPacket類型,我們則開(kāi)始讀取操作,調(diào)用doReadData,這個(gè)方法將是整個(gè)Read篇的核心方法。
- 如果隊(duì)列中沒(méi)有任務(wù),我們先去判斷,是否是上一次是讀取了數(shù)據(jù),但是沒(méi)有數(shù)據(jù)的標(biāo)記,如果是的話我們則斷開(kāi)
socket連接(注:還記得么,我們之前應(yīng)用篇有說(shuō)過(guò),調(diào)取讀取任務(wù)時(shí)給一個(gè)超時(shí),如果超過(guò)這個(gè)時(shí)間,還沒(méi)讀取到任務(wù),則會(huì)斷開(kāi)連接,就是在這觸發(fā)的)。 - 如果我們是安全的連接(基于TLS的
Socket),我們就去調(diào)用flushSSLBuffers,把數(shù)據(jù)從SSL通道中,移到我們的全局緩沖區(qū)preBuffer中。
講到這,大家可能覺(jué)得有些迷糊,為了能幫助大家理解,這里我準(zhǔn)備了一張流程圖,來(lái)講講整個(gè)框架讀取數(shù)據(jù)的流程:

- 這張圖就是整個(gè)數(shù)據(jù)的流向了,這里我們讀取數(shù)據(jù)分為兩種情況,一種是基于
TLS,一種是普通的數(shù)據(jù)讀取。
- 而基于
TLS的數(shù)據(jù)讀取,又分為兩種,一種是基于CFStream,另一種則是安全通道SecureTransport形式。 - 這兩種類型的
TLS都會(huì)在各自的通道內(nèi),完成數(shù)據(jù)的解密,然后解密后的數(shù)據(jù)又流向了全局緩沖區(qū)prebuffer。 - 這個(gè)全局緩沖區(qū)
prebuffer就像一個(gè)蓄水池,如果我們一直不去做讀取任務(wù)的話,它里面的數(shù)據(jù)會(huì)越來(lái)越多,當(dāng)我們讀取其中所有數(shù)據(jù),它就會(huì)回歸最初的狀態(tài)。 - 我們用
currentRead的方式,從prebuffer中讀取數(shù)據(jù),當(dāng)讀到我們想要的位置時(shí),就會(huì)回調(diào)代理,用戶得到數(shù)據(jù)。
二.講講兩種TLS建立連接的過(guò)程
講到這里,就不得不提一下,這里個(gè)框架開(kāi)啟TLS的過(guò)程。它對(duì)外提供了這么一個(gè)方法來(lái)開(kāi)啟TLS:
- (void)startTLS:(NSDictionary *)tlsSettings
可以根據(jù)一個(gè)字典,去開(kāi)啟并且配置TLS,那么這個(gè)字典里包含什么內(nèi)容呢?
一共包含以下這些key:
//配置SSL上下文的設(shè)置
// 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ù),如果設(shè)置了就會(huì)報(bào)錯(cuò)關(guān)閉socket
// 10. kCFStreamSSLAllowsAnyRoot
// 11. kCFStreamSSLAllowsExpiredRoots
// 12. kCFStreamSSLAllowsExpiredCertificates
// 13. kCFStreamSSLValidatesCertificateChain
// 14. kCFStreamSSLLevel
其中有些Key的值,具體是什么意思,value如何設(shè)置,可以查查蘋果文檔,限于篇幅,我們就不贅述了,只需要了解重要的幾個(gè)參數(shù)即可。
后面一部分是被廢棄的參數(shù),如果我們?cè)O(shè)置了,就會(huì)報(bào)錯(cuò)關(guān)閉socket連接。
除此之外,還有這么3個(gè)key被我們遺漏了,這3個(gè)key,是框架內(nèi)部用來(lái)判斷,并且做一些處理的標(biāo)識(shí):
kCFStreamSSLIsServer //判斷當(dāng)前是否是服務(wù)端
GCDAsyncSocketManuallyEvaluateTrust //判斷是否需要手動(dòng)信任SSL
GCDAsyncSocketUseCFStreamForTLS //判斷是否使用CFStream形式的TLS
這3個(gè)key的大意如注釋,后面我們還會(huì)講到,其中最重要的是GCDAsyncSocketUseCFStreamForTLS這個(gè)key,一旦我們?cè)O(shè)置為YES,將開(kāi)啟CFStream的TLS,關(guān)于這種基于流的TLS與普通的TLS的區(qū)別,我們來(lái)看看官方說(shuō)明:
- 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.
從上述說(shuō)明中,我們可以得知,CFStream形式的TLS僅僅可以被用于iOS平臺(tái),并且它是一種過(guò)時(shí)的加解密技術(shù),如果我們沒(méi)有必要,最好還是不要用這種方式的TLS。
至于它的實(shí)現(xiàn),我們接著往下看。
//開(kāi)啟TLS
- (void)startTLS:(NSDictionary *)tlsSettings
{
LogTrace();
if (tlsSettings == nil)
{
tlsSettings = [NSDictionary dictionary];
}
//新生成一個(gè)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標(biāo)記加上
flags |= kQueuedTLS;
//開(kāi)始讀取TLS的任務(wù),讀到這個(gè)包會(huì)做TLS認(rèn)證。在這之前的包還是不用認(rèn)證就可以傳送完
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
}});
}
這個(gè)方法就是對(duì)外提供的開(kāi)啟TLS的方法,它把傳進(jìn)來(lái)的字典,包成一個(gè)TLS的特殊包,這個(gè)GCDAsyncSpecialPacket類包里面就一個(gè)字典屬性:
- (id)initWithTLSSettings:(NSDictionary *)settings;
然后我們把這個(gè)包添加到讀寫queue中去,并且標(biāo)記當(dāng)前的狀態(tài),然后去執(zhí)行maybeDequeueRead或maybeDequeueWrite。
需要注意的是,這里只有讀到這個(gè)GCDAsyncSpecialPacket時(shí),才開(kāi)始TLS認(rèn)證和握手。
接著我們就來(lái)到了maybeDequeueRead這個(gè)方法,這個(gè)方法我們?cè)谇懊娴谝粭l中講到過(guò),忘了的可以往上拉一下頁(yè)面就可以看到。
它就是讓我們的ReadQueue中的讀任務(wù)離隊(duì),并且開(kāi)始執(zhí)行這條讀任務(wù)。
- 當(dāng)我們讀到的是
GCDAsyncSpecialPacket類型的包,則開(kāi)始進(jìn)行TLS認(rèn)證。 - 當(dāng)我們讀到的是
GCDAsyncReadPacket類型的包,則開(kāi)始進(jìn)行一次讀取數(shù)據(jù)的任務(wù)。 - 如果
ReadQueue為空,則對(duì)幾種情況進(jìn)行判斷,是否是讀取上一次數(shù)據(jù)失敗,則斷開(kāi)連接。
如果是基于TLS的Socket,則把SSL安全通道的數(shù)據(jù),移到全局緩沖區(qū)preBuffer中。如果數(shù)據(jù)仍然為空,則恢復(fù)讀source,等待下一次讀source的觸發(fā)。
接著我們來(lái)看看這其中第一條,當(dāng)讀到的是一個(gè)GCDAsyncSpecialPacket類型的包,我們會(huì)調(diào)用maybeStartTLS這個(gè)方法:
//可能開(kāi)啟TLS
- (void)maybeStartTLS
{
//只有讀和寫TLS都開(kāi)啟
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
//需要安全傳輸
BOOL useSecureTransport = YES;
#if TARGET_OS_IPHONE
{
//拿到當(dāng)前讀的數(shù)據(jù)
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
//得到設(shè)置字典
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
//拿到Key為CFStreamTLS的 value
NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];
if (value && [value boolValue])
//如果是用CFStream的,則安全傳輸為NO
useSecureTransport = NO;
}
#endif
//如果使用安全通道
if (useSecureTransport)
{
//開(kāi)啟TLS
[self ssl_startTLS];
}
//CFStream形式的Tls
else
{
#if TARGET_OS_IPHONE
[self cf_startTLS];
#endif
}
}
}
這里根據(jù)我們之前添加標(biāo)記,判斷是否讀寫TLS狀態(tài),是才繼續(xù)進(jìn)行接下來(lái)的TLS認(rèn)證。
接著我們拿到當(dāng)前GCDAsyncSpecialPacket,取得配置字典中key為GCDAsyncSocketUseCFStreamForTLS的值:
如果為YES則說(shuō)明使用CFStream形式的TLS,否則使用SecureTransport安全通道形式的TLS。關(guān)于這個(gè)配置項(xiàng),還有二者的區(qū)別,我們前面就講過(guò)了。
接著我們分別來(lái)看看這兩個(gè)方法,先來(lái)看看ssl_startTLS。
這個(gè)方法非常長(zhǎng),大概有400多行,所以為了篇幅和大家閱讀體驗(yàn),樓主簡(jiǎn)化了一部分內(nèi)容用省略號(hào)+注釋的形式表示。大家可以參照著源碼來(lái)閱讀。
//開(kāi)啟TLS
- (void)ssl_startTLS
{
LogTrace();
LogVerbose(@"Starting TLS (via SecureTransport)...");
//狀態(tài)標(biāo)記
OSStatus status;
//拿到當(dāng)前讀的數(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;
}
//拿到設(shè)置
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
// Create SSLContext, and setup IO callbacks and connection ref
//根據(jù)key來(lái)判斷,當(dāng)前包是否是服務(wù)端的
BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue];
//創(chuàng)建SSL上下文
#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
{
//如果是服務(wù)端的創(chuàng)建服務(wù)端上下文,否則是客戶端的上下文,用stream形式
if (isServer)
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
else
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
//為空則報(bào)錯(cuò)返回
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上下文設(shè)置 IO回調(diào) 分別為SSL 讀寫函數(shù)
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
//設(shè)置出錯(cuò)
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
return;
}
//在握手之調(diào)用,建立SSL連接 ,第一次連接 1
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
//連接出錯(cuò)
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
return;
}
//是否應(yīng)該手動(dòng)的去信任SSL
BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue];
//如果需要手動(dòng)去信任
if (shouldManuallyEvaluateTrust)
{
//是服務(wù)端的話,不需要,報(bào)錯(cuò)返回
if (isServer)
{
[self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]];
return;
}
//第二次連接 再去連接用kSSLSessionOptionBreakOnServerAuth的方式,去連接一次,這種方式可以直接信任服務(wù)端證書
status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
//錯(cuò)誤直接返回
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上下文的設(shè)置
id value;
//這個(gè)參數(shù)是用來(lái)獲取證書名驗(yàn)證,如果設(shè)置為NULL,則不驗(yàn)證
// 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);
//把證書名設(shè)置給SSL
status = SSLSetPeerDomainName(sslContext, peer, peerLen);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]];
return;
}
}
//不是string就錯(cuò)誤返回
else if (value)
{
//這個(gè)斷言啥用也沒(méi)有啊。。
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對(duì)應(yīng)的value,則都報(bào)棄用的錯(cuò)誤
// 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開(kāi)始讀,讀這個(gè)長(zhǎng)度到 SSL提前緩沖的writeBuffer中去
memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength);
//移動(dòng)提前的讀buffer
[preBuffer didRead:preBufferLength];
//移動(dòng)sslPreBuffer的寫buffer
[sslPreBuffer didWrite:preBufferLength];
}
//拿到上次錯(cuò)誤的code,并且讓上次錯(cuò)誤code = 沒(méi)錯(cuò)
sslErrCode = lastSSLHandshakeError = noErr;
// Start the SSL Handshake process
//開(kāi)始SSL握手過(guò)程
[self ssl_continueSSLHandshake];
}
這個(gè)方法的結(jié)構(gòu)也很清晰,主要就是建立TLS連接,并且配置SSL上下文對(duì)象:sslContext,為TLS握手做準(zhǔn)備。
這里我們就講講幾個(gè)重要的關(guān)于SSL的函數(shù),其余細(xì)節(jié)可以看看注釋:
- 創(chuàng)建SSL上下文對(duì)象:
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
這個(gè)函數(shù)用來(lái)創(chuàng)建一個(gè)SSL上下文,我們接下來(lái)會(huì)把配置字典tlsSettings中所有的參數(shù),都設(shè)置到這個(gè)sslContext中去,然后用這個(gè)sslContext進(jìn)行TLS后續(xù)操作,握手等。
- 給SSL設(shè)置讀寫回調(diào):
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
這兩個(gè)回調(diào)函數(shù)如下:
//讀函數(shù)
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
{
//拿到socket
GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;
//斷言當(dāng)前為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兩個(gè)函數(shù)進(jìn)行SSL的讀寫處理,關(guān)于這兩個(gè)函數(shù),我們后面再來(lái)說(shuō)。
- 發(fā)起
SSL連接:
status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
到這一步,前置的重要操作就完成了,接下來(lái)我們是對(duì)SSL進(jìn)行一些額外的參數(shù)配置:
我們根據(jù)tlsSettings中GCDAsyncSocketManuallyEvaluateTrust字段,去判斷是否需要手動(dòng)信任服務(wù)端證書,調(diào)用如下函數(shù)
status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
這個(gè)函數(shù)是用來(lái)設(shè)置一些可選項(xiàng)的,當(dāng)然不止kSSLSessionOptionBreakOnServerAuth這一種,還有許多種類型的可選項(xiàng),感興趣的朋友可以自行點(diǎn)進(jìn)去看看這個(gè)枚舉。
接著我們按照字典中的設(shè)置項(xiàng),一項(xiàng)一項(xiàng)去設(shè)置ssl上下文,類似:
status = SSLSetPeerDomainName(sslContext, peer, peerLen);
設(shè)置完這些有效的,我們還需要去檢查無(wú)效的key,萬(wàn)一我們?cè)O(shè)置了這些廢棄的api,我們需要報(bào)錯(cuò)處理。
做完這些操作后,我們初始化了一個(gè)sslPreBuffer,這個(gè)ssl安全通道下的全局緩沖區(qū):
sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
然后把prebuffer全局緩沖區(qū)中的數(shù)據(jù)全部挪到sslPreBuffer中去,這里為什么要這么做呢?按照我們上面的流程圖來(lái)說(shuō),正確的數(shù)據(jù)流向應(yīng)該是從sslPreBuffer->prebuffer的,樓主在這里也思考了很久,最后我的想法是,就是初始化的時(shí)候,數(shù)據(jù)的流向的統(tǒng)一,在我們真正數(shù)據(jù)讀取的時(shí)候,就不需要做額外的判斷了。
到這里我們所有的握手前初始化工作都做完了。
接著我們調(diào)用了ssl_continueSSLHandshake方法開(kāi)始SSL握手:
//SSL的握手
- (void)ssl_continueSSLHandshake
{
LogTrace();
//用我們的SSL上下文對(duì)象去握手
OSStatus status = SSLHandshake(sslContext);
//拿到握手的結(jié)果,賦值給上次握手的結(jié)果
lastSSLHandshakeError = status;
//如果沒(méi)錯(cuò)
if (status == noErr)
{
LogVerbose(@"SSLHandshake complete");
//把開(kāi)始讀寫TLS,從標(biāo)記中移除
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;
//把Socket安全通道標(biāo)記加上
flags |= kSocketSecure;
//拿到代理
__strong id theDelegate = delegate;
if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)])
{
dispatch_async(delegateQueue, ^{ @autoreleasepool {
//調(diào)用socket已經(jīng)開(kāi)啟安全通道的代理方法
[theDelegate socketDidSecure:self];
}});
}
//停止讀取
[self endCurrentRead];
//停止寫
[self endCurrentWrite];
//開(kāi)始下一次讀寫任務(wù)
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
//如果是認(rèn)證錯(cuò)誤
else if (status == errSSLPeerAuthCompleted)
{
LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval");
__block SecTrustRef trust = NULL;
//從sslContext拿到證書相關(guān)的細(xì)節(jié)
status = SSLCopyPeerTrust(sslContext, &trust);
//SSl證書賦值出錯(cuò)
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)建一個(gè)完成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認(rèn)證
[theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];
}});
}
//沒(méi)實(shí)現(xiàn)代理直接報(bào)錯(cuò)關(guā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;
}
}
//握手錯(cuò)誤為 IO阻塞的
else if (status == errSSLWouldBlock)
{
LogVerbose(@"SSLHandshake continues...");
// Handshake continues...
//
// This method will be called again from doReadData or doWriteData.
}
else
{
//其他錯(cuò)誤直接關(guān)閉連接
[self closeWithError:[self sslError:status]];
}
}
這個(gè)方法就做了一件事,就是SSL握手,我們調(diào)用了這個(gè)函數(shù)完成握手:
OSStatus status = SSLHandshake(sslContext);
然后握手的結(jié)果分為4種情況:
- 如果返回為
noErr,這個(gè)會(huì)話已經(jīng)準(zhǔn)備好了安全的通信,握手成功。
- 如果返回的
value為errSSLWouldBlock,握手方法必須再次調(diào)用。 - 如果返回為
errSSLServerAuthCompleted,如果我們要調(diào)用代理,我們需要相信服務(wù)器,然后再次調(diào)用握手,去恢復(fù)握手或者關(guān)閉連接。 - 否則,返回的
value表明了錯(cuò)誤的code。
其中需要說(shuō)說(shuō)的是errSSLWouldBlock,這個(gè)是IO阻塞下的錯(cuò)誤,也就是服務(wù)器的結(jié)果還沒(méi)來(lái)得及返回,當(dāng)握手結(jié)果返回的時(shí)候,這個(gè)方法會(huì)被再次觸發(fā)。
還有就是errSSLServerAuthCompleted下,我們回調(diào)了代理:
[theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];
我們可以去手動(dòng)對(duì)證書進(jìn)行認(rèn)證并且信任,當(dāng)完成回調(diào)后,會(huì)調(diào)用到這個(gè)方法里來(lái),再次進(jìn)行握手:
//修改信息后再次進(jìn)行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]];
}
}
到這里,我們就整個(gè)完成安全通道下的TLS認(rèn)證。
接著我們來(lái)看看基于CFStream的TLS:
因?yàn)?code>CFStream是上層API,所以它的TLS流程相當(dāng)簡(jiǎn)單,我們來(lái)看看cf_startTLS這個(gè)方法:
//CF流形式的TLS
- (void)cf_startTLS
{
LogTrace();
LogVerbose(@"Starting TLS (via CFStream)...");
//如果preBuffer的中可讀數(shù)據(jù)大于0,錯(cuò)誤關(guān)閉
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;
//標(biāo)記為CFStream
flags |= kUsingCFStreamForTLS;
//如果創(chuàng)建讀寫stream失敗
if (![self createReadAndWriteStream])
{
[self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
return;
}
//注冊(cè)回調(diào),這回監(jiān)聽(tīng)可讀數(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");
//拿到當(dāng)前包
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.
//直接設(shè)置給讀寫stream
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);
//設(shè)置失敗
if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug.
{
[self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]];
return;
}
//打開(kāi)流
if (![self openStreams])
{
[self closeWithError:[self otherError:@"Error in CFStreamOpen"]];
return;
}
LogVerbose(@"Waiting for SSL Handshake to complete...");
}
1.這個(gè)方法很簡(jiǎn)單,首先它掛起了讀寫source,然后重新初始化了讀寫流,并且綁定了回調(diào),和添加了runloop。
這里我們?yōu)槭裁匆弥匦逻@么做?看過(guò)之前connect篇的同學(xué)就知道,我們?cè)谶B接成功之后,去初始化過(guò)讀寫流,這些操作之前都做過(guò)。而在這里重新初始化,并不會(huì)重新創(chuàng)建,只是修改讀寫流的一些參數(shù),其中主要是下面這個(gè)方法,傳遞了一個(gè)YES過(guò)去:
if (![self registerForStreamCallbacksIncludingReadWrite:YES])
這個(gè)參數(shù)會(huì)使方法里多添加一種觸發(fā)回調(diào)的方式:kCFStreamEventHasBytesAvailable。
當(dāng)有數(shù)據(jù)可讀時(shí)候,觸發(fā)Stream回調(diào)。
2.接著我們用下面這個(gè)函數(shù)把TLS的配置參數(shù),設(shè)置給讀寫stream:
//直接設(shè)置給讀寫stream
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);
3.最后打開(kāi)讀寫流,整個(gè)CFStream形式的TLS就完成了。
看到這,大家可能對(duì)數(shù)據(jù)觸發(fā)的問(wèn)題有些迷惑??偨Y(jié)一下,我們到現(xiàn)在一共有3種觸發(fā)的回調(diào):
- 讀寫
source:這個(gè)和socket綁定在一起,一旦有數(shù)據(jù)到達(dá),就會(huì)觸發(fā)事件句柄,但是我們可以看到在cf_startTLS方法中我們調(diào)用了:
//掛起讀寫source
[self suspendReadSource];
[self suspendWriteSource];
所以,對(duì)于CFStream形式的TLS的讀寫并不是由source觸發(fā)的,而其他的都是由source來(lái)觸發(fā)。
-
CFStream綁定的幾種事件的讀寫回調(diào)函數(shù):
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
這個(gè)和CFStream形式的TLS相關(guān),會(huì)觸發(fā)這種形式的握手,流末尾等出現(xiàn)的錯(cuò)誤,還有該形式下數(shù)據(jù)到達(dá)。
因?yàn)槲覀冊(cè)谝婚_(kāi)始的連接完成就初始化過(guò)stream,所以非CFStream形式下也回觸發(fā)這個(gè)回調(diào),只是不會(huì)在數(shù)據(jù)到達(dá)觸發(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)
這個(gè)函數(shù)并不是由系統(tǒng)觸發(fā),而是需要我們主動(dòng)去調(diào)用SSLRead和SSLWrite兩個(gè)函數(shù),回調(diào)才能被觸發(fā)。
暫時(shí)的結(jié)尾:
篇幅原因,本篇斷在這里。如果大家對(duì)本文內(nèi)容有些地方不明白的話,也沒(méi)關(guān)系,等我們下篇把核心方法doReadData講完,在整個(gè)梳理一遍,或許大家就會(huì)對(duì)整個(gè)框架的Read流程有一個(gè)清晰的認(rèn)識(shí)。
過(guò)完年,因?yàn)楦鞣N節(jié)后綜合征。。導(dǎo)致這個(gè)系列的內(nèi)容拖了比較長(zhǎng)的時(shí)間,最近會(huì)加快腳步,早日填完這個(gè)系列的坑。
書山有路勤為徑,學(xué)海無(wú)涯苦作舟。自勉之~