iOS即时通讯之CocoaAsyncSocket源码解析四

Posted Francis

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS即时通讯之CocoaAsyncSocket源码解析四相关的知识,希望对你有一定的参考价值。

原文

前言:

本文为CocoaAsyncSocket源码系列中第二篇:Read篇,将重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于TLS的不同读取操作等等。
注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。如果不是诚心想学习IM相关知识,在这里就可以离场了...

 注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释

目录:
  • 1.浅析Read读取,并阐述数据从socket到用户手中的流程。✅
  • 2.讲讲两种TLS建立连接的过程。✅
  • 3.深入讲解Read的核心方法---doReadData的实现。❌

正文:

一.浅析Read读取,并阐述数据从socket到用户手中的流程

大家用过这个框架就知道,我们每次读取数据之前都需要主动调用这么一个Read方法:

[gcdSocket readDataWithTimeout:-1 tag:110];

设置一个超时和tag值,这样我们就可以在这个超时的时间里,去读取到达当前socket的数据了。

那么本篇Read就从这个方法开始说起,我们点进框架里,来到这个方法:

 1 - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag
 2 {
 3      [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
 4 }
 5 
 6 - (void)readDataWithTimeout:(NSTimeInterval)timeout
 7                      buffer:(NSMutableData *)buffer
 8                bufferOffset:(NSUInteger)offset
 9                         tag:(long)tag
10 {
11      [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
12 }
13 
14 //用偏移量 maxLength 读取数据
15 - (void)readDataWithTimeout:(NSTimeInterval)timeout
16                      buffer:(NSMutableData *)buffer
17                bufferOffset:(NSUInteger)offset
18                   maxLength:(NSUInteger)length
19                         tag:(long)tag
20 {
21      if (offset > [buffer length]) {
22           LogWarn(@"Cannot read: offset > [buffer length]");
23           return;
24      }
25 
26      GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
27                                                                startOffset:offset
28                                                                  maxLength:length
29                                                                    timeout:timeout
30                                                                 readLength:0
31                                                                 terminator:nil
32                                                                        tag:tag];
33 
34      dispatch_async(socketQueue, ^{ @autoreleasepool {
35 
36           LogTrace();
37 
38           if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
39           {
40             //往读的队列添加任务,任务是包的形式
41                [readQueue addObject:packet];
42                [self maybeDequeueRead];
43           }
44      }});
45 }

这个方法很简单。最终调用,去创建了一个GCDAsyncReadPacket类型的对象packet,简单来说这个对象是用来标识读取任务的。然后把这个packet对象添加到读取队列中。然后去调用:

[self maybeDequeueRead];

去从队列中取出读取任务包,做读取操作。

还记得我们之前Connect篇讲到的GCDAsyncSocket这个类的一些属性,其中有这么一个: 

//当前这次读取数据任务包
GCDAsyncReadPacket *currentRead;

这个属性标识了我们当前这次读取的任务,当读取到packet任务时,其实这个属性就被赋值成packet,做数据读取。

接着来看看GCDAsyncReadPacket这个类,同样我们先看看属性:

 1 @interface GCDAsyncReadPacket : NSObject
 2 {
 3   @public
 4     //当前包的数据 ,(容器,有可能为空)
 5     NSMutableData *buffer;
 6     //开始偏移 (数据在容器中开始写的偏移)
 7     NSUInteger startOffset;
 8     //已读字节数 (已经写了个字节数)
 9     NSUInteger bytesDone;
10 
11     //想要读取数据的最大长度 (有可能没有)
12     NSUInteger maxLength;
13     //超时时长
14     NSTimeInterval timeout;
15     //当前需要读取总长度  (这一次read读取的长度,不一定有,如果没有则可用maxLength)
16     NSUInteger readLength;
17 
18     //包的边界标识数据 (可能没有)
19     NSData *term;
20     //判断buffer的拥有者是不是这个类,还是用户。
21     //跟初始化传不传一个buffer进来有关,如果传了,则拥有者为用户 NO, 否则为YES
22     BOOL bufferOwner;
23     //原始传过来的data长度
24     NSUInteger originalBufferLength;
25     //数据包的tag
26     long tag;
27 }

这个类的内容还是比较多的,但是其实理解起来也很简单,它主要是来装当前任务的一些标识和数据,使我们能够正确的完成我们预期的读取任务。
这些属性,大家同样过一个眼熟即可,后面大家就能理解它们了。

这个类还有一堆方法,包括初始化的、和一些数据的操作方法,其具体作用如下注释:

 1 //初始化
 2 - (id)initWithData:(NSMutableData *)d
 3        startOffset:(NSUInteger)s
 4          maxLength:(NSUInteger)m
 5            timeout:(NSTimeInterval)t
 6         readLength:(NSUInteger)l
 7         terminator:(NSData *)e
 8                tag:(long)i;
 9 
10 //确保容器大小给多余的长度
11 - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead;
12 ////预期中读的大小,决定是否走preBuffer
13 - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
14 //读取指定长度的数据
15 - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;
16 
17 //上两个方法的综合
18 - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
19 
20 //根据一个终结符去读数据,直到读到终结的位置或者最大数据的位置,返回值为该包的确定长度
21 - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr;
22 ////查找终结符,在prebuffer之后,返回值为该包的确定长度
23 - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;

这里暂时仍然不准备去讲这些方法,等我们用到了在去讲它。

我们通过上述的属性和这些方法,能够把数据正确的读取到packet的属性buffer中,再用代理回传给用户。

这个GCDAsyncReadPacket类暂时就先这样了,我们接着往下看,前面讲到调用maybeDequeueRead开始读取任务,我们接下来就看看这个方法:

//让读任务离队,开始执行这条读任务
- (void)maybeDequeueRead
{
    LogTrace();
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");

    // If we\'re not currently processing a read AND we have an available read stream

    //如果当前读的包为空,而且flag为已连接
    if ((currentRead == nil) && (flags & kConnected))
    {
        //如果读的queue大于0 (里面装的是我们封装的GCDAsyncReadPacket数据包)
        if ([readQueue count] > 0)
        {
            // Dequeue the next object in the write queue
            //使得下一个对象从写的queue中离开

            //从readQueue中拿到第一个写的数据
            currentRead = [readQueue objectAtIndex:0];
            //移除
            [readQueue removeObjectAtIndex:0];

            //我们的数据包,如果是GCDAsyncSpecialPacket这种类型,这个包里装了TLS的一些设置
            //如果是这种类型的数据,那么我们就进行TLS
            if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
            {
                LogVerbose(@"Dequeued GCDAsyncSpecialPacket");

                // Attempt to start TLS
                //标记flag为正在读取TLS
                flags |= kStartingReadTLS;

                // This method won\'t do anything unless both kStartingReadTLS and kStartingWriteTLS are set
                //只有读写都开启了TLS,才会做TLS认证
                [self maybeStartTLS];
            }
            else
            {
                LogVerbose(@"Dequeued GCDAsyncReadPacket");

                // Setup read timer (if needed)
                //设置读的任务超时,每次延时的时候还会调用 [self doReadData];
                [self setupReadTimerWithTimeout:currentRead->timeout];

                // Immediately read, if possible
                //读取数据
                [self doReadData];
            }
        }

        //读的队列没有数据,标记flag为,读了没有数据则断开连接状态
        else if (flags & kDisconnectAfterReads)
        {
            //如果标记有写然后断开连接
            if (flags & kDisconnectAfterWrites)
            {
                //如果写的队列为0,而且写为空
                if (([writeQueue count] == 0) && (currentWrite == nil))
                {
                    //断开连接
                    [self closeWithError:nil];
                }
            }
            else
            {
                //断开连接
                [self closeWithError:nil];
            }
        }
        //如果有安全socket。
        else if (flags & kSocketSecure)
        {
            [self flushSSLBuffers];

            //如果可读字节数为0
            if ([preBuffer availableBytes] == 0)
            {
                //
                if ([self usingCFStreamForTLS]) {
                    // Callbacks never disabled
                }
                else {
                    //重新恢复读的source。因为每次开始读数据的时候,都会挂起读的source
                    [self resumeReadSource];
                }
            }
        }
    }
}

详细的细节看注释即可,这里我们讲讲主要的作用:

  1. 我们首先做了一些是否连接,读队列任务是否大于0等等一些判断。当然,如果判断失败,那么就不在读取,直接返回。
  2. 接着我们从全局的readQueue中,拿到第一条任务,去做读取,我们来判断这个任务的类型,如果是GCDAsyncSpecialPacket类型的,我们将开启TLS认证。(后面再来详细讲)

    如果是是我们之前加入队列中的GCDAsyncReadPacket类型,我们则开始读取操作,调用doReadData,这个方法将是整个Read篇的核心方法。

  3. 如果队列中没有任务,我们先去判断,是否是上一次是读取了数据,但是没有数据的标记,如果是的话我们则断开socket连接(注:还记得么,我们之前应用篇有说过,调取读取任务时给一个超时,如果超过这个时间,还没读取到任务,则会断开连接,就是在这触发的)。
  4. 如果我们是安全的连接(基于TLS的Socket),我们就去调用flushSSLBuffers,把数据从SSL通道中,移到我们的全局缓冲区preBuffer中。

讲到这,大家可能觉得有些迷糊,为了能帮助大家理解,这里我准备了一张流程图,来讲讲整个框架读取数据的流程:

  1. 这张图就是整个数据的流向了,这里我们读取数据分为两种情况,一种是基于TLS,一种是普通的数据读取。
  2. 而基于TLS的数据读取,又分为两种,一种是基于CFStream,另一种则是安全通道SecureTransport形式。
  3. 这两种类型的TLS都会在各自的通道内,完成数据的解密,然后解密后的数据又流向了全局缓冲区prebuffer
  4. 这个全局缓冲区prebuffer就像一个蓄水池,如果我们一直不去做读取任务的话,它里面的数据会越来越多,当我们读取其中所有数据,它就会回归最初的状态。
  5. 我们用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

至于它的实现,我们接着往下看。

 1 //开启TLS
 2 - (void)startTLS:(NSDictionary *)tlsSettings
 3 {
 4      LogTrace();
 5 
 6      if (tlsSettings == nil)
 7     {
 8 
 9         tlsSettings = [NSDictionary dictionary];
10     }
11      //新生成一个TLS特殊的包
12      GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
13 
14 
15      dispatch_async(socketQueue, ^{ @autoreleasepool {
16 
17           if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
18           {
19             //添加到读写Queue中去
20                [readQueue addObject:packet];
21                [writeQueue addObject:packet];
22                //把TLS标记加上
23                flags |= kQueuedTLS;
24                //开始读取TLS的任务,读到这个包会做TLS认证。在这之前的包还是不用认证就可以传送完
25                [self maybeDequeueRead];
26                [self maybeDequeueWrite];
27           }
28      }});
29 
30 }

这个方法就是对外提供的开启TLS的方法,它把传进来的字典,包成一个TLS的特殊包,这个GCDAsyncSpecialPacket类包里面就一个字典属性: 

- (id)initWithTLSSettings:(NSDictionary *)settings;

然后我们把这个包添加到读写queue中去,并且标记当前的状态,然后去执行maybeDequeueReadmaybeDequeueWrite
需要注意的是,这里只有读到这个GCDAsyncSpecialPacket时,才开始TLS认证和握手。

接着我们就来到了maybeDequeueRead这个方法,这个方法我们在前面第一条中讲到过,忘了的可以往上拉一下页面就可以看到。
它就是让我们的ReadQueue中的读任务离队,并且开始执行这条读任务。

  • 当我们读到的是GCDAsyncSpecialPacket类型的包,则开始进行TLS认证。
  • 当我们读到的是GCDAsyncReadPacket类型的包,则开始进行一次读取数据的任务。
  • 如果ReadQueue为空,则对几种情况进行判断,是否是读取上一次数据失败,则断开连接。
    如果是基于TLSSocket,则把SSL安全通道的数据,移到全局缓冲区preBuffer中。如果数据仍然为空,则恢复读source,等待下一次读source的触发。

接着我们来看看这其中第一条,当读到的是一个GCDAsyncSpecialPacket类型的包,我们会调用maybeStartTLS这个方法: 

 1 //可能开启TLS
 2 - (void)maybeStartTLS
 3 {
 4 
 5     //只有读和写TLS都开启
 6      if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
 7      {
 8         //需要安全传输
 9           BOOL useSecureTransport = YES;
10 
11           #if TARGET_OS_IPHONE
12           {
13             //拿到当前读的数据
14                GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
15             //得到设置字典
16                NSDictionary *tlsSettings = tlsPacket->tlsSettings;
17 
18             //拿到Key为CFStreamTLS的 value
19                NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];
20 
21                if (value && [value boolValue])
22                 //如果是用CFStream的,则安全传输为NO
23                     useSecureTransport = NO;
24           }
25           #endif
26           //如果使用安全通道
27           if (useSecureTransport)
28           {
29             //开启TLS
30                [self ssl_startTLS];
31           }
32         //CFStream形式的Tls
33           else
34           {
35           #if TARGET_OS_IPHONE
36                [self cf_startTLS];
37           #endif
38           }
39      }
40 }

