注册

CocoaAsyncSocket源码Read(一)

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

附上一张 SSL / TSL
d540873627740cde1a6abca4e549eebb.png


  • 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];
}
}
}
}
}

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

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

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

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

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

b7a8fabb52d53717ef315865db4b8838.png


  1. 这张图就是整个数据的流向了,这里我们读取数据分为两种情况,一种是基于TLS,一种是普通的数据读取。
  • 而基于TLS的数据读取,又分为两种,一种是基于CFStream,另一种则是安全通道SecureTransport形式。
  • 这两种类型的TLS都会在各自的通道内,完成数据的解密,然后解密后的数据又流向了全局缓冲区prebuffer
  • 这个全局缓冲区prebuffer就像一个蓄水池,如果我们一直不去做读取任务的话,它里面的数据会越来越多,当我们读取其中所有数据,它就会回归最初的状态。
  • 我们用currentRead的方式,从prebuffer中读取数据,当读到我们想要的位置时,就会回调代理,用户得到数据。





0 个评论

要回复文章请先登录注册