GCDAsyncSocket源码解析

1,334 阅读17分钟

GCDAsyncSocket源码
本文使用demo的代码

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里面写了多少)
}

主要结构如下图 图片1.png

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。 截屏2021-03-19 上午9.56.47.png

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上执行的。

截屏2021-03-19 下午2.28.22.png

读数据

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

三种读取数据的方式

看注释我们发现有三种读数据的方式

  1. 读取所有可读的数据
  2. 读取固定长度的数据
  3. 按照指定分隔符读取数据 我们这个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源码解读"}
  1. 假如从readSource监听里面我们得到当前可读取的数据大小为70,包括head、term、data
  2. 分配一个可以容纳70个字节大小的缓冲区preBuffer对象,preBuffer包括一个指向数据空间首地址的指针preBuffer,一个writePointer,一个readPointer,开始的时候这三个指针都指向开始时位置。
  3. 从socket读取数据,将数据存储在preBuffer的preBuffer上,此时往preBuffer上存储了70个字节的数据,那么移动writePonter70个字节,表示我们已经朝preBuffer上写了70个字节的数据.
  4. 我们遍历这个preBuffer里面的数据,查找到term的位置,然后读取到head部分的数据.假如head和term占10个字节.我们将head数据存储起来,放在一个对象上面,准备返回回去.
  5. 然后移动readPointe10个字节,表示我们当前读取了10个字节,还剩下writePointer-readPointer的内容没有读取
  6. 获取到head之后,我们从head里面解析出数据部分有60,那么我们接着从preBuffer里面,读取剩下的60个字节的内容,就是data.
  7. 数据全部读取完成之后,我们重置writePointer和readPointer的位置.

截屏2021-03-19 下午3.23.50.png

read from socket

将数据读取到preBuffer

第一次此时client向server发送数据时候,我们的preBuffe人里面是没有数据的,所以不会从preBuffer里面读取.会直接从socket里面读取。
读取过程如下:

  1. 此时我们的currentRead->term不为空,按照分隔符来读,此时我们的currentRead的buffer的length=0,所以readIntoPreBuffer=true,会往临时缓存里面读。
  2. preBuffer在初始化的时候给了一个大小1024个字节.[preBuffer ensureCapacityForWrite]这个方法保证我们的preBuffer有足够的大小能够存放数据,大小不够的话就扩容,可以看下这个方法
  3. 获取到preBuffer的writerPonter,赋值给buffer。
  4. 从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

  1. 移动了preBuffer的write指针,表示往preBuffer上写了多少内容.
  2. readLengthForTermWithPreBuffer 查找分隔符,头部大小.
  3. 确保currentRead有足够的空间,同样也会扩容
  4. memcpy将头部的从preBuffer拷贝到curretRead的buffer上.此时preBuffer里面的内容还没有读完.
  5. 移动preBuffer的read指针,表示当前已经读取了head和term个字节。
  6. 修改currentRead的bytesDone,表示当前读取完了多少字节,head读取完毕
  7. 调用completeCurrentRead,然后在deletageQueue调用代理方法,将读取到的head返回回去.
  8. 接着从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的大小

  1. 从上面读取到这里,此时我们的preBuffer的availabelBytes是有值的,因为我们之前没有读完,之前只读取了头
  2. ensureCapacityForAdditionalDataOfLength确保有足够的空间
  3. memcpy将preBuffer的数据复制到currentRead
  4. 移动preBuffer的read指针,此时数据读完,发现readPointer==writePointer,就重置readPointer和writePointer.
  5. 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端读取数据已经分析完毕.

总结

  1. 通过对GCDAsyncSocket代码的阅读,对socket编程有一个更具体的认识。
  2. 知道了一些解析buffer的套路,对数据流有了进一步的认识.
  3. 其实通过这个CS-Demo,对三次握手的过程有了更深的认识,包括发送数据有两条,确认包可能只有一条这种现象用抓包工具多次遇到
  4. 假如我client write了很大的一条数据给server,分包这个过程TCP、IP层都帮我们处理好了,我们接收到的buffer是拼装好的,我们需要约定一些读取策略,能让我们正确的解析data