这里根据我们之前添加标记,判断是否读写TLS状态,是才继续进行接下来的TLS认证。
接着我们拿到当前GCDAsyncSpecialPacket,取得配置字典中keyGCDAsyncSocketUseCFStreamForTLS的值:
如果为YES则说明使用CFStream形式的TLS,否则使用SecureTransport安全通道形式的TLS。关于这个配置项,还有二者的区别,我们前面就讲过了。

接着我们分别来看看这两个方法,先来看看ssl_startTLS

这个方法非常长,大概有400多行,所以为了篇幅和大家阅读体验,楼主简化了一部分内容用省略号+注释的形式表示。大家可以参照着源码来阅读。

  1 //开启TLS
  2 - (void)ssl_startTLS
  3 {
  4      LogTrace();
  5 
  6      LogVerbose(@"Starting TLS (via SecureTransport)...");
  7 
  8     //状态标记
  9      OSStatus status;
 10 
 11     //拿到当前读的数据包
 12      GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
 13      if (tlsPacket == nil) // Code to quiet the analyzer
 14      {
 15           NSAssert(NO, @"Logic error");
 16 
 17           [self closeWithError:[self otherError:@"Logic error"]];
 18           return;
 19      }
 20     //拿到设置
 21      NSDictionary *tlsSettings = tlsPacket->tlsSettings;
 22 
 23      // Create SSLContext, and setup IO callbacks and connection ref
 24 
 25     //根据key来判断,当前包是否是服务端的
 26      BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue];
 27 
 28     //创建SSL上下文
 29      #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
 30      {
 31         //如果是服务端的创建服务端上下文,否则是客户端的上下文,用stream形式
 32           if (isServer)
 33                sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
 34           else
 35                sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
 36           //为空则报错返回
 37           if (sslContext == NULL)
 38           {
 39                [self closeWithError:[self otherError:@"Error in SSLCreateContext"]];
 40                return;
 41           }
 42      }
 43 
 44      #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
 45      {
 46           status = SSLNewContext(isServer, &sslContext);
 47           if (status != noErr)
 48           {
 49                [self closeWithError:[self otherError:@"Error in SSLNewContext"]];
 50                return;
 51           }
 52      }
 53      #endif
 54 
 55     //给SSL上下文设置 IO回调 分别为SSL 读写函数
 56      status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
 57     //设置出错
 58      if (status != noErr)
 59      {
 60           [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
 61           return;
 62      }
 63 
 64      //在握手之调用,建立SSL连接 ,第一次连接 1
 65      status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
 66     //连接出错
 67      if (status != noErr)
 68      {
 69           [self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
 70           return;
 71      }
 72 
 73     //是否应该手动的去信任SSL
 74      BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue];
 75     //如果需要手动去信任
 76      if (shouldManuallyEvaluateTrust)
 77      {
 78         //是服务端的话,不需要,报错返回
 79           if (isServer)
 80           {
 81                [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]];
 82                return;
 83           }
 84           //第二次连接 再去连接用kSSLSessionOptionBreakOnServerAuth的方式,去连接一次,这种方式可以直接信任服务端证书
 85           status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
 86         //错误直接返回
 87           if (status != noErr)
 88           {
 89                [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]];
 90                return;
 91           }
 92 
 93           #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
 94 
 95           // Note from Apple\'s documentation:
 96           //
 97           // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8.
 98           // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the
 99           // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus
1

以上是关于iOS即时通讯之CocoaAsyncSocket源码解析四的主要内容,如果未能解决你的问题,请参考以下文章

IOS 即时通讯 + 微信聊天框架 + 源码

李洪强关于即时通讯

iOS开发之即时通讯之Socket(AsyncSocket)

iOS即时通讯之简单聊天室

iOS开发之即时通讯之Socket(AsyncSocket)

iOS开发之即时通讯之Socket(AsyncSocket)