socket
对于没有网络编程经验的我们,对这个socket的认识仅停留在各个教程的CS模型点对点聊天上,基本上就是服务端创建一个socket,然后调用bind、listen、accept,客户端就是创建一个socket,然后调用connect.认识仅仅是如此,实际上socket编程的基本流程就是这样,接下来我们会看到GCDAsyncSocket里面的流程也是如此。
源码结构
GCDAsyncSocket就一个.h文件,一个.m文件。
主要的类
GCDAsyncSocketPreBuffer
这个类的作用是作为一个临时的缓冲区,来存放从socket里面读取的内容.这个类会存在扩容的情况.
@interface GCDAsyncSocketPreBuffer : NSObject {
uint8_t *preBuffer; //申请的内存的指针
size_t preBufferSize; //申请了多少内存
uint8_t *readPointer; //是从当前的preBuffer里面读取了多少内容.(从preBuffer里面读了多少)
uint8_t *writePointer; //给preBuffer写入的内容的多少内容的位置.(往preBuffer里面写了多少)
}
主要结构如下图
ensureCapacityForWrite
这个方法的作用就是保证当前的buffer能够容纳从socket读取到的字节数,如果不够,就要扩容。
//确认读的大小
- (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;
}
}
GCDAsyncReadPacket
这个类也是存储数据的,并且这个类是读操作主要的类,为什么要有两个保存数据的buffer。可以看到注释 If we can read all the data directly into the packet's buffer without resizing it first, then we do so. Otherwise we use the prebuffer. 意思是说如果我们能够不用改变GCDAsyncReadPacket的buffer的大小,能够一次性将数据全部存放到GCDAsyncReadPacket的buffer里面,那么久使用GCDAsyncReadPacket,否则使用GCDAsyncSocketPreBuffer。具体的原因在这段注释上面有,我不是很懂,感兴趣的可以看一下。
@interface GCDAsyncReadPacket : NSObject{
@public
NSMutableData *buffer; //保存读取到的数据
NSUInteger startOffset; //偏移
NSUInteger bytesDone; //当前已经读取的大小
NSUInteger maxLength;
NSTimeInterval timeout; //超时时间
NSUInteger readLength; //要读取多少个字节数
NSData *term; //读取的分割符号
BOOL bufferOwner;
NSUInteger originalBufferLength;
long tag;
}
readLengthForTermWithPreBuffer:found:
这个方法根据分隔符去读取preBuffer,看看分隔符和分隔符之前的内容总共有多少字节.假如我们交互的报文的格式如下,由head+term+data组成,那么我们要读取这个数据的时候,首先要根据term读取到head,然后根据head里面的size读取到当前这个报文的data的size有多大,并且读取到type。
GCDAsyncSocket
这个类就是socket的主要类,包括创建socket,bind,listen,accept,以及读数据.我们分析的时候会省略掉ipv6、un和SSL证书的部分.下面列举出主要的字段
@implementation GCDAsyncSocket
{
uint32_t flags; //记录当前socket的状态
uint16_t config; //基础配置,可以设置禁用ipv4或者ipv6等
__weak id<GCDAsyncSocketDelegate> delegate; //代理,server收到连接或者是接收到数据的代理
dispatch_queue_t delegateQueue; //这个代理队列(demo里面传的是主队列,主队列是串行的)
int socket4FD; //从client连接到server的文件描述符,读写数据就是通过这个fd
NSData * connectInterface4; //ipv4的地址
dispatch_queue_t socketQueue; //accept是在这个队列,并且从socket4FD里面触发读数据也是在这个队列,必须是串行队列
dispatch_source_t accept4Source; //当有ipv4socket连接到server时候,会触发这个souce,然后里面调用accept来处理连接
dispatch_source_t readSource; //socket4FD的source源
NSMutableArray *readQueue; //读任务的数组。
GCDAsyncReadPacket *currentRead; //当前读的任务
GCDAsyncSocketPreBuffer *preBuffer; //缓冲区
unsigned long socketFDBytesAvailable; //当前socket里面有多少数据可读
}
init
初始化一个GCDAsyncSocket实例,设置代理,设置代理队列和socketQueue已经其他的写初始化工作
Configuration
设置代理,代理队列,ipv4,ipv6是否可用等
Accepting
创建server socket的主要方法,设置创建socket的参数,创建socket,listen,accept等都在这里
Disconnecting
断开连接的方法
Diagnostics
一些工具类方法,getter
reading
读数据的关键方法,主要有两种读的方法,一种是按照分隔符来读,一种是读取指定大小,这两种方法我们的demo都有用。因为我们的demo是采用head+term+data的模式,首先会按照分隔符来读,读到head,然后获取到data的size,然后再按照指定大小来读。
writing
写数据的方法
Security/Class Utilities等
这些就不用看了,需要提一下这个方法 +(NSData *)CRLFData,这个方法返回的是我们的分隔符,其实就是\r\n,我们的demo数据里面不会有\r\n,所以使用这个,假如我们data里面可能有\r\n,那么分隔符就得重新设计。
基本执行流程
创建socket的流程
TYHSocketManager -- initSocket
gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; //创建了一个GCDAsyncSocket对象,设置代理队列为主队列
TYHSocketManager -- accept
[gcdSocket acceptOnPort:Kport error:&error]; //在指定的端口接受连接.
接着看一下 acceptOnInterface:(NSString )inInterface port:(uint16_t)port error:(NSError *)errPtr 这个方法,这里我们省略了ipv4和一些校验已经警告等代码,我们会发现,这其实就是我们平时接触到的socket编程基本的api调用。 这个方法其实就是在socketQueue这个串行的队列里面创建了server的套接字,然后设置一个监听的source,当有可读的任务时,会在子线程串行队列socketQueue上面执行。
acceptOnInterface
- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr{
NSString *interface = [inInterface copy];
__block BOOL result = NO;
__block NSError *err = nil;
//创建socket的block
int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
int socketFD = socket(domain, SOCK_STREAM, 0);
int status;
//设置socket非阻塞
status = fcntl(socketFD, F_SETFL, O_NONBLOCK);
int reuseOn = 1;
status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]);
status = listen(socketFD, 1024);
return socketFD;
};
dispatch_block_t block = ^{ @autoreleasepool {
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port];
//创建socket
socket4FD = createSocket(AF_INET, interface4);
// 创建read source,当socket4FD可读的时候,会触发这个监听,在串行的socketQueue这个队列上执行
accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
int socketFD = socket4FD;
dispatch_source_t acceptSource = accept4Source;
__weak GCDAsyncSocket *weakSelf = self;
dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
//这个地方会在子线程触发,但是是运行在串行队列上
__strong GCDAsyncSocket *strongSelf = weakSelf;
unsigned long i = 0;
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));
}});
dispatch_resume(accept4Source);
//标记当前的server的socket为start
flags |= kSocketStarted;
result = YES;
}};
//这个主要是保证运行这个block是在我们的串行的socketQueue上运行
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
//这个是在主线程运行,但是不是在主队列,同步运行,会等上面的block执行完成后,才会执行下面的return
dispatch_sync(socketQueue, block);
return result;
}
doAccept
看一下doAccpet这个方法,也是去掉了一些ipv6和校验方法.这个方法里面调用了accept方法,获取到了childSocketFD,这个就是用来和client交互的套接字.当前我们还是在子线程(socketQueue)里面执行.接着异步到了delegateQueue执行,创建了一个GCDAsyncSocket实例,和childSocketFD关联。然后设置标记为了开始和连接.接着又异步到了acceptSocket的socketQueue里面,设置read source和write source。
- (BOOL)doAccept:(int)parentSocketFD{
int childSocketFD;
struct sockaddr_in addr;
socklen_t addrLen = sizeof(addr);
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
//设置为非阻塞
fcntl(childSocketFD, F_SETFL, O_NONBLOCK);
int nosigpipe = 1;
setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
//代理队列,是一个串行的,我们的demo里面传过来的是主队列
if (delegateQueue){
__strong id theDelegate = delegate;
//当前是在子线程,但是是在串行的socketQueue上
dispatch_async(delegateQueue, ^{ @autoreleasepool {
//这个地方回到了代理队列(主队列)
//根据连上的childSocketFD,创建一个GCDAsyncSokcet,队列还是delegateQueue这个串行的队列.但是socketQueue是重新创建的
GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate delegateQueue:delegateQueue socketQueue:NULL];
acceptedSocket->socket4FD = childSocketFD;
acceptedSocket->flags = (kSocketStarted | kConnected);
// Setup read and write sources for accepted socket
dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool {
//又到了子线程,socketQueue
[acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
}});
if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]){
//这句话还是在主队列执行的
[theDelegate socket:self didAcceptNewSocket:acceptedSocket];
}
}});
}
return YES;
}
setupReadAndWriteSourcesForNewlyConnectedSocket
设置readSource的监听,当socketFD有可读的数据的时候,会在当前的socketQueue上执行。我们每一个新的连接childSocket,是在串行的队列上执行的,这个队列就是和server相关的socketQueue上,然后每一个childFD又会创建一个新的GCDAsyncSocket实例acceptedSocket,和这个实例相关又创建一个串行的队列socketQueue,也就是每一个GCDAsyncSocket都有一个自己的串行队列socketQueue,还有同一个全局的主队列delegateQueue.每个childFD的读写操作都是在它自己的串行队列上处理。并且设置了当前可读的socketFDBytesAvailable为0。假如client给我们发送数据,就能够在socketQueue这个队列上处理,调用doReadData这个方法。doReadData我们先放一放。
- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD{
//设置读的回调,写的去掉了
readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue);
__weak GCDAsyncSocket *weakSelf = self;
dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool {
__strong GCDAsyncSocket *strongSelf = weakSelf;
/**
当前的readSource里面有多少可读的数据.这个地方可以注意一下。假如我的client给服务器发送的数据很少.
那么这个地方可能只触发一次,假如有很多呢(比如几十兆,我测试的时候发了30M的txt),那么就有可能触发多次,这个地方在后面的具体的读的方法会有所体现.
*/
strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource);
if (strongSelf->socketFDBytesAvailable > 0)
//真正读数据的方法
[strongSelf doReadData];
else
//可读的数据为0,读完了, EOF = End Of File
[strongSelf doReadEOF];
}});
socketFDBytesAvailable = 0;
flags &= ~kReadSourceSuspended;
dispatch_resume(readSource);
flags |= kSocketCanAcceptBytes;
flags |= kWriteSourceSuspended;
}
socket:didAcceptNewSocket:
这个地方是在delegateQueue主队列上执行,有新的连接来了,只是有连接,还没有发送数据。调用readDataToData这个方法是用来创建一个读数据的对象,然后设置当前按照分隔符来读(因为我们的demo发送的数据就是按照分隔符来区分的),后面我们会看到,继续往下看
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
//sock是当前的server对象.newSocket是连接到server的对象,我们需要持有newSocket对象,不然就释放了,将它添加在数组里面.然后调用readDataToData这个方法
[_sockets addObject:newSocket];
//这个方法其实就是按照分隔符来读,分隔符这里设置为[GCDAsyncSocket CRLFData]
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
}
readDataToData:withTimeout:buffer:bufferOffset:maxLength:tag:
当前我们创建了一个读包的GCDAsyncReadPacket对象packet,这个对象我们设置的buffer的大小是0,但是设置了terminator,我们从上面传过来的,因为我们读数据的时候,要先按照分隔符来读取头部,然后在按照长度读取data部分.所以这个packet是按照分隔符来读的
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)maxLength
tag:(long)tag {
//此时我们传递过来的buffer=nil,offset=0,maxLengtg=0
GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:maxLength timeout:timeout readLength:0 terminator:data tag:tag];
dispatch_async(socketQueue, ^{ @autoreleasepool {
//这里又到了子线程
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)){
//往读数据的数组里面添加一个pack
[readQueue addObject:packet];
//也许会有读的任务
[self maybeDequeueRead];
}
}});
}
maybeDequeueRead
这里只放关键的代码.运行到这里来的时候,是我们有新的连接进来,此时我们的currentRead这个是为nil的,并且readQueue也放进去了一个按照分隔符来读的readPack,那么就取出当前的读的“任务”赋值给 currentRead,然后移除读的任务
- (void)maybeDequeueRead{
// If we're not currently processing a read AND we have an available read stream
if ((currentRead == nil) && (flags & kConnected)){
if ([readQueue count] > 0){
currentRead = [readQueue objectAtIndex:0];
[readQueue removeObjectAtIndex:0];
// Setup read timer (if needed)
//设置超时时间的source
[self setupReadTimerWithTimeout:currentRead->timeout];
// Immediately read, if possible
//开始读数据
[self doReadData];
}
}
}
创建socket总结
1.在主线程非主队列socketQueue创建了server的socket,调用了bind,listen函数来监听连接。
2.然后创建了一个read source,任务会在子线程但是socketQueue队列上执行。
3.有客户端连接的话,会在子线程获取到childSocket,然后回到主队列创建acceptSocket。
4.然后会在acceptSocket的socketQueue上创建read source监听,用来监听client向server写数据。当有些数据的时候,回到用doReadData方法。
5.在主队列delegateQueue上执行代理方法.创建一个GCDAsyncReadPacket的pack,是一个按照分隔符读取的pack。
6.然后在acceptSocket的socketQueue上调用maybeDequeueRead,然后调用doReadData。
注意
我们每一个GCDAsyncSocket对象都有自己的一个socketQueue,包括server的GCDAsyncSocket对象和客户端连接后创建的GCDAsyncSocket这个对象,但是他们都使用同一个delegateQueue,每一个childSocket的readSource都是在自己的socketQueue里面处理,和其他的连接没有关系,和server的socketQueue也没有关系。上图的1-3步骤是在server sokcet的socketQueue里面执行,6-8步骤是在连接上server的socketQueue上执行的。
读数据
doReadData
我们先解决上面的那个问题,会有两部分调用到doReadData,分别为建立连接之后的代理然后创建readPact和从read source监听到可读时。先看doReadData前面的方法.
1.我们客户端建立连接后,没有给服务器发送数据,那么此时currentRead不为nil,会网下执行,但是此时socketFDBytesAvailable=0,没有可读数据,并且[preBuffer availableBytes]==0,那么就会return,假如后面read source有数据来了,会往下执行。
2.假如我们在主队列那个代理那个地方延时一段时间,我直接sleep(1000),然后client向服务器发送数据,那么此时currentRead == nil,此时会将read source挂起,不会继续读数据,同样也会return,但是此时的socketFDBytesAvailable是有值的,等主队列运行到这里之后,就会往下执行。
//什么时候这个currrentRead会为空,从上面我们知道,在主队列的回调里面会创建这个currentRead
if ((currentRead == nil) || (flags & kReadsPaused)){
if (socketFDBytesAvailable > 0){
[self suspendReadSource];
}
return;
}
BOOL hasBytesAvailable = NO;
unsigned long estimatedBytesAvailable = 0;
//socketFDBytesAvailable这个值是在read source的监听里面赋值的。
estimatedBytesAvailable = socketFDBytesAvailable;
hasBytesAvailable = (estimatedBytesAvailable > 0);
if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)){
[self resumeReadSource];
return;
}
三种读取数据的方式
看注释我们发现有三种读数据的方式
- 读取所有可读的数据
- 读取固定长度的数据
- 按照指定分隔符读取数据 我们这个demo会涉及到2,3两种读数据的方法
// 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.
读取流程
读取流程介绍
客户端给服务器发送下面的二进制数据流.如下数据我们该怎么读
head = {
"type": "txt",
"size": "60" (这个60是data部分的长度,不是总报文的长度)
}
term = "\r\n";
data = {"GCDAsyncSocket源码解读"}
- 假如从readSource监听里面我们得到当前可读取的数据大小为70,包括head、term、data
- 分配一个可以容纳70个字节大小的缓冲区preBuffer对象,preBuffer包括一个指向数据空间首地址的指针preBuffer,一个writePointer,一个readPointer,开始的时候这三个指针都指向开始时位置。
- 从socket读取数据,将数据存储在preBuffer的preBuffer上,此时往preBuffer上存储了70个字节的数据,那么移动writePonter70个字节,表示我们已经朝preBuffer上写了70个字节的数据.
- 我们遍历这个preBuffer里面的数据,查找到term的位置,然后读取到head部分的数据.假如head和term占10个字节.我们将head数据存储起来,放在一个对象上面,准备返回回去.
- 然后移动readPointe10个字节,表示我们当前读取了10个字节,还剩下writePointer-readPointer的内容没有读取
- 获取到head之后,我们从head里面解析出数据部分有60,那么我们接着从preBuffer里面,读取剩下的60个字节的内容,就是data.
- 数据全部读取完成之后,我们重置writePointer和readPointer的位置.
read from socket
将数据读取到preBuffer
第一次此时client向server发送数据时候,我们的preBuffe人里面是没有数据的,所以不会从preBuffer里面读取.会直接从socket里面读取。
读取过程如下:
- 此时我们的currentRead->term不为空,按照分隔符来读,此时我们的currentRead的buffer的length=0,所以readIntoPreBuffer=true,会往临时缓存里面读。
- preBuffer在初始化的时候给了一个大小1024个字节.[preBuffer ensureCapacityForWrite]这个方法保证我们的preBuffer有足够的大小能够存放数据,大小不够的话就扩容,可以看下这个方法
- 获取到preBuffer的writerPonter,赋值给buffer。
- 从socketFD上读取指定字节到buffer上,就是调用了read函数
{
NSUInteger bytesToRead;
if (currentRead->term != nil){
// Read type #3 - read up to a terminator
bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer];
}else{
bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable];
}
if (bytesToRead > SIZE_MAX) {
bytesToRead = SIZE_MAX;
}
if (readIntoPreBuffer){
/*
这个方法用来确保preBuffer能够存放bytesToRead这么多个字节.
1:假如有2048个字节要存储,bytesToRead = 2048,
2:但是preBuffer的size只有1024,那么就会重新开启 1024 + (2048 - 1024) 个字节的空间
3: 并且调整preBuffer的各种指向
*/
[preBuffer ensureCapacityForWrite:bytesToRead];
//这个是当前的preBuffer的writeBuffer指针的位置.表示将当前的数据从那里开始写
buffer = [preBuffer writeBuffer];
}
else{
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];
buffer = (uint8_t *)[currentRead->buffer mutableBytes]
+ currentRead->startOffset
+ currentRead->bytesDone;
}
int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
if (result < 0){ //读取失败
if (errno == EWOULDBLOCK)
waiting = YES;
else
error = [self errnoErrorWithReason:@"Error in read() function"];
socketFDBytesAvailable = 0;
}else if (result == 0){
//没有读到内容,读到末尾了 EOF = End Of File (文件末尾)
socketEOF = YES;
socketFDBytesAvailable = 0;
}else{
//读到了多少字节
bytesRead = result;
/*
bytesRead:实际读到的字节数
bytesToRead:读取的字节数.
我要读10个字节,只返回了8个字节,说明丢包了。
*/
if (bytesRead < bytesToRead){
socketFDBytesAvailable = 0;
}else{
if (socketFDBytesAvailable <= bytesRead)
socketFDBytesAvailable = 0;
else
socketFDBytesAvailable -= bytesRead;
}
if (socketFDBytesAvailable == 0){
waiting = YES;
}
}
}
将数据从preBuffer拷贝到currentRead
- 移动了preBuffer的write指针,表示往preBuffer上写了多少内容.
- readLengthForTermWithPreBuffer 查找分隔符,头部大小.
- 确保currentRead有足够的空间,同样也会扩容
- memcpy将头部的从preBuffer拷贝到curretRead的buffer上.此时preBuffer里面的内容还没有读完.
- 移动preBuffer的read指针,表示当前已经读取了head和term个字节。
- 修改currentRead的bytesDone,表示当前读取完了多少字节,head读取完毕
- 调用completeCurrentRead,然后在deletageQueue调用代理方法,将读取到的head返回回去.
- 接着从head里面获取到data的大小,然后按照指定字节的大小来读,调用方法readDataToLength:withTimeout:tag:
if (readIntoPreBuffer)
{
//移动preBuffer的writeBuffer指针.writeBuffer是从socket读取了bytesRead字节数据,存储在了writeBuffer的位置.
[preBuffer didWrite:bytesRead];
NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy];
uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ currentRead->bytesDone;
//将preBuffer里面bytesToCopy字节的内容拷贝至readBuf上
memcpy(readBuf, [preBuffer readBuffer], bytesToCopy);
//移动preBuffer的readPointer指针,表示当前已经读取了bytesToCopy个字节
[preBuffer didRead:bytesToCopy];
//修改currentRead的bytesDone,当前已经读取了bytesToCopy字节的内容
currentRead->bytesDone += bytesToCopy;
totalBytesReadForCurrentRead += bytesToCopy;
}
read from buffer
此时又创建了一个GCDAsyncReadPacket,这个是按照数据大小来读的,因为我们前面已经从head里面获取到了data的大小
- 从上面读取到这里,此时我们的preBuffer的availabelBytes是有值的,因为我们之前没有读完,之前只读取了头
- ensureCapacityForAdditionalDataOfLength确保有足够的空间
- memcpy将preBuffer的数据复制到currentRead
- 移动preBuffer的read指针,此时数据读完,发现readPointer==writePointer,就重置readPointer和writePointer.
- done = (currentRead->bytesDone == currentRead->readLength); 这一部分的判断的作用是,假如我client给server发送了30M的数据,那么可能多次触发readSource,只有当累计读取到的数据和头部的size大小一样时,才算是读取完毕。
if ([preBuffer availableBytes] > 0){
if (currentRead->term != nil){
bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
}else{
bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]];
}
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy];
uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset+currentRead->bytesDone;
//从preBuffer的readBuffer读取bytesToCopy字节到buffer上
memcpy(buffer, [preBuffer readBuffer], bytesToCopy);
//移动preBuffer的readPointer指针,并且如果readPointer和writePointer相等了,就重置readPointer和writePointer
[preBuffer didRead:bytesToCopy];
currentRead->bytesDone += bytesToCopy;
totalBytesReadForCurrentRead += bytesToCopy;
if (currentRead->readLength > 0){
//当前读取到的字节数bytesDone和需要读取的字节数readLength相等
done = (currentRead->bytesDone == currentRead->readLength);
}else if (currentRead->term != nil){
if (!done && currentRead->maxLength > 0){
if (currentRead->bytesDone >= currentRead->maxLength){
error = [self readMaxedOutError];
}
}
} else{
done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength));
}
}
至此,GCDAsyncSocket的server端读取数据已经分析完毕.
总结
- 通过对GCDAsyncSocket代码的阅读,对socket编程有一个更具体的认识。
- 知道了一些解析buffer的套路,对数据流有了进一步的认识.
- 其实通过这个CS-Demo,对三次握手的过程有了更深的认识,包括发送数据有两条,确认包可能只有一条这种现象用抓包工具多次遇到
- 假如我client write了很大的一条数据给server,分包这个过程TCP、IP层都帮我们处理好了,我们接收到的buffer是拼装好的,我们需要约定一些读取策略,能让我们正确的解析data