前言:
本文为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];
}
}
}
}
}
详细的细节看注释即可,这里我们讲讲主要的作用:
- 我们首先做了一些是否连接,读队列任务是否大于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上下文,我们接下来会把配置字典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
两个函数,回调才能被触发。
暂时的结尾:
篇幅原因,本篇断在这里。如果大家对本文内容有些地方不明白的话,也没关系,等我们下篇把核心方法doReadData
讲完,在整个梳理一遍,或许大家就会对整个框架的Read
流程有一个清晰的认识。
过完年,因为各种节后综合征。。导致这个系列的内容拖了比较长的时间,最近会加快脚步,早日填完这个系列的坑。
书山有路勤为径,学海无涯苦作舟。自勉之~
网友评论
1、网络优化之就近接入。
2、网络优化之不同网络类型配置调优。
3、网络优化之IP连接策略(tcp、http自动切换)。
4、网络优化之防dns劫持。
5、网络优化之分片、断点续传策略。
6、网络优化之应用层ack。
7、网络优化之超时重传策略。
8、网络优化之数据协议选择、流量控制。
9、网络传输数据加解密。