iOS即时通讯之CocoaAsyncSocket源码解析五
接上篇:iOS即时通讯之CocoaAsyncSocket源码解析四 原文
前言:
本文为CocoaAsyncSocket
Read篇终,将重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于TLS
的不同读取操作等等。
正文:
前文讲完了两次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 > )
{
[self suspendReadSource];
}
}
return;
}
当我们当前读取的包是空或者标记为读停止状态的时候,则不会去读取数据。
前者不难理解,因为我们要读取的数据最终是要传给currentRead
中去的,所以如果currentRead
为空,我们去读数据也没有意义。
后者kReadsPaused
标记是从哪里加上的呢?我们全局搜索一下,发现它才read
超时的时候被添加。
讲到这我们顺便来看这个读取超时的一个逻辑,我们每次做读取任务传进来的超时,都会调用这么一个方法:
[self setupReadTimerWithTimeout:currentRead->timeout];
//初始化读的超时
- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout
{
if (timeout >= 0.0)
{
//生成一个定时器source
readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, , , 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, );
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, ); // Unpause reads, and continue
//在把paused标记移除
flags &= ~kReadsPaused;
//继续去读取数据
[self doReadData];
}
else
{
//输出读取超时,并断开连接
LogVerbose(@"ReadTimeout"); [self closeWithError:[self readTimeoutError]];
}
}
}
这里调用了续时代理,如果我们实现了这个代理,则可以增加这个超时时间,然后重新生成超时定时器,移除读取停止的标记kReadsPaused
。继续去读取数据。
否则我们就断开socket
。
注意:这个定时器会被取消,如果当前数据包被读取完成,这样就不会走到定时器超时的时间,则不会断开socket
。讲到这是不是大家就有印象了?这个就是之前在楼主:
iOS即时通讯,从入门到“放弃”?中讲过的可以被用来做PingPong
机制的原理。
我们接着回到doReadData
中,我们讲到如果当前读取包为空或者状态为kReadsPaused
,我们就去执行一些非读取数据的处理。
这里我们第一步去判断当前连接是否为kSocketSecure
,也就是安全通道的TLS
。如果是我们则调用
if (flags & kSocketSecure)
{
//刷新,把TLS加密型的数据从链路上移到prebuffer中 (当前暂停的时候做)
[self flushSSLBuffers];
}
按理说,我们有当前读取包的时候,在去从prebuffer
、socket
中去读取,但是这里为什么要提前去读呢?
我们来看看这个框架作者的解释:
// 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
,读取数据的时候需要解密的过程,而这个过程是费时的,我们没必要让用户在读取数据的时候去等待这个解密的过程,我们可以提前在数据一到达,就去读取解密。
而且这种方式,还能时刻根据TLS
的goodbye
包来准确的检测到TCP
断开连接。
在我们来看flushSSLBuffers
方法之前,我们先来看看这个一直提到的全局缓冲区prebuffer
的定义,它其实就是下面这么一个类的实例:
Part3.GCDAsyncSocketPreBuffer
的定义
@interface GCDAsyncSocketPreBuffer : NSObject
{
//unsigned char
//提前的指针,指向这块提前的缓冲区
uint8_t *preBuffer;
//size_t 它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。
//它可以存储在理论上是可能的任何类型的数组的最大大小
size_t preBufferSize;
//读的指针
uint8_t *readPointer;
//写的指针
uint8_t *writePointer;
}
里面存了3个指针,包括preBuffer起点指针、当前读写所处位置指针、以及一个preBufferSize
,这个size
为preBuffer
所指向的位置,在内存中分配的空间大小。
我们来看看它的几个方法:
//初始化
- (id)initWithCapacity:(size_t)numBytes
{
if ((self = [super init]))
{
//设置size
preBufferSize = numBytes;
//申请size大小的内存给preBuffer
preBuffer = malloc(preBufferSize); //为同一个值
readPointer = preBuffer;
writePointer = preBuffer;
}
return self;
}
包括一个初始化方法,去初始化preBufferSize
大小的一块内存空间。然后3个指针都指向这个空间。
- (void)dealloc
{
if (preBuffer)
free(preBuffer);
}
销毁的方法:释放preBuffer。
//确认读的大小
- (void)ensureCapacityForWrite:(size_t)numBytes
{
//拿到当前可用的空间大小
size_t availableSpace = [self availableSpace]; //如果申请的大小大于可用的大小
if (numBytes > availableSpace)
{
//需要多出来的大小
size_t additionalBytes = numBytes - availableSpace;
//新的总大小
size_t newPreBufferSize = preBufferSize + additionalBytes;
//重新去分配preBuffer
uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); //读的指针偏移量(已读大小)
size_t readPointerOffset = readPointer - preBuffer;
//写的指针偏移量(已写大小)
size_t writePointerOffset = writePointer - preBuffer;
//提前的Buffer重新复制
preBuffer = newPreBuffer;
//大小重新赋值
preBufferSize = newPreBufferSize; //读写指针重新赋值 + 上偏移量
readPointer = preBuffer + readPointerOffset;
writePointer = preBuffer + writePointerOffset;
}
}
确保prebuffer可用空间的方法:这个方法会重新分配preBuffer
,直到可用大小等于传递进来的numBytes
,已用大小不会变。
//仍然可读的数据,过程是先写后读,只有写的大于读的,才能让你继续去读,不然没数据可读了
- (size_t)availableBytes
{
return writePointer - readPointer;
} - (uint8_t *)readBuffer
{
return readPointer;
} - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr
{
if (bufferPtr) *bufferPtr = readPointer;
if (availableBytesPtr) *availableBytesPtr = [self availableBytes];
} //读数据的指针
- (void)didRead:(size_t)bytesRead
{
readPointer += bytesRead;
//如果读了这么多,指针和写的指针还相同的话,说明已经读完,重置指针到最初的位置
if (readPointer == writePointer)
{
// The prebuffer has been drained. Reset pointers.
readPointer = preBuffer;
writePointer = preBuffer;
}
}
//prebuffer的剩余空间 = preBufferSize(总大小) - (写的头指针 - preBuffer一开的指针,即已被写的大小) - (size_t)availableSpace
{
return preBufferSize - (writePointer - preBuffer);
} - (uint8_t *)writeBuffer
{
return writePointer;
} - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr
{
if (bufferPtr) *bufferPtr = writePointer;
if (availableSpacePtr) *availableSpacePtr = [self availableSpace];
} - (void)didWrite:(size_t)bytesWritten
{
writePointer += bytesWritten;
} - (void)reset
{
readPointer = preBuffer;
writePointer = preBuffer;
}
然后就是对读写指针进行处理的方法,如果读了多少数据readPointer
就后移多少,写也是一样。
而获取当前未读数据,则是用已写指针-已读指针,得到的差值,当已读=已写的时候,说明prebuffer数据读完,则重置读写指针的位置,还是指向初始化位置。
讲完全局缓冲区对于指针的处理,我们接着往下说
Part4.flushSSLBuffers
方法:
//缓冲ssl数据
- (void)flushSSLBuffers
{
LogTrace();
//断言为安全Socket
NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket");
//如果preBuffer有数据可读,直接返回
if ([preBuffer availableBytes] > )
{
return;
} #if TARGET_OS_IPHONE
//如果用的CFStream的TLS,把数据用CFStream的方式搬运到preBuffer中
if ([self usingCFStreamForTLS])
{
//如果flag为kSecureSocketHasBytesAvailable,而且readStream有数据可读
if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
{
LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); //默认一次读的大小为4KB??
CFIndex defaultBytesToRead = ( * ); //用来确保有这么大的提前buffer缓冲空间
[preBuffer ensureCapacityForWrite:defaultBytesToRead];
//拿到写的buffer
uint8_t *buffer = [preBuffer writeBuffer]; //从readStream中去读, 一次就读4KB,读到数据后,把数据写到writeBuffer中去 如果读的大小小于readStream中数据流大小,则会不停的触发callback,直到把数据读完为止。
CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
//打印结果
LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); //大于0,说明读写成功
if (result > )
{
//把写的buffer头指针,移动result个偏移量
[preBuffer didWrite:result];
} //把kSecureSocketHasBytesAvailable 仍然可读的标记移除
flags &= ~kSecureSocketHasBytesAvailable;
} return;
} #endif //不用CFStream的处理方法 //先设置一个预估可用的大小
__block NSUInteger estimatedBytesAvailable = ;
//更新预估可用的Block
dispatch_block_t updateEstimatedBytesAvailable = ^{ //预估大小 = 未读的大小 + SSL的可读大小
estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; size_t sslInternalBufSize = ;
//获取到ssl上下文的大小,从sslContext中
SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
//再加上下文的大小
estimatedBytesAvailable += sslInternalBufSize;
}; //调用这个Block
updateEstimatedBytesAvailable(); //如果大于0,说明有数据可读
if (estimatedBytesAvailable > )
{ LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); //标志,循环是否结束,SSL的方式是会阻塞的,直到读的数据有estimatedBytesAvailable大小为止,或者出错
BOOL done = NO;
do
{
LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); // Make sure there's enough room in the prebuffer
//确保有足够的空间给prebuffer
[preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; // Read data into prebuffer
//拿到写的buffer
uint8_t *buffer = [preBuffer writeBuffer];
size_t bytesRead = ;
//用SSLRead函数去读,读到后,把数据写到buffer中,estimatedBytesAvailable为需要读的大小,bytesRead这一次实际读到字节大小,为sslContext上下文
OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); //把写指针后移bytesRead大小
if (bytesRead > )
{
[preBuffer didWrite:bytesRead];
} LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); //如果读数据出现错误
if (result != noErr)
{
done = YES;
}
else
{
//在更新一下可读的数据大小
updateEstimatedBytesAvailable();
} }
//只有done为NO,而且 estimatedBytesAvailable大于0才继续循环
while (!done && estimatedBytesAvailable > );
}
}
这个方法有点略长,包含了两种SSL
的数据处理:
CFStream
类型:我们会调用下面这个函数去从stream
并且读取数据并解密:CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
数据被读取到后,直接转移到了prebuffer中,并且调用:
[preBuffer didWrite:result];
让写指针后移读取到的数据大小。
这里有两个关于CFReadStreamRead
方法,需要注意的问题:
1)就是我们调用它去读取4KB数据,并不仅仅是只读这么多,而是因为这个方法是会递归调用的,它每次只读4KB,直到把stream
中的数据读完。
2)我们之前设置的CFStream
函数的回调,在数据来了之后只会被触发一次,以后数据再来都不会触发。直到我们调用这个方法,把stream
中的数据读完,下次再来数据才会触发函数回调。这也是我们在使用CFStream
的时候,不需要担心像source
那样,有数据会不断的被触发回调,而需要挂起像source
那样挂起stream
(实际也没有这样的方法)。
2. SSL
安全通道类型:这里我们主要是循环去调用下面这个函数去读取数据:
OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
其他的基本和CFStream
一致
这里需要注意的是SSLRead
这个方法,并不是直接从我们的socket
中获取到的数据,而是从我们一开始绑定的SSL
回调函数中,得到数据。而回调函数本身,也需要调用read
函数从socket
中获取到加密的数据。然后再经由SSLRead
这个方法,数据被解密,并且传递给buffer
。
至于SSLRead
绑定的回调函数,是怎么处理数据读取的,因为它处理数据的流程,和我们doReadData
后续数据读取处理基本相似,所以现在暂时不提。
我们绕了一圈,讲完了这个包为空或者当前暂停状态下的前置处理,总结一下:
- 就是如果是
SSL
类型的数据,那么先解密了,缓冲到prebuffer
中去。 - 判断当前
socket
可读数据大于0,非CFStream
SSL类型,则挂起source,防止反复触发。
Part5.接着我们开始doReadData
正常数据处理流程:
首先它大的方向,依然是分为3种类型的数据处理:
1.SSL
安全通道; 2.CFStream
类型SSL
; 3.普通数据传输。
因为这3种类型的代码,重复部分较大,处理流程基本类似,只不过调用读取方法所有区别:
//1.
OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
//2.
CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
//3.
ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
而SSLRead
回调函数内部,也调用了第3种read
读取,这个我们后面会说。
现在这里我们将跳过前两种(方法部分调用可以见上面的flushSSLBuffers
方法),只讲第3种普通数据的读取操作,而SSL的读取操作,基本一致。
先来看看当前数据包任务是否完成,是如何定义的:
由于框架提供的对外read
接口:
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
将数据读取是否完成的操作,大致分为这3个类型:
1.全读;2读取一定的长度;3读取到某个标记符为止。
当且仅当上面3种类型对应的操作完成,才视作当前包任务完成,才会回调我们在类中声明的读取消息的代理:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
否则就等待着,直到当前数据包任务完成。
然后我们读取数据的流程大致如下:
先从prebuffer
中去读取,如果读完了,当前数据包任务仍未完成,那么再从socket
中去读取。
而判断包是否读完,都是用我们上面的3种类型,来对应处理的。
讲了半天理论,想必大家看的有点不耐烦了,接下来看看代码实际是如何处理的吧:
step1:从prebuffer中读取数据:
//先从提前缓冲区去读,如果缓冲区可读大小大于0
if ([preBuffer availableBytes] > )
{
// There are 3 types of read packets:
//
// 1) Read all available data.
// 2) Read a specific length of data.
// 3) Read up to a particular terminator.
//3种类型的读法,1、全读、2、读取特定长度、3、读取到一个明确的界限 NSUInteger bytesToCopy; //如果当前读的数据界限不为空
if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator
//直接读到界限
bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
}
else
{
// Read type #1 or #2
//读取数据,读到指定长度或者数据包的长度为止
bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]];
} // Make sure we have enough room in the buffer for our read.
//从上两步拿到我们需要读的长度,去看看有没有空间去存储
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; // Copy bytes from prebuffer into packet buffer //拿到我们需要追加数据的指针位置
#pragma mark - 不明白
//当前读的数据 + 开始偏移 + 已经读完的??
uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset +
currentRead->bytesDone;
//从prebuffer处复制过来数据,bytesToCopy长度
memcpy(buffer, [preBuffer readBuffer], bytesToCopy); // Remove the copied bytes from the preBuffer
//从preBuffer移除掉已经复制的数据
[preBuffer didRead:bytesToCopy]; LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); // Update totals //已读的数据加上
currentRead->bytesDone += bytesToCopy;
//当前已读的数据加上
totalBytesReadForCurrentRead += bytesToCopy; // Check to see if the read operation is done
//判断是不是读完了
if (currentRead->readLength > )
{
// Read type #2 - read a specific length of data
//如果已读 == 需要读的长度,说明已经读完
done = (currentRead->bytesDone == currentRead->readLength);
}
//判断界限标记
else if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method
//如果没做完,且读的最大长度大于0,去判断是否溢出
if (!done && currentRead->maxLength > )
{
// We're not done and there's a set maxLength.
// Have we reached that maxLength yet? //如果已读的大小大于最大的大小,则报溢出错误
if (currentRead->bytesDone >= currentRead->maxLength)
{
error = [self readMaxedOutError];
}
}
}
else
{
// Read type #1 - read all available data
//
// We're done as soon as
// - we've read all available data (in prebuffer and socket)
// - we've read the maxLength of read packet.
//判断已读大小和最大大小是否相同,相同则读完
done = ((currentRead->maxLength > ) && (currentRead->bytesDone == currentRead->maxLength));
} }
这个方法就是利用我们之前提到的3种类型,来判断数据包需要读取的长度,然后调用:
memcpy(buffer, [preBuffer readBuffer], bytesToCopy);
把数据从preBuffer
中,移到了currentRead
数据包中。
step2:从socket
中读取数据:
// 从socket中去读取 //是否读到EOFException ,这个错误指的是文件结尾了还在继续读,就会导致这个错误被抛出
BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) //如果没完成,且没错,没读到结尾,且没有可读数据了
BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more //如果没完成,且没错,没读到结尾,有可读数据
if (!done && !error && !socketEOF && hasBytesAvailable)
{
//断言,有可读数据
NSAssert(([preBuffer availableBytes] == ), @"Invalid logic");
//是否读到preBuffer中去
BOOL readIntoPreBuffer = NO;
uint8_t *buffer = NULL;
size_t bytesRead = ; //如果flag标记为安全socket
if (flags & kSocketSecure)
{
//...类似flushSSLBuffer的一系列操作
}
else
{
// Normal socket operation
//普通的socket 操作 NSUInteger bytesToRead; // There are 3 types of read packets:
//
// 1) Read all available data.
// 2) Read a specific length of data.
// 3) Read up to a particular terminator. //和上面类似,读取到边界标记??不是吧
if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator //读这个长度,如果到maxlength,就用maxlength。看如果可用空间大于需要读的空间,则不用prebuffer
bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable
shouldPreBuffer:&readIntoPreBuffer];
} else
{
// Read type #1 or #2
//直接读这个长度,如果到maxlength,就用maxlength
bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable];
} //大于最大值,则先读最大值
if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3)
bytesToRead = SIZE_MAX;
} // Make sure we have enough room in the buffer for our read.
//
// We are either reading directly into the currentRead->buffer,
// or we're reading into the temporary preBuffer. if (readIntoPreBuffer)
{
[preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer];
}
else
{
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes]
+ currentRead->startOffset
+ currentRead->bytesDone;
} // Read data into buffer int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
#pragma mark - 开始读取数据,最普通的形式 read //读数据
ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
LogVerbose(@"read from socket = %i", (int)result);
//读取错误
if (result < )
{
//EWOULDBLOCK IO阻塞
if (errno == EWOULDBLOCK)
//先等待
waiting = YES;
else
//得到错误
error = [self errnoErrorWithReason:@"Error in read() function"];
//把可读取的长度设置为0
socketFDBytesAvailable = ;
}
//读到边界了
else if (result == )
{
socketEOF = YES;
socketFDBytesAvailable = ;
}
//正常
else
{
//设置读到的数据长度
bytesRead = result; //如果读到的数据小于应该读的长度,说明这个包没读完
if (bytesRead < bytesToRead)
{
// The read returned less data than requested.
// This means socketFDBytesAvailable was a bit off due to timing,
// because we read from the socket right when the readSource event was firing.
socketFDBytesAvailable = ;
}
//正常
else
{
//如果 socketFDBytesAvailable比读了的数据小的话,直接置为0
if (socketFDBytesAvailable <= bytesRead)
socketFDBytesAvailable = ;
//减去已读大小
else
socketFDBytesAvailable -= bytesRead;
}
//如果 socketFDBytesAvailable 可读数量为0,把读的状态切换为等待
if (socketFDBytesAvailable == )
{
waiting = YES;
}
}
}
本来想讲点什么。。发现确实没什么好讲的,无非就是判断应该读取的长度,然后调用:
ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
从socket
中得到读取的实际长度。
唯一需要讲一下的可能是数据流向的问题,这里调用:
bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer];
来判断数据是否先流向prebuffer
,还是直接流向currentRead
,而SSL的读取中也有类似方法:
- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr
这个方法核心的思路就是,如果当前读取包,长度给明了,则直接流向currentRead
,如果数据长度不清楚,那么则去判断这一次读取的长度,和currentRead
可用空间长度去对比,如果长度比currentRead
可用空间小,则流向currentRead
,否则先用prebuffer
来缓冲。
至于细节方面,大家对着github
中的源码注释看看吧,这么大篇幅的业务代码,一行行讲确实没什么意义。
走完这两步读取,接着就是第三步:
step3:判断数据包完成程度:
这里有3种情况:
1.数据包刚好读完;2.数据粘包;3.数据断包;
注:这里判断粘包断包的长度,都是我们一开始调用read
方法给的长度或者分界符得出的。
很显然,第一种就什么都不用处理,完美匹配。
第二种情况,我们把需要的长度放到currentRead
,多余的长度放到prebuffer
中去。
第三种情况,数据还没读完,我们暂时为未读完。
这里就不贴代码了。
就这样普通读取数据的整个流程就走完了,而SSL
的两种模式,和上述基本一致。
我们接着根据之前读取的结果,来判断数据是否读完:
//检查是否读完
if (done)
{
//完成这次数据的读取
[self completeCurrentRead];
//如果没出错,没有到边界,prebuffer中还有可读数据
if (!error && (!socketEOF || [preBuffer availableBytes] > ))
{
//让读操作离队,继续进行下一次读取
[self maybeDequeueRead];
}
}
如果读完,则去做读完的操作,并且进行下一次读取。
我们来看看读完的操作:
//完成了这次的读数据
- (void)completeCurrentRead
{
LogTrace();
//断言currentRead
NSAssert(currentRead, @"Trying to complete current read when there is no current read."); //结果数据
NSData *result = nil; //如果是我们自己创建的Buffer
if (currentRead->bufferOwner)
{
// We created the buffer on behalf of the user.
// Trim our buffer to be the proper size.
//修剪buffer到合适的大小
//把大小设置到我们读取到的大小
[currentRead->buffer setLength:currentRead->bytesDone];
//赋值给result
result = currentRead->buffer;
}
else
{
// We did NOT create the buffer.
// The buffer is owned by the caller.
// Only trim the buffer if we had to increase its size.
//这是调用者的data,我们只会去加大尺寸
if ([currentRead->buffer length] > currentRead->originalBufferLength)
{
//拿到的读的size
NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone;
//拿到原始尺寸
NSUInteger origSize = currentRead->originalBufferLength; //取得最大的
NSUInteger buffSize = MAX(readSize, origSize);
//把buffer设置为较大的尺寸
[currentRead->buffer setLength:buffSize];
}
//拿到数据的头指针
uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; //reslut为,从头指针开始到长度为写的长度 freeWhenDone为YES,创建完就释放buffer
result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO];
} __strong id theDelegate = delegate; #pragma mark -总算到调用代理方法,接受到数据了
if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)])
{
//拿到当前的数据包
GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer dispatch_async(delegateQueue, ^{ @autoreleasepool {
//把result在代理queue中回调出去。
[theDelegate socket:self didReadData:result withTag:theRead->tag];
}});
}
//取消掉读取超时
[self endCurrentRead];
}
这里对currentRead
的data
做了个长度的设置。然后调用代理把最终包给回调出去。最后关掉我们之前提到的读取超时。
还是回到doReadData
,就剩下最后一点处理了:
//如果这次读的数量大于0
else if (totalBytesReadForCurrentRead > )
{
// We're not done read type #2 or #3 yet, but we have read in some bytes __strong id theDelegate = delegate; //如果响应读数据进度的代理
if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)])
{
long theReadTag = currentRead->tag; //代理queue中回调出去
dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag];
}});
}
}
这里未完成,如果这次读取大于0,如果响应读取进度的代理,则把当前进度回调出去。
最后检查错误:
//检查错误
if (error)
{
//如果有错直接报错断开连接
[self closeWithError:error];
}
//如果是读到边界错误
else if (socketEOF)
{
[self doReadEOF];
} //如果是等待
else if (waiting)
{
//如果用的是CFStream,则读取数据和source无关
//非CFStream形式
if (![self usingCFStreamForTLS])
{
// Monitor the socket for readability (if we're not already doing so)
//重新恢复source
[self resumeReadSource];
}
}
如果有错,直接断开socket
,如果是边界错误,调用边界错误处理,如果是等待,说明当前包还没读完,如果非CFStream
的TLS
,则恢复source
,等待下一次数据到达的触发。
关于这个读取边界错误EOF
,这里我简单的提下,其实它就是服务端发出一个边界错误,说明不会再有数据发送给我们了。我们讲无法再接收到数据,但是我们其实还是可以写数据,发送给服务端的。
而doReadEOF
这个方法的处理,就是做了这么一件事。判断我们是否需要这种不可读,只能写的连接。
我们来简单看看这个方法:
Part6.读取边界错误处理:
//读到EOFException,边界错误
- (void)doReadEOF
{
LogTrace();
//这个方法可能被调用很多次,如果读到EOF的时候,还有数据在prebuffer中,在调用doReadData之后?? 这个方法可能被持续的调用 //标记为读EOF
flags |= kSocketHasReadEOF; //如果是安全socket
if (flags & kSocketSecure)
{
//去刷新sslbuffer中的数据
[self flushSSLBuffers];
} //标记是否应该断开连接
BOOL shouldDisconnect = NO;
NSError *error = nil; //如果状态为开始读写TLS
if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS))
{
//我们得到EOF在开启TLS之前,这个TLS握手是不可能的,因此这是不可恢复的错误 //标记断开连接
shouldDisconnect = YES;
//如果是安全的TLS,赋值错误
if ([self usingSecureTransportForTLS])
{
error = [self sslError:errSSLClosedAbort];
}
}
//如果是读流关闭状态
else if (flags & kReadStreamClosed)
{ //不应该被关闭
shouldDisconnect = NO;
}
else if ([preBuffer availableBytes] > )
{
//仍然有数据可读的时候不关闭
shouldDisconnect = NO;
}
else if (config & kAllowHalfDuplexConnection)
{ //拿到socket
int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; //轮询用的结构体 /*
struct pollfd {
int fd; //文件描述符
short events; //要求查询的事件掩码 监听的
short revents; //返回的事件掩码 实际发生的
};
*/ struct pollfd pfd[];
pfd[].fd = socketFD;
//写数据不会导致阻塞。
pfd[].events = POLLOUT;
//这个为当前实际发生的事情
pfd[].revents = ; /*
poll函数使用pollfd类型的结构来监控一组文件句柄,ufds是要监控的文件句柄集合,nfds是监控的文件句柄数量,timeout是等待的毫秒数,这段时间内无论I/O是否准备好,poll都会返回。timeout为负数表示无线等待,timeout为0表示调用后立即返回。执行结果:为0表示超时前没有任何事件发生;-1表示失败;成功则返回结构体中revents不为0的文件描述符个数。pollfd结构监控的事件类型如下:
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
*/
//阻塞的,但是timeout为0,则不阻塞,直接返回
poll(pfd, , ); //如果被触发的事件是写数据
if (pfd[].revents & POLLOUT)
{
// Socket appears to still be writeable //则标记为不关闭
shouldDisconnect = NO;
//标记为读流关闭
flags |= kReadStreamClosed; // Notify the delegate that we're going half-duplex
//通知代理,我们开始半双工
__strong id theDelegate = delegate; //调用已经关闭读流的代理方法
if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)])
{
dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidCloseReadStream:self];
}});
}
}
else
{
//标记为断开
shouldDisconnect = YES;
}
}
else
{
shouldDisconnect = YES;
} //如果应该断开
if (shouldDisconnect)
{
if (error == nil)
{
//判断是否是安全TLS传输
if ([self usingSecureTransportForTLS])
{
///标记错误信息
if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful)
{
error = [self sslError:sslErrCode];
}
else
{
error = [self connectionClosedError];
}
}
else
{
error = [self connectionClosedError];
}
}
//关闭socket
[self closeWithError:error];
}
//不断开
else
{
//如果不是用CFStream流
if (![self usingCFStreamForTLS])
{
// Suspend the read source (if needed)
//挂起读source
[self suspendReadSource];
}
}
}
简单说一下,这个方法主要是对socket
是否需要主动关闭进行了判断:这里仅仅以下3种情况,不会关闭socket
:
- 读流已经是关闭状态(如果加了这个标记,说明为半双工连接状态)。
preBuffer
中还有可读数据,我们需要等数据读完才能关闭连接。- 配置标记为
kAllowHalfDuplexConnection
,我们则要开始半双工处理。我们调用了: poll(pfd, , );
函数,如果触发了写事件
POLLOUT
,说明我们半双工连接成功,则我们可以在读流关闭的状态下,仍然可以向服务器写数据。
其他情况下,一律直接关闭socket
。
而不关闭的情况下,我们会挂起source
。这样我们就只能可写不可读了。
最后还是提下SSL
的回调方法,数据解密的地方。两种模式的回调;
Part7.两种SSL
数据解密位置:
1.CFStream
:当我们调用:
CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
数据就会被解密。
2.SSL
安全通道:当我们调用:
OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
会触发SSL
绑定的函数回调:
//读函数
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];
}
接着我们在下面的方法进行了数据读取:
//SSL读取数据最终方法
- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength
{
//...
ssize_t result = read(socketFD, buf, bytesToRead);
//....
}
其实read
这一步,数据是没有被解密的,然后传递回SSLReadFunction
,在传递到SSLRead
内部,数据被解密。
尾声:
这个系列就剩下最后一篇Write
了。由于内容相对比较简单,预计就一篇写完了。
如果一直看到这里的朋友,会发现,相对之前有些内容,讲解没那么详细了。其实原因主要有两点,一是代码数量庞大,确实无法详细。二是楼主对这个系列写的有点不耐烦,想要尽快结束了..
不过至少整篇的源码注释在github
上是有的,我觉得大家自己去对着源码去阅读理解同样重要,如果一直逐字逐行的去讲,那就真的没什么意义了。
iOS即时通讯之CocoaAsyncSocket源码解析五的更多相关文章
- iOS即时通讯之CocoaAsyncSocket源码解析四
原文 前言: 本文为CocoaAsyncSocket源码系列中第二篇:Read篇,将重点涉及该框架是如何利用缓冲区对数据进行读取.以及各种情况下的数据包处理,其中还包括普通的.和基于TLS的不同读取操 ...
- iOS即时通讯之CocoaAsyncSocket源码解析二
原文 前言 本文承接上文:iOS即时通讯之CocoaAsyncSocket源码解析一 上文我们提到了GCDAsyncSocket的初始化,以及最终connect之前的准备工作,包括一些错误检查:本机地 ...
- iOS即时通讯之CocoaAsyncSocket源码解析一
申明:本文内容属于转载整理,原文连接 前言: CocoaAsyncSocket是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的.强大的异步套接字库,向上封装 ...
- iOS即时通讯之CocoaAsyncSocket源码解析三
原文 前言 本文实例Github地址:即时通讯的数据粘包.断包处理实例. 本文旨以实例的方式,使用CocoaAsyncSocket这个框架进行数据封包和拆包.来解决频繁的数据发送下,导致的数据粘包.以 ...
- Celery 源码解析五: 远程控制管理
今天要聊的话题可能被大家关注得不过,但是对于 Celery 来说确实很有用的功能,曾经我在工作中遇到这类情况,就是我们将所有的任务都放在同一个队列里面,然后有一天突然某个同学的代码写得不对,导致大量的 ...
- dubbo源码解析五 --- 集群容错架构设计与原理分析
欢迎来我的 Star Followers 后期后继续更新Dubbo别的文章 Dubbo 源码分析系列之一环境搭建 博客园 Dubbo 入门之二 --- 项目结构解析 博客园 Dubbo 源码分析系列之 ...
- iOS开发之Masonry框架源码解析
Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...
- ReactiveCocoa源码解析(五) SignalProtocol的observe()、Map、Filter延展实现
上篇博客我们对Signal的基本实现以及Signal的面向协议扩展进行了介绍, 详细内容请移步于<Signal中的静态属性静态方法以及面向协议扩展>.并且聊了Signal的所有的g功能扩展 ...
- ReactiveSwift源码解析(五) SignalProtocol的observe()、Map、Filter延展实现
上篇博客我们对Signal的基本实现以及Signal的面向协议扩展进行了介绍, 详细内容请移步于<Signal中的静态属性静态方法以及面向协议扩展>.并且聊了Signal的所有的g功能扩展 ...
随机推荐
- JProfiler监控
原文: https://blog.csdn.net/jijilan/article/details/83022715
- 最大连续和 Medium
Given a two-dimensional array of positive and negative integers, a sub-rectangle is any contiguous s ...
- N皇后问题(递归)
//八皇后递归解法 //#include<iostream> //using namespace std; #include<stdio.h> ] = {-,-,-,-,-,- ...
- 标准库path源码解读
先看标准库 作用:关于路径的一些实用操作 https://github.com/golang/go/blob/master/src/path/path.go 源码地址 func IsAbs func ...
- JSP页面包含其他页面的三种方式及区别
一. <%@ include file="header.inc"%> 该指令在编译之前先读入指定的页面中的内容(并不对动态内容求值),融合后的完整页面再被整体的转换为一 ...
- 北京师范大学第十五届ACM决赛-重现赛D Disdain Chain (规律+组合数学)
链接:https://ac.nowcoder.com/acm/contest/3/D 来源:牛客网 Disdain Chain 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 2621 ...
- poj 3714 Raid(平面最近点对)
Raid Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 7473 Accepted: 2221 Description ...
- Eclipse开发工具的编码问题
乱码:文件有一个编码,打开文件的工具(Eclipse或者浏览器)有一个编码,当两个编码不同就会出现编码异常或乱码. 参考: Eclipse修改编码格式 背景:在Eclipse的开发使用中,我们经常使用 ...
- 大数据学习笔记之初识Hadoop
1.Hadoop概述 1.1 Hadoop名字的由来 Hadoop项目作者的孩子给一个棕黄色的大象样子的填充玩具的命名 Hadoop的官网:http://hadoop.apache.org . 1.2 ...
- Alter改变终结
#alter#删除date列但若表中只有一个字段无法使用drop删除ALTER TABLE z_staff_info_copy1 DROP `date`;ALTER TABLE z_staff_inf ...