注册

CocoaAsyncSocket源码Read(四)

前文讲完了两次TLS建立连接的流程,接着就是本篇的重头戏了:doReadData方法。在这里我不准备直接把这个整个方法列出来,因为就光这一个方法,加上注释有1200行,整个贴过来也无法展开描述,所以在这里我打算对它分段进行讲解:

注:以下代码整个包括在doReadData大括号中:

//读取数据
- (void)doReadData
{
....
}
Part1.无法正常读取数据时的前置处理:
//如果当前读取的包为空,或者flag为读取停止,这两种情况是不能去读取数据的
if ((currentRead == nil) || (flags & kReadsPaused))
{
LogVerbose(@"No currentRead or kReadsPaused");

// Unable to read at this time
//如果是安全的通信,通过TLS/SSL
if (flags & kSocketSecure)
{
//刷新SSLBuffer,把数据从链路上移到prebuffer中 (当前不读取数据的时候做)
[self flushSSLBuffers];
}

//判断是否用的是 CFStream的TLS
if ([self usingCFStreamForTLS])
{

}
else
{
//挂起source
if (socketFDBytesAvailable > 0)
{
[self suspendReadSource];
}
}
return;
}

当我们当前读取的包是空或者标记为读停止状态的时候,则不会去读取数据。
前者不难理解,因为我们要读取的数据最终是要传给currentRead中去的,所以如果currentRead为空,我们去读数据也没有意义。
后者kReadsPaused标记是从哪里加上的呢?我们全局搜索一下,发现它才read超时的时候被添加。
讲到这我们顺便来看这个读取超时的一个逻辑,我们每次做读取任务传进来的超时,都会调用这么一个方法:

Part2.读取超时处理:
[self setupReadTimerWithTimeout:currentRead->timeout];

//初始化读的超时
- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout
{
if (timeout >= 0.0)
{
//生成一个定时器source
readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);

__weak GCDAsyncSocket *weakSelf = self;

//句柄
dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"

__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;

//执行超时操作
[strongSelf doReadTimeout];

#pragma clang diagnostic pop
}});

#if !OS_OBJECT_USE_OBJC
dispatch_source_t theReadTimer = readTimer;

//取消的句柄
dispatch_source_set_cancel_handler(readTimer, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"

LogVerbose(@"dispatch_release(readTimer)");
dispatch_release(theReadTimer);

#pragma clang diagnostic pop
});
#endif

//定时器延时 timeout时间执行
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
//间隔为永远,即只执行一次
dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(readTimer);
}
}

这个方法定义了一个GCD定时器,这个定时器只执行一次,间隔就是我们的超时,很显然这是一个延时执行,那小伙伴要问了,这里为什么我们不用NSTimer或者下面这种方式:
[self performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>

原因很简单,performSelector是基于runloop才能使用的,它本质是转化成runloop基于非端口的源source0。很显然我们所在的socketQueue开辟出来的线程,并没有添加一个runloop。而NSTimer也是一样。

所以这里我们用GCD Timer,因为它是基于XNU内核来实现的,并不需要借助于runloop

这里当超时时间间隔到达时,我们会执行超时操作:

[strongSelf doReadTimeout];


//执行超时操作
- (void)doReadTimeout
{
// This is a little bit tricky.
// Ideally we'd like to synchronously query the delegate about a timeout extension.
// But if we do so synchronously we risk a possible deadlock.
// So instead we have to do so asynchronously, and callback to ourselves from within the delegate block.

//因为这里用同步容易死锁,所以用异步从代理中回调

//标记读暂停
flags |= kReadsPaused;

__strong id theDelegate = delegate;

//判断是否实现了延时 补时的代理
if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)])
{
//拿到当前读的包
GCDAsyncReadPacket *theRead = currentRead;

//代理queue中回调
dispatch_async(delegateQueue, ^{ @autoreleasepool {

NSTimeInterval timeoutExtension = 0.0;

//调用代理方法,拿到续的时长
timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag
elapsed:theRead->timeout
bytesDone:theRead->bytesDone];

//socketQueue中,做延时
dispatch_async(socketQueue, ^{ @autoreleasepool {

[self doReadTimeoutWithExtension:timeoutExtension];
}});
}});
}
else
{
[self doReadTimeoutWithExtension:0.0];
}
}
//做读取数据延时
- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension
{
if (currentRead)
{
if (timeoutExtension > 0.0)
{
//把超时加上
currentRead->timeout += timeoutExtension;

// Reschedule the timer
//重新生成时间
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC));
//重置timer时间
dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);

// Unpause reads, and continue
//在把paused标记移除
flags &= ~kReadsPaused;
//继续去读取数据
[self doReadData];
}
else
{
//输出读取超时,并断开连接
LogVerbose(@"ReadTimeout");

[self closeWithError:[self readTimeoutError]];
}
}
}

这里调用了续时代理,如果我们实现了这个代理,则可以增加这个超时时间,然后重新生成超时定时器,移除读取停止的标记kReadsPaused。继续去读取数据。
否则我们就断开socket
注意:这个定时器会被取消,如果当前数据包被读取完成,这样就不会走到定时器超时的时间,则不会断开socket

我们接着回到doReadData中,我们讲到如果当前读取包为空或者状态为kReadsPaused,我们就去执行一些非读取数据的处理。
这里我们第一步去判断当前连接是否为kSocketSecure,也就是安全通道的TLS。如果是我们则调用:

if (flags & kSocketSecure)
{
//刷新,把TLS加密型的数据从链路上移到prebuffer中 (当前暂停的时候做)
[self flushSSLBuffers];
}

按理说,我们有当前读取包的时候,在去从prebuffersocket中去读取,但是这里为什么要提前去读呢?
我们来看看这个框架作者的解释:

// Here's the situation:
// We have an established secure connection.
// There may not be a currentRead, but there might be encrypted data sitting around for us.
// When the user does get around to issuing a read, that encrypted data will need to be decrypted.
// So why make the user wait?
// We might as well get a head start on decrypting some data now.
// The other reason we do this has to do with detecting a socket disconnection.
// The SSL/TLS protocol has it's own disconnection handshake.
// So when a secure socket is closed, a "goodbye" packet comes across the wire.
// We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection.

简单来讲,就是我们用TLS类型的Socket,读取数据的时候需要解密的过程,而这个过程是费时的,我们没必要让用户在读取数据的时候去等待这个解密的过程,我们可以提前在数据一到达,就去读取解密。
而且这种方式,还能时刻根据TLSgoodbye包来准确的检测到TCP断开连接。



作者:Cooci
链接:https://www.jianshu.com/p/5a2df8a6a54e







0 个评论

要回复文章请先登录注册