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];
}
按理说,我们有当前读取包的时候,在去从
prebuffer
、socket
中去读取,但是这里为什么要提前去读呢?我们来看看这个框架作者的解释:
// 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
,读取数据的时候需要解密的过程,而这个过程是费时的,我们没必要让用户在读取数据的时候去等待这个解密的过程,我们可以提前在数据一到达,就去读取解密。
而且这种方式,还能时刻根据TLS
的goodbye
包来准确的检测到TCP
断开连接。
作者:Cooci
链接:https://www.jianshu.com/p/5a2df8a6a54